<?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__
