calcDaysIntervalNamedIndex for force using named index and returning only named index calcDaysIntervalNumIndex for force using numeric index and returning only numeric index
811 lines
25 KiB
PHP
811 lines
25 KiB
PHP
<?php
|
|
|
|
/*
|
|
* date convert and check functions
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace CoreLibs\Combined;
|
|
|
|
use Exception;
|
|
|
|
class DateTime
|
|
{
|
|
/** @var array<int,string> */
|
|
public const DAY_SHORT = [
|
|
1 => 'Mon',
|
|
2 => 'Tue',
|
|
3 => 'Wed',
|
|
4 => 'Thu',
|
|
5 => 'Fri',
|
|
6 => 'Sat',
|
|
7 => 'Sun',
|
|
];
|
|
/** @var array<int,string> */
|
|
public const DAY_LONG = [
|
|
1 => 'Monday',
|
|
2 => 'Tuesday',
|
|
3 => 'Wednesday',
|
|
4 => 'Thursday',
|
|
5 => 'Friday',
|
|
6 => 'Saturday',
|
|
7 => 'Sunday',
|
|
];
|
|
/** @var array<int,string> */
|
|
public const MONTH_LONG = [
|
|
1 => 'January',
|
|
2 => 'February',
|
|
3 => 'March',
|
|
4 => 'April',
|
|
5 => 'May',
|
|
6 => 'June',
|
|
7 => 'July',
|
|
8 => 'August',
|
|
9 => 'September',
|
|
10 => 'October',
|
|
11 => 'November',
|
|
12 => 'December',
|
|
];
|
|
/** @var array<int,string> */
|
|
public const MONTH_SHORT = [
|
|
1 => 'Jan',
|
|
2 => 'Feb',
|
|
3 => 'Mar',
|
|
4 => 'Apr',
|
|
5 => 'May',
|
|
6 => 'Jun',
|
|
7 => 'Jul',
|
|
8 => 'Aug',
|
|
9 => 'Sep',
|
|
10 => 'Oct',
|
|
11 => 'Nov',
|
|
12 => 'Dec',
|
|
];
|
|
|
|
/**
|
|
* a simple wrapper for the date format
|
|
* if an invalid timestamp is give zero timestamp unix time is used
|
|
*
|
|
* @param int|float $timestamp unix timestamp
|
|
* @param bool $show_micro show the micro time (default false)
|
|
* @param bool $micro_as_float Add the micro time with . instead
|
|
* of ms (default false)
|
|
* @return string formated date+time in Y-M-D h:m:s ms
|
|
*/
|
|
public static function dateStringFormat(
|
|
int|float $timestamp,
|
|
bool $show_micro = false,
|
|
bool $micro_as_float = false
|
|
): string {
|
|
// split up the timestamp, assume . in timestamp
|
|
// array pad $ms if no microtime
|
|
list ($timestamp, $ms) = array_pad(explode('.', (string)round($timestamp, 4)), 2, null);
|
|
$string = date("Y-m-d H:i:s", (int)$timestamp);
|
|
if ($show_micro && $ms) {
|
|
if ($micro_as_float == false) {
|
|
$string .= ' ' . $ms . 'ms';
|
|
} else {
|
|
$string .= '.' . $ms;
|
|
}
|
|
}
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* formats a timestamp into interval, not into a date
|
|
*
|
|
* @param string|int|float $timestamp interval in seconds and optional
|
|
* float micro seconds
|
|
* @param bool $show_micro show micro seconds, default true
|
|
* @return string interval formatted string or string as is
|
|
*/
|
|
public static function timeStringFormat(
|
|
string|int|float $timestamp,
|
|
bool $show_micro = true
|
|
): string {
|
|
// check if the timestamp has any h/m/s/ms inside, if yes skip
|
|
if (preg_match("/(h|m|s|ms)/", (string)$timestamp)) {
|
|
return (string)$timestamp;
|
|
}
|
|
// split to 6 (nano seconds)
|
|
list($timestamp, $ms) = array_pad(explode('.', (string)round((float)$timestamp, 6)), 2, null);
|
|
// if micro seconds is on and we have none, set to 0
|
|
if ($show_micro && $ms === null) {
|
|
$ms = 0;
|
|
}
|
|
// if negative remember
|
|
$negative = false;
|
|
if ((int)$timestamp < 0) {
|
|
$negative = true;
|
|
}
|
|
$timestamp = abs((float)$timestamp);
|
|
$timegroups = [86400, 3600, 60, 1];
|
|
$labels = ['d', 'h', 'm', 's'];
|
|
$time_string = '';
|
|
// if timestamp is zero, return zero string
|
|
if ($timestamp == 0) {
|
|
// if no seconds and we have no microseconds either, show no micro seconds
|
|
if ($ms == 0) {
|
|
$ms = null;
|
|
}
|
|
$time_string = '0s';
|
|
} else {
|
|
for ($i = 0, $iMax = count($timegroups); $i < $iMax; $i++) {
|
|
$output = floor((float)$timestamp / $timegroups[$i]);
|
|
$timestamp = (float)$timestamp % $timegroups[$i];
|
|
// output has days|hours|min|sec
|
|
if ($output || $time_string) {
|
|
$time_string .= $output . $labels[$i] . (($i + 1) != count($timegroups) ? ' ' : '');
|
|
}
|
|
}
|
|
}
|
|
// only add ms if we have an ms value
|
|
if ($ms !== null) {
|
|
// prefix the milliseoncds with 0. and round it max 3 digits and then convert to int
|
|
$ms = round((float)('0.' . $ms), 3) * 1000;
|
|
// add ms if there
|
|
if ($show_micro) {
|
|
$time_string .= ' ' . $ms . 'ms';
|
|
} elseif (!$time_string) {
|
|
$time_string .= $ms . 'ms';
|
|
}
|
|
}
|
|
if ($negative) {
|
|
$time_string = '-' . $time_string;
|
|
}
|
|
return (string)$time_string;
|
|
}
|
|
|
|
/**
|
|
* update timeStringFormat with year and month support
|
|
*
|
|
* The following flags have to be set to be timeStringFormat compatible.
|
|
* Not that on seconds overflow this method will throw an exception, timeStringFormat returned -1s
|
|
* show_only_days: true,
|
|
* skip_zero: false,
|
|
* skip_last_zero: false,
|
|
* truncate_nanoseconds: true,
|
|
* truncate_zero_seconds_if_microseconds: false
|
|
*
|
|
* @param int|float $seconds Seconds to convert, maxium 6 decimals,
|
|
* else \UnexpectedValueException will be thrown
|
|
* if days too large or years too large \LengthException is thrown
|
|
* @param string $truncate_after [=''] Truncate after which time name, will not round, hard end
|
|
* values are parts names or interval short names (y, d, f, ...)
|
|
* if illegal value \UnexpectedValueException is thrown
|
|
* @param bool $natural_seperator [=false] use ',' and 'and', if off use space
|
|
* @param bool $name_space_seperator [=false] add a space between the number and the time name
|
|
* @param bool $show_microseconds [=true] show microseconds
|
|
* @param bool $short_time_name [=true] use the short time names (eg s instead of seconds)
|
|
* @param bool $skip_last_zero [=true] skip all trailing zero values, eg 5m 0s => 5m
|
|
* @param bool $skip_zero [=true] do not show zero values anywhere, eg 1h 0m 20s => 1h 20s
|
|
* @param bool $show_only_days [=false] do not show years or months, show only days
|
|
* if truncate after is set to year or month
|
|
* throws \UnexpectedValueException
|
|
* @param bool $auto_fix_microseconds [=false] if the micro seconds decimals are more than 6, round them
|
|
* on defaul throw \UnexpectedValueException
|
|
* @param bool $truncate_nanoseconds [=false] if microseconds decimals >3 then normal we show 123.4ms
|
|
* cut the .4 is set to true
|
|
* @param bool $truncate_zero_seconds_if_microseconds [=true] if we have 0.123 seconds then if true no seconds
|
|
* will be shown
|
|
* @return string
|
|
* @throws \UnexpectedValueException if seconds has more than 6 decimals
|
|
* if truncate has an illegal value
|
|
* if truncate is set to year or month and show_only_days is turned on
|
|
* @throws \LengthException if seconds is too large and show_days_only is selected and days is negetive
|
|
* or if years is negativ
|
|
*/
|
|
public static function intervalStringFormat(
|
|
int|float $seconds,
|
|
string $truncate_after = '',
|
|
bool $natural_seperator = false,
|
|
bool $name_space_seperator = false,
|
|
bool $show_microseconds = true,
|
|
bool $short_time_name = true,
|
|
bool $skip_last_zero = true,
|
|
bool $skip_zero = true,
|
|
bool $show_only_days = false,
|
|
bool $auto_fix_microseconds = false,
|
|
bool $truncate_nanoseconds = false,
|
|
bool $truncate_zero_seconds_if_microseconds = true,
|
|
): string {
|
|
// auto fix long seconds, else \UnexpectedValueException will be thrown on error
|
|
// check if we have float and -> round to 6
|
|
if ($auto_fix_microseconds === true && is_float($seconds)) {
|
|
$seconds = round($seconds, 6);
|
|
}
|
|
// flag negative + set abs
|
|
$negative = $seconds < 0 ? '-' : '';
|
|
$seconds = abs($seconds);
|
|
// create base time
|
|
$date_now = new \DateTime("@0");
|
|
try {
|
|
$date_seconds = new \DateTime("@$seconds");
|
|
} catch (\Exception $e) {
|
|
throw new \UnexpectedValueException(
|
|
'Seconds value is invalid, too large or more than six decimals: ' . $seconds,
|
|
1,
|
|
$e
|
|
);
|
|
}
|
|
$interval = date_diff($date_now, $date_seconds);
|
|
// if show_only_days and negative but input postive alert that this has to be done in y/m/d ...
|
|
if ($interval->y < 0) {
|
|
throw new \LengthException('Input seconds value is too large for years output: ' . $seconds, 2);
|
|
} elseif ($interval->days < 0 && $show_only_days === true) {
|
|
throw new \LengthException('Input seconds value is too large for days output: ' . $seconds, 3);
|
|
}
|
|
$parts = [
|
|
'years' => 'y',
|
|
'months' => 'm',
|
|
'days' => 'd',
|
|
'hours' => 'h',
|
|
'minutes' => 'i',
|
|
'seconds' => 's',
|
|
'microseconds' => 'f',
|
|
];
|
|
$short_name = [
|
|
'years' => 'y', 'months' => 'm', 'days' => 'd',
|
|
'hours' => 'h', 'minutes' => 'm', 'seconds' => 's',
|
|
'microseconds' => 'ms'
|
|
];
|
|
// $skip = false;
|
|
if (!empty($truncate_after)) {
|
|
// if truncate after not in key or value in parts
|
|
if (!in_array($truncate_after, array_keys($parts)) && !in_array($truncate_after, array_values($parts))) {
|
|
throw new \UnexpectedValueException(
|
|
'truncate_after has an invalid value: ' . $truncate_after,
|
|
4
|
|
);
|
|
}
|
|
// if truncate after is y or m and we have show_only_days, throw exception
|
|
if ($show_only_days === true && in_array($truncate_after, ['y', 'years', 'm', 'months'])) {
|
|
throw new \UnexpectedValueException(
|
|
'If show_only_days is turned on, the truncate_after cannot be years or months: '
|
|
. $truncate_after,
|
|
5
|
|
);
|
|
}
|
|
// $skip = true;
|
|
}
|
|
$formatted = [];
|
|
$zero_formatted = [];
|
|
$value_set = false;
|
|
$add_zero_seconds = false;
|
|
foreach ($parts as $time_name => $part) {
|
|
if (
|
|
// skip for micro seconds
|
|
($show_microseconds === false && $part == 'f') ||
|
|
// skip for if days only and we have year or month
|
|
($show_only_days === true && in_array($part, ['y', 'm']))
|
|
) {
|
|
continue;
|
|
}
|
|
$add_value = 0;
|
|
if ($show_only_days === true && $part == 'd') {
|
|
$value = $interval->days;
|
|
} else {
|
|
$value = $interval->$part;
|
|
}
|
|
// print "-> V: $value | $part, $time_name"
|
|
// . " | Set: " . ($value_set ? 'Y' : 'N') . ", SkipZ: " . ($skip_zero ? 'Y' : 'N')
|
|
// . " | SkipLZ: " . ($skip_last_zero ? 'Y' : 'N')
|
|
// . " | " . ($value != 0 ? 'Not zero' : 'ZERO') . "<br>";
|
|
if ($value != 0) {
|
|
if ($part == 'f') {
|
|
if ($truncate_nanoseconds === true) {
|
|
$value = round($value, 3);
|
|
}
|
|
$value *= 1000;
|
|
// anything above that is nano seconds?
|
|
}
|
|
if ($value) {
|
|
$value_set = true;
|
|
}
|
|
$add_value = 1;
|
|
} elseif (
|
|
$value == 0 &&
|
|
$value_set === true && (
|
|
$skip_last_zero === false ||
|
|
$skip_zero === false
|
|
)
|
|
) {
|
|
$add_value = 2;
|
|
}
|
|
// echo "ADD VALUE: $add_value<br>";
|
|
if ($add_value) {
|
|
// build format
|
|
$format = "$value";
|
|
if ($name_space_seperator) {
|
|
$format .= " ";
|
|
}
|
|
if ($short_time_name) {
|
|
$format .= $short_name[$time_name];
|
|
} elseif ($value == 1) {
|
|
$format .= substr($time_name, 0, -1);
|
|
} else {
|
|
$format .= $time_name;
|
|
}
|
|
if ($add_value == 1) {
|
|
if (count($zero_formatted) && $skip_zero === false) {
|
|
$formatted = array_merge($formatted, $zero_formatted);
|
|
}
|
|
$zero_formatted = [];
|
|
$formatted[] = $format;
|
|
} elseif ($add_value == 2) {
|
|
$zero_formatted[] = $format;
|
|
}
|
|
}
|
|
// if seconds is zero
|
|
if (
|
|
$part == 's' && $value == 0 &&
|
|
$show_microseconds === true &&
|
|
$truncate_zero_seconds_if_microseconds === false
|
|
) {
|
|
$add_zero_seconds = true;
|
|
}
|
|
// stop after a truncate is matching
|
|
if ($part == $truncate_after || $truncate_after == $time_name) {
|
|
break;
|
|
}
|
|
}
|
|
// add all zero entries if we have skip last off
|
|
if (count($zero_formatted) && $skip_last_zero === false) {
|
|
$formatted = array_merge($formatted, $zero_formatted);
|
|
}
|
|
// print "=> F: " . print_r($formatted, true)
|
|
// . " | Z: " . print_r($zero_list, true)
|
|
// . " | ZL: " . print_r($zero_last_list, true)
|
|
// . "<br>";
|
|
if (count($formatted) == 0) {
|
|
// if we have truncate on, then we assume nothing was found
|
|
if (!empty($truncate_after)) {
|
|
if (in_array($truncate_after, array_values($parts))) {
|
|
$truncate_after = array_flip($parts)[$truncate_after];
|
|
}
|
|
$time_name = $truncate_after;
|
|
} else {
|
|
$time_name = 'seconds';
|
|
}
|
|
return '0' . ($name_space_seperator ? ' ' : '')
|
|
. ($short_time_name ? $short_name[$time_name] : $time_name);
|
|
} elseif (count($formatted) == 1) {
|
|
return $negative .
|
|
($add_zero_seconds ?
|
|
'0'
|
|
. ($name_space_seperator ? ' ' : '')
|
|
. ($short_time_name ? $short_name['seconds'] : 'seconds')
|
|
. ' '
|
|
: ''
|
|
)
|
|
. $formatted[0];
|
|
} elseif ($natural_seperator === false) {
|
|
return $negative . implode(' ', $formatted);
|
|
} else {
|
|
$str = implode(', ', array_slice($formatted, 0, -1));
|
|
if (!empty($formatted[count($formatted) - 1])) {
|
|
$str .= ' and ' . (string)array_pop($formatted);
|
|
}
|
|
return $negative . $str;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* does a reverse of the timeStringFormat and converts the string from
|
|
* xd xh xm xs xms to a timestamp.microtime format
|
|
*
|
|
* @param string|int|float $timestring formatted interval
|
|
* @return string|int|float converted float interval, or string as is
|
|
*/
|
|
public static function stringToTime(string|int|float $timestring): string|int|float
|
|
{
|
|
$timestamp = 0;
|
|
if (!preg_match("/(d|h|m|s|ms)/", (string)$timestring)) {
|
|
return $timestring;
|
|
}
|
|
$timestring = (string)$timestring;
|
|
// pos for preg match read + multiply factor
|
|
$timegroups = [2 => 86400, 4 => 3600, 6 => 60, 8 => 1];
|
|
$matches = [];
|
|
// if start with -, strip and set negative
|
|
$negative = false;
|
|
if (preg_match("/^-/", $timestring)) {
|
|
$negative = true;
|
|
$timestring = substr($timestring, 1);
|
|
}
|
|
// preg match: 0: full string
|
|
// 2, 4, 6, 8 are the to need values
|
|
preg_match("/^((\d+)d ?)?((\d+)h ?)?((\d+)m ?)?((\d+)s ?)?((\d+)ms)?$/", $timestring, $matches);
|
|
// multiply the returned matches and sum them up. the last one (ms) is added with .
|
|
foreach ($timegroups as $i => $time_multiply) {
|
|
if (isset($matches[$i]) && is_numeric($matches[$i])) {
|
|
$timestamp += (float)$matches[$i] * $time_multiply;
|
|
}
|
|
}
|
|
if (isset($matches[10]) && is_numeric($matches[10])) {
|
|
$timestamp .= '.' . $matches[10];
|
|
}
|
|
if ($negative) {
|
|
// cast to flaot so we can do a negative multiplication
|
|
$timestamp = (float)$timestamp * -1;
|
|
}
|
|
return $timestamp;
|
|
}
|
|
|
|
/**
|
|
* Returns long or short day of week name based on ISO day of week number
|
|
* 1: Monday
|
|
* ...
|
|
* 7: Sunday
|
|
*
|
|
* @param int $isodow 1: Monday, 7: Sunday
|
|
* @param bool $long Default false 'Mon', if true 'Monday'
|
|
* @return string Day of week string either short 'Mon' or long 'Monday'
|
|
*/
|
|
public static function setWeekdayNameFromIsoDow(int $isodow, bool $long = false): string
|
|
{
|
|
// if not valid, set to invalid
|
|
if ($isodow < 1 || $isodow > 7) {
|
|
return $long ? 'Invalid' : 'Inv';
|
|
}
|
|
return date($long ? 'l' : 'D', strtotime("Sunday +{$isodow} days") ?: null);
|
|
}
|
|
|
|
/**
|
|
* Get the day of week Name from date
|
|
*
|
|
* @param string $date Any valid date
|
|
* @param bool $long Default false 'Mon', if true 'Monday'
|
|
* @return string Day of week string either short 'Mon' or long 'Monday'
|
|
*/
|
|
public static function setWeekdayNameFromDate(string $date, bool $long = false): string
|
|
{
|
|
if (!self::checkDate($date)) {
|
|
return $long ? 'Invalid' : 'Inv';
|
|
}
|
|
return date($long ? 'l' : 'D', strtotime($date) ?: null);
|
|
}
|
|
|
|
/**
|
|
* Get the day of week Name from date
|
|
*
|
|
* @param string $date Any valid date
|
|
* @return int ISO Weekday number 1: Monday, 7: Sunday, -1 for invalid date
|
|
*/
|
|
public static function setWeekdayNumberFromDate(string $date): int
|
|
{
|
|
if (!self::checkDate($date)) {
|
|
return -1;
|
|
}
|
|
return (int)date('N', strtotime($date) ?: null);
|
|
}
|
|
|
|
/**
|
|
* splits & checks date, wrap around for check_date function
|
|
*
|
|
* @param string $date a date string in the format YYYY-MM-DD
|
|
* @return bool true if valid date, false if date not valid
|
|
*/
|
|
public static function checkDate(string $date): bool
|
|
{
|
|
if (empty($date)) {
|
|
return false;
|
|
}
|
|
list ($year, $month, $day) = array_pad(
|
|
preg_split("/[\/-]/", $date) ?: [],
|
|
3,
|
|
null
|
|
);
|
|
if (!$year || !$month || !$day) {
|
|
return false;
|
|
}
|
|
if (!checkdate((int)$month, (int)$day, (int)$year)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* splits & checks date, wrap around for check_date function
|
|
*
|
|
* @param string $datetime date (YYYY-MM-DD) + time (HH:MM:SS), SS can be dropped
|
|
* @return bool true if valid date, false if date not valid
|
|
*/
|
|
public static function checkDateTime(string $datetime): bool
|
|
{
|
|
if (!$datetime) {
|
|
return false;
|
|
}
|
|
// catch last overflow if sec has - in front
|
|
list ($year, $month, $day, $hour, $min, $sec, $sec_overflow) = array_pad(
|
|
preg_split("/[\/\- :]/", $datetime) ?: [],
|
|
7,
|
|
null
|
|
);
|
|
if (!$year || !$month || !$day) {
|
|
return false;
|
|
}
|
|
if (!checkdate((int)$month, (int)$day, (int)$year)) {
|
|
return false;
|
|
}
|
|
if (!is_numeric($hour) || !is_numeric($min)) {
|
|
return false;
|
|
}
|
|
if (!empty($sec) && !is_numeric($sec)) {
|
|
return false;
|
|
}
|
|
if (!empty($sec) && ($sec < 0 || $sec > 60)) {
|
|
return false;
|
|
};
|
|
// in case we have - for seconds
|
|
if (!empty($sec_overflow)) {
|
|
return false;
|
|
}
|
|
if (
|
|
($hour < 0 || $hour > 24) ||
|
|
($min < 0 || $min > 60)
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* compares two dates, tries to convert them via strtotime to timestamps
|
|
* returns int/bool in:
|
|
* -1 if the first date is smaller the last
|
|
* 0 if both are equal
|
|
* 1 if the first date is bigger than the last
|
|
* false if date validation/conversion failed
|
|
*
|
|
* @param string $start_date start date string in YYYY-MM-DD
|
|
* @param string $end_date end date string in YYYY-MM-DD
|
|
* @return int int -1 (s<e)/0 (s=e)/1 (s>e) as difference
|
|
* @throws \UnexpectedValueException On empty start/end values
|
|
*/
|
|
public static function compareDate(string $start_date, string $end_date): int
|
|
{
|
|
// pre check for empty or wrong
|
|
if ($start_date == '--' || $end_date == '--' || empty($start_date) || empty($end_date)) {
|
|
throw new \UnexpectedValueException('Start or End date not set or are just "--"', 1);
|
|
}
|
|
// if invalid, quit
|
|
if (($start_timestamp = strtotime($start_date)) === false) {
|
|
throw new \UnexpectedValueException("Error parsing start date through strtotime()", 2);
|
|
}
|
|
if (($end_timestamp = strtotime($end_date)) === false) {
|
|
throw new \UnexpectedValueException("Error parsing end date through strtotime()", 3);
|
|
}
|
|
$comp = 0;
|
|
// convert anything to Y-m-d and then to timestamp
|
|
// this is to remove any time parts
|
|
$start_timestamp = strtotime(date('Y-m-d', $start_timestamp));
|
|
$end_timestamp = strtotime(date('Y-m-d', $end_timestamp));
|
|
// compare, or end with false
|
|
if ($start_timestamp < $end_timestamp) {
|
|
$comp = -1;
|
|
} elseif ($start_timestamp == $end_timestamp) {
|
|
$comp = 0;
|
|
} elseif ($start_timestamp > $end_timestamp) {
|
|
$comp = 1;
|
|
}
|
|
return $comp;
|
|
}
|
|
|
|
/**
|
|
* compares the two dates + times. if seconds missing in one set,
|
|
* adds :00, converts date + times via strtotime to timestamps
|
|
* returns int/bool in:
|
|
* -1 if the first date is smaller the last
|
|
* 0 if both are equal
|
|
* 1 if the first date is bigger than the last
|
|
* false if date/times validation/conversion failed
|
|
*
|
|
* @param string $start_datetime start date/time in YYYY-MM-DD HH:mm:ss
|
|
* @param string $end_datetime end date/time in YYYY-MM-DD HH:mm:ss
|
|
* @return int -1 (s<e)/0 (s=e)/1 (s>e) as difference
|
|
* @throws \UnexpectedValueException On empty start/end values
|
|
*/
|
|
public static function compareDateTime(string $start_datetime, string $end_datetime): int
|
|
{
|
|
// pre check for empty or wrong
|
|
if ($start_datetime == '--' || $end_datetime == '--' || empty($start_datetime) || empty($end_datetime)) {
|
|
throw new \UnexpectedValueException('Start or end timestamp not set or are just "--"', 1);
|
|
}
|
|
// quit if invalid timestamp
|
|
if (($start_timestamp = strtotime($start_datetime)) === false) {
|
|
throw new \UnexpectedValueException("Error parsing start timestamp through strtotime()", 2);
|
|
}
|
|
if (($end_timestamp = strtotime($end_datetime)) === false) {
|
|
throw new \UnexpectedValueException("Error parsing end timestamp through strtotime()", 3);
|
|
}
|
|
$comp = 0;
|
|
// compare, or return false
|
|
if ($start_timestamp < $end_timestamp) {
|
|
$comp = -1;
|
|
} elseif ($start_timestamp == $end_timestamp) {
|
|
$comp = 0;
|
|
} elseif ($start_timestamp > $end_timestamp) {
|
|
$comp = 1;
|
|
}
|
|
return $comp;
|
|
}
|
|
|
|
/**
|
|
* calculates the days between two dates
|
|
* return: overall days, week days, weekend days as array 0...2 or named
|
|
* as overall, weekday and weekend
|
|
*
|
|
* @param string $start_date valid start date (y/m/d)
|
|
* @param string $end_date valid end date (y/m/d)
|
|
* @param bool $return_named [default=false] return array type, false (default), true for named
|
|
* @param bool $include_end_date [default=true] include end date in calc
|
|
* @param bool $exclude_start_date [default=false] include end date in calc
|
|
* @return array{0:int,1:int,2:int,3:bool}|array{overall:int,weekday:int,weekend:int,reverse:bool}
|
|
* 0/overall, 1/weekday, 2/weekend, 3/reverse
|
|
*/
|
|
public static function calcDaysInterval(
|
|
string $start_date,
|
|
string $end_date,
|
|
bool $return_named = false,
|
|
bool $include_end_date = true,
|
|
bool $exclude_start_date = false
|
|
): array {
|
|
// pos 0 all, pos 1 weekday, pos 2 weekend
|
|
$days = [
|
|
0 => 0,
|
|
1 => 0,
|
|
2 => 0,
|
|
3 => false,
|
|
];
|
|
// if anything invalid, return 0,0,0
|
|
try {
|
|
$start = new \DateTime($start_date);
|
|
$end = new \DateTime($end_date);
|
|
} catch (Exception $e) {
|
|
if ($return_named === true) {
|
|
return [
|
|
'overall' => 0,
|
|
'weekday' => 0,
|
|
'weekend' => 0,
|
|
'reverse' => false
|
|
];
|
|
} else {
|
|
return $days;
|
|
}
|
|
}
|
|
// so we include the last day too, we need to add +1 second in the time
|
|
// if start is before end, switch dates and flag
|
|
$days[3] = false;
|
|
if ($start > $end) {
|
|
$new_start = $end;
|
|
$end = $start;
|
|
$start = $new_start;
|
|
$days[3] = true;
|
|
}
|
|
// get period for weekends/weekdays
|
|
$options = 0;
|
|
if ($include_end_date) {
|
|
$options |= \DatePeriod::INCLUDE_END_DATE;
|
|
}
|
|
if ($exclude_start_date) {
|
|
$options |= \DatePeriod::EXCLUDE_START_DATE;
|
|
}
|
|
$period = new \DatePeriod($start, new \DateInterval('P1D'), $end, $options);
|
|
foreach ($period as $dt) {
|
|
$curr = $dt->format('D');
|
|
if ($curr == 'Sat' || $curr == 'Sun') {
|
|
$days[2]++;
|
|
} else {
|
|
$days[1]++;
|
|
}
|
|
$days[0]++;
|
|
}
|
|
if ($return_named === true) {
|
|
return [
|
|
'overall' => $days[0],
|
|
'weekday' => $days[1],
|
|
'weekend' => $days[2],
|
|
'reverse' => $days[3],
|
|
];
|
|
} else {
|
|
return $days;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* wrapper for calcDaysInterval with numeric return only
|
|
*
|
|
* @param string $start_date valid start date (y/m/d)
|
|
* @param string $end_date valid end date (y/m/d)
|
|
* @param bool $include_end_date [default=true] include end date in calc
|
|
* @param bool $exclude_start_date [default=false] include end date in calc
|
|
* @return array{0:int,1:int,2:int,3:bool}
|
|
*/
|
|
public static function calcDaysIntervalNumIndex(
|
|
string $start_date,
|
|
string $end_date,
|
|
bool $include_end_date = true,
|
|
bool $exclude_start_date = false
|
|
): array {
|
|
$values = self::calcDaysInterval(
|
|
$start_date,
|
|
$end_date,
|
|
false,
|
|
$include_end_date,
|
|
$exclude_start_date
|
|
);
|
|
return [
|
|
$values[0],
|
|
$values[1],
|
|
$values[2],
|
|
$values[3],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* wrapper for calcDaysInterval with named return only
|
|
*
|
|
* @param string $start_date valid start date (y/m/d)
|
|
* @param string $end_date valid end date (y/m/d)
|
|
* @param bool $include_end_date [default=true] include end date in calc
|
|
* @param bool $exclude_start_date [default=false] include end date in calc
|
|
* @return array{overall:int,weekday:int,weekend:int,reverse:bool}
|
|
*/
|
|
public static function calcDaysIntervalNamedIndex(
|
|
string $start_date,
|
|
string $end_date,
|
|
bool $include_end_date = true,
|
|
bool $exclude_start_date = false
|
|
): array {
|
|
$values = self::calcDaysInterval(
|
|
$start_date,
|
|
$end_date,
|
|
true,
|
|
$include_end_date,
|
|
$exclude_start_date
|
|
);
|
|
return [
|
|
'overall' => $values['overall'],
|
|
'weekday' => $values['weekday'],
|
|
'weekend' => $values['weekend'],
|
|
'reverse' => $values['reverse'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* check if a weekend day (sat/sun) is in the given date range
|
|
* Can have time too, but is not needed
|
|
*
|
|
* @param string $start_date Y-m-d
|
|
* @param string $end_date Y-m-d
|
|
* @return bool True for has weekend, False for has not
|
|
*/
|
|
public static function dateRangeHasWeekend(
|
|
string $start_date,
|
|
string $end_date,
|
|
): bool {
|
|
$dd_start = new \DateTime($start_date);
|
|
$dd_end = new \DateTime($end_date);
|
|
// flip if start is after end
|
|
if ($dd_start > $dd_end) {
|
|
$new_start = $dd_end;
|
|
$dd_end = $dd_start;
|
|
$dd_start = $new_start;
|
|
}
|
|
// if start > end, flip
|
|
if (
|
|
// starts with a weekend
|
|
$dd_start->format('N') >= 6 ||
|
|
// start day plus diff will be 6 and so fall into a weekend
|
|
((int)$dd_start->format('w') + $dd_start->diff($dd_end)->days) >= 6
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// __END__
|