Files
CoreLibs-Composer-All/src/Combined/DateTime.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] ?? 0,
$values[1] ?? 0,
$values[2] ?? 0,
$values[3] ?? false,
];
}
/**
* 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'] ?? 0,
'weekday' => $values['weekday'] ?? 0,
'weekend' => $values['weekend'] ?? 0,
'reverse' => $values['reverse'] ?? false,
];
}
/**
* 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__