artabro/wire/core/WireDateTime.php
2024-08-27 11:35:37 +02:00

1106 lines
36 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Date/Time Tools ($datetime API variable)
*
* #pw-summary The $datetime API variable provides helpers for working with dates/times and conversion between formats.
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @method string relativeTimeStr($ts, $abbreviate = false, $useTense = true)
*
*/
class WireDateTime extends Wire {
/**
* Date formats in date() format
*
*/
static protected $dateFormats = array(
// Gregorian little-endian, starting with day (used by most of world)
'l, j F Y', // Monday, 1 April 2012
'j F Y', // 1 April 2012
'd-M-Y', // 01-Apr-2012
'dMy', // 01Apr12
'd/m/Y', // 01/04/2012
'd.m.Y', // 01.04.2012
'd/m/y', // 01/04/12
'd.m.y', // 01.04.12
'j/n/Y', // 1/4/2012
'j.n.Y', // 1.4.2012
'j/n/y', // 1/4/12
'j.n.y', // 1.4.12
// Gregorian big-endian, starting with year (used in Asian countries, Hungary, Sweden, and US armed forces)
'Y-m-d', // 2012-04-01 (ISO-8601)
'Y/m/d', // 2012/04/01 (ISO-8601)
'Y.n.j', // 2012.4.1 (common in China)
'Y/n/j', // 2012/4/1
'Y F j', // 2012 April 1
'Y-M-j, l', // 2012-Apr-1, Monday
'Y-M-j', // 2012-Apr-1
'YMj', // 2012Apr1
// Middle-endian, starting with month (US and some Canada)
'l, F j, Y', // Monday, April 1, 2012
'F j, Y', // April 1, 2012
'M j, Y', // Apr 1, 2012
'm/d/Y', // 04/01/2012
'm.d.Y', // 04.01.2012
'm/d/y', // 04/01/12
'm.d.y', // 04.01.12
'n/j/Y', // 4/1/2012
'n.j.Y', // 4.1.2012
'n/j/y', // 4/1/12
'n.j.y', // 4.1.12
// Other
//'%x', // 04/01/12 (locale based date)
//'%c', // Tue Feb 5 00:45:10 2012 (locale based date/time)
// 'U', // Unix Timestamp
);
static protected $timeFormats = array(
'g:i a', // 5:01 pm
'h:i a', // 05:01 pm
'g:i A', // 5:01 PM
'h:i A', // 05:01 PM
'H:i', // 17:01 (with leading zeros)
'H:i:s', // 17:01:01 (with leading zeros)
// Relative
'!relative',
'!relative-',
'!rel',
'!rel-',
'!r',
'!r-',
);
/**
* Date/time translation table from PHP date() to strftime() and JS/jQuery
*
*/
static protected $dateConversion = array(
// date strftime js regex
'l' => array( '%A', 'DD', '\w+'), // Name of day, long Monday
'D' => array( '%a', 'D', '\w+'), // Name of day, short Mon
'F' => array( '%B', 'MM', '\w+'), // Name of month, long April
'M' => array( '%b', 'M', '\w+'), // Name of month, short Apr
'j' => array( '%-d', 'd', '\d{1,2}'), // Day without leading zeros 1
'd' => array( '%d', 'dd', '\d{2}'), // Day with leading zeros 01
'n' => array( '%-m', 'm', '\d{1,2}'), // Month without leading zeros 4
'm' => array( '%m', 'mm', '\d{2}'), // Month with leading zeros 04
'y' => array( '%y', 'y', '\d{2}'), // Year 2 character 12
'Y' => array( '%Y', 'yy', '\d{4}'), // Year 4 character 2012
'N' => array( '%u', '', '[1-7]'), // Day of the week (1-7) 1
'w' => array( '%w', '', '[0-6]'), // Zero-based day of week (0-6) 0
'S' => array( '', '', '\w{2}'), // Day ordinal suffix st, nd, rd, th
'z' => array( '%-j', 'o', '\d{1,3}'), // Day of the year (0-365) 123
'W' => array( '%W', '', '\d{1,2}'), // Week # of the year 42
't' => array( '', '', '(28|29|30|31)'), // Number of days in month 28
'o' => array( '%V', '', '\d{1,2}'), // ISO-8601 week number 42
'a' => array( '%P', 'tt', '[aApP][mM]'), // am or pm am
'A' => array( '%p', 'TT', '[aApP][mM]'), // AM or PM AM
'g' => array( '%-I', 'h', '\d{1,2}'), // 12-hour format, no leading zeros 5
'h' => array( '%I', 'hh', '\d{2}'), // 12-hour format, leading zeros 05
'G' => array( '%-H', 'h24', '\d{1,2}'), // 24-hour format, no leading zeros 5
'H' => array( '%H', 'hh24', '\d{2}'), // 24-hour format, leading zeros 05
'i' => array( '%M', 'mm', '[0-5][0-9]'), // Minutes 09
's' => array( '%S', 'ss', '[0-5][0-9]'), // Seconds 59
'e' => array( '', '', '\w+'), // Timezone Identifier UTC, GMT, Atlantic/Azores
'I' => array( '', '', '[01]'), // Daylight savings time? 1=yes, 0=no
'O' => array( '', '', '[-+]\d{4}'), // Difference to Greenwich time/hrs +0200
'P' => array( '', '', '[-+]\d{2}:\d{2}'), // Same as above, but with colon +02:00
'T' => array( '', '', '\w+'), // Timezone abbreviation MST, EST
'Z' => array( '', '', '-?\d+'), // Timezone offset in seconds -43200 through 50400
'c' => array( '', '', '[-+:T\d]{19,25}'), // ISO-8601 date 2004-02-12T15:19:21+00:00
'r' => array( '', '', '\w+, \d+ \w+ \d{4}'), // RFC-2822 date Thu, 21 Dec 2000 16:01:07 +0200
'U' => array( '%s', '@', '\d+'), // Unix timestamp 123344556
);
protected $caches = array();
/**
* Return all predefined PHP date() formats for use as dates
*
* ~~~~~
* // output all date formats
* $formats = $datetime->getDateFormats();
* echo "<pre>" . print_r($formats, true) . "</pre>";
* ~~~~~
*
* #pw-advanced
*
* @return array Returns an array of strings containing recognized date formats
*
*/
public function getDateFormats() { return self::$dateFormats; }
public static function _getDateFormats() { return self::$dateFormats; }
/**
* Return all predefined PHP date() formats for use as times
*
* ~~~~~
* // output all time formats
* $formats = $datetime->getTimeFormats();
* echo "<pre>" . print_r($formats, true) . "</pre>";
* ~~~~~
*
* #pw-advanced
*
* @return array Returns an array of strings containing recognized time formats
*
*/
public function getTimeFormats() { return self::$timeFormats; }
public static function _getTimeFormats() { return self::$timeFormats; }
/**
* Given a date/time string and expected format, convert it to a unix timestamp
*
* @param string $str Date/time string
* @param string $format Format of the date/time string in PHP date syntax
* @return int Unix timestamp
*
*/
public function stringToTimestamp($str, $format) {
if(empty($str)) return '';
// already a timestamp
if(ctype_digit(ltrim("$str", '-'))) return (int) $str;
$format = trim("$format");
if(!strlen($format)) return strtotime($str);
// use PHP 5.3's date parser if its available
if(function_exists('date_parse_from_format')) {
// PHP 5.3+
$a = date_parse_from_format($format, $str);
if(isset($a['warnings']) && count($a['warnings'])) {
foreach($a['warnings'] as $warning) {
$this->warning($warning . " (value='$str', format='$format')");
}
}
if($a['year'] && $a['month'] && $a['day']) {
return mktime($a['hour'], $a['minute'], $a['second'], $a['month'], $a['day'], $a['year']);
}
}
$regex = '!^' . $this->convertDateFormat($format, 'regex') . '$!';
if(!preg_match($regex, $str, $m)) {
// wire('pages')->message("'$format' - '$regex' - '$str'");
// if we can't match it, then just send it to strtotime
return strtotime($str);
}
$s = '';
// month
if(isset($m['n'])) $s .= $m['n'] . '/';
else if(isset($m['m'])) $s .= $m['m'] . '/';
else if(isset($m['F'])) $s .= $m['F'] . ' ';
else if(isset($m['M'])) $s .= $m['M'] . ' ';
else return strtotime($str);
// separator character
$c = substr($s, -1);
// day
if(isset($m['j'])) $s .= $m['j'] . $c;
else if(isset($m['d'])) $s .= $m['d'] . $c;
else $s .= '1' . $c;
// year
if(isset($m['y'])) $s .= $m['y'];
else if(isset($m['Y'])) $s .= $m['Y'];
else $s .= date('Y');
$s .= ' ';
$useTime = true;
// hour
if(isset($m['g'])) $s .= $m['g'] . ':';
else if(isset($m['h'])) $s .= $m['h'] . ':';
else if(isset($m['G'])) $s .= $m['G'] . ':';
else if(isset($m['H'])) $s .= $m['H'] . ':';
else $useTime = false;
// time
if($useTime) {
// minute
if(isset($m['i'])) {
$s .= $m['i'];
// second
if(isset($m['s'])) $s .= ':' . $m['s'];
}
// am/pm
if(isset($m['a'])) $s .= ' ' . $m['a'];
else if(isset($m['A'])) $s .= ' ' . $m['A'];
}
return strtotime($s);
}
/**
* Format a date with the given PHP date() or PHP strftime() format
*
* This method will accept either [PHP date](http://php.net/manual/en/function.date.php)
* or [PHP strftime](http://php.net/manual/en/function.strftime.php) compatible formats,
* determine what kind it is, and return the timestamp formatted with it.
*
* ~~~~~~
* // Output the current date/time in ISO-8601 like format
* echo $datetime->formatDate(time(), 'Y-m-d H:i a');
* ~~~~~~
*
* #pw-internal
*
* @param int $value Unix timestamp of date/time
* @param string $format PHP date() or strftime() format string to use for formatting
* @return string Formatted date string
*
*/
public function formatDate($value, $format) {
if(!$value) return '';
if(!strlen("$format") || $format === 'U' || $format === '%s') return (int) $value; // unix timestamp
$relativeStr = '';
if(strpos($format, '!') !== false) {
if(preg_match('/([!][relativ]+-?)/', $format, $matches)) {
$relativeStr = $this->date(ltrim($matches[1], '!'), $value);
$format = str_replace($matches[1], '///', $format);
}
}
if(strpos($format, '%') !== false) {
// use strftime() if format string contains a %
if(strpos($format, '%-') !== false) {
// not all systems support the '%-' option in strftime to trim leading zeros
// so we are doing our own implementation here
$TRIM0 = true;
$format = str_replace('%-', 'TRIM0%', $format);
} else {
$TRIM0 = false;
}
$value = $this->strftime($format, $value);
if($TRIM0) $value = str_replace(array('TRIM00', 'TRIM0'), '', $value);
} else {
$value = $this->date($format, $value);
}
if(strlen($relativeStr)) $value = str_replace('///', $relativeStr, $value);
return $value;
}
/**
* Given a PHP date() format, convert it to either 'js', 'strftime' or 'regex' format
*
* #pw-advanced
*
* @param string $format PHP date() format
* @param string $type New format to convert to: either 'js', 'strftime', or 'regex'
* @return string
*
*/
public function convertDateFormat($format, $type) {
$newFormat = '';
$lastc = '';
for($n = 0; $n < strlen($format); $n++) {
$c = $format[$n];
if($c == '\\') {
// begin escaped character
$lastc = $c;
continue;
}
if($lastc == '\\') {
// literal character, not translated
$lastc = $c;
$newFormat .= $c;
continue;
}
if(!isset(self::$dateConversion[$c])) {
// unknown character
if($type == 'regex' && in_array($c, array('.', '[', ']', '(', ')', '*', '+', '?'))) {
$c = '\\' . $c; // escape regex chars
}
$newFormat .= $c;
continue;
}
list($strftime, $js, $regex) = self::$dateConversion[$c];
if($type == 'js') {
$newFormat .= $js;
} else if($type == 'strftime') {
$newFormat .= $strftime;
} else if($type == 'regex') {
$newFormat .= '\b(?<' . $c . '>' . $regex . ')\b'; // regex captured with name of date() format char
} else {
$newFormat .= $c;
}
$lastc = $c;
}
return $newFormat;
}
/**
* Format a date, using PHP date(), strftime() or other special strings (see arguments).
*
* This is designed to work the same way as PHP's `date()` but be able to accept any common format
* used in ProcessWire. This is helpful for reducing code in places where you might have logic
* determining when to use `date()`, `strftime()`, `wireRelativeTimeStr()` or some other date
* formatting function.
*
* ~~~~~~
* // Output the current date/time in relative format
* echo $datetime->date('relative');
* ~~~~~~
*
* @param string|int $format Use one of the following:
* - [PHP date](http://php.net/manual/en/function.date.php) format
* - [PHP strftime](http://php.net/manual/en/function.strftime.php) format (detected by presence of a '%' somewhere in it)
* - `relative` for a relative date/time string.
* - `relative-` for a relative date/time string with no tense.
* - `rel` for an abbreviated relative date/time string.
* - `rel-` for an abbreviated relative date/time string with no tense.
* - `r` for an extra-abbreviated relative date/time string.
* - `r-` for an extra-abbreviated relative date/time string with no tense.
* - `ts` makes it return a unix timestamp
* - blank string makes it use the system date format ($config->dateFormat)
* - If given an integer and no second argument specified, it is assumed to be the second ($ts) argument.
* @param int|string|null $ts Optionally specify the date/time stamp or strtotime() compatible string. If not specified, current time is used.
* @return string|bool Formatted date/time, or boolean false on failure
*
*/
function date($format = '', $ts = null) {
if(is_null($ts)) {
// ts not specified, or it was specified in $format
if(ctype_digit("$format")) {
// ts specified in format
$ts = (int) $format;
$format = '';
} else {
// use current timestamp
$ts = time();
}
} else if(is_string($ts) && ctype_digit("$ts")) {
// ts is a digit string, convert to integer
$ts = (int) $ts;
} else if(is_string($ts)) {
// ts is a non-integer string, we assume to be a strtotime() compatible formatted date
$ts = strtotime($ts);
}
if($format == '') $format = $this->wire()->config->dateFormat;
if($format == 'relative') {
$value = $this->relativeTimeStr($ts);
} else if($format == 'relative-') {
$value = $this->relativeTimeStr($ts, false, false);
} else if($format == 'rel') {
$value = $this->relativeTimeStr($ts, true);
} else if($format == 'rel-') {
$value = $this->relativeTimeStr($ts, true, false);
} else if($format == 'r') {
$value = $this->relativeTimeStr($ts, 1);
} else if($format == 'r-') {
$value = $this->relativeTimeStr($ts, 1, false);
} else if($format == 'ts') {
$value = $ts;
} else if(strpos($format, '%') !== false && version_compare(PHP_VERSION, '8.1.0', '<')) {
$value = $this->strftime($format, $ts);
} else {
$value = date($format, $ts);
$value = $this->localizeDateText($value, $format);
}
return $value;
}
/**
* Parse about any English textual datetime description into a Unix timestamp using PHPs strtotime()
*
* This function behaves the same as PHPs version except that it optionally accepts an `$options` array
* and lets you specify the return value for empty or zeroed dates like 0000-00-00. If given a zerod date
* then it returns null by default (rather than throwing an error as PHP8 does).
*
* @param string $str Date/time string
* @param array|int $options Options to modify behavior, or specify int for the `baseTimestamp` option.
* - `emptyReturnValue` (int|null|false): Value to return for empty or zero-only date strings (default=null)
* - `baseTimestamp` (int|null): The timestamp which is used as a base for the calculation of relative dates.
* @return false|int|null
* @see https://www.php.net/manual/en/function.strtotime.php
* @since 3.0.178
*
*/
function strtotime($str, $options = array()) {
$defaults = array(
'emptyReturnValue' => null,
'baseTimestamp' => null,
);
if(is_int($options)) $defaults['baseTimestamp'] = $options;
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
$str = trim($str);
if(empty($str)) return $options['emptyReturnValue'];
if(strpos($str, '00') === 0) {
$test = trim(preg_replace('/[^\d]/', '', $str), '0');
if(!strlen($test)) return $options['emptyReturnValue'];
}
if($options['baseTimestamp'] === null) return strtotime($str);
return strtotime($str, $options['baseTimestamp']) ;
}
/**
* strftime() replacement function that works in PHP 8.1+ (though not locale aware)
*
* @param string $format
* @param null|int|string $timestamp
* @return string|false
* @since 3.0.197
*
*/
public function strftime($format, $timestamp = null) {
$format = (string) $format;
if(is_string($timestamp)) {
if(empty($timestamp)) {
$timestamp = null;
} else if(ctype_digit($timestamp)) {
$timestamp = (int) $timestamp;
} else {
$timestamp = $this->strtotime($timestamp);
}
}
if($timestamp === null) $timestamp = time();
if(version_compare(PHP_VERSION, '8.1.0', '<')) {
return strftime($format, $timestamp);
}
$format = $this->strftimeToDateFormat($format);
$value = date($format, $timestamp);
$value = $this->localizeDateText($value, $format);
return $value;
}
/**
* Convert strftime() format to date() format
*
* @param string $format
* @return string
* @since 3.0.197
*
*/
protected function strftimeToDateFormat($format) {
// replacements, in addition to those specified in self::$dateConversion
// strftime format => date format
$replacements = array(
'%e' => 'j', // Day of the month without leading zeros
'%j' => 'z', // Day of the year, 3 digits with leading zeros
'%U' => '_', // Week number of the given year, starting with the first Sunday as the first week (not implemented)
'%h' => 'M', // Abbreviated month name
'%C' => '_', // Two digit representation of the century (year divided by 100, truncated to an integer) (not implemented)
'%g' => 'y', // Two digit representation of the year going by ISO-8601:1988 standards (see %V)
'%G' => 'Y', // 4 digit year
'%k' => 'G', // Hour in 24-hour format
'%l' => 'g', // Hour in 12-hour format
'%r' => 'h:i:s A', // Example: 09:34:17 PM
'%R' => 'G:i', // Example: 00:35 for 12:35 AM
'%T' => 'G:i:s', // Example: 21:34:17 for 09:34:17 PM
'%X' => 'G:i:s', // Preferred time representation based on locale, without the date, Example: 03:59:16 or 15:59:16
'%Z' => 'T', // The time zone abbreviation. Example: EST for Eastern Time
'%c' => 'Y-m-d H:i:s', // Preferred date and time stamp based on locale
'%D' => 'm/d/y', // Example: 02/05/09 for February 5, 2009
'%F' => 'Y-m-d', // Example: 2009-02-05 for February 5, 2009
'%n' => '\\n', // newline
'%t' => '\\t', // tab
'%%' => '%', // literal percent
);
foreach(self::$dateConversion as $dateFormat => $formats) {
$strftimeFormat = $formats[0];
if(empty($strftimeFormat)) continue;
if(strpos($format, $strftimeFormat) !== false) $replacements[$strftimeFormat] = $dateFormat;
}
return strtr($format, $replacements);
}
/**
* Given a unix timestamp (or date string), returns a formatted string indicating the time relative to now
*
* For example:
*
* - 2 years ago
* - 3 months ago
* - 1 day ago
* - 30 seconds ago
* - Just now
* - 1 day from now
* - 5 months from now
* - 3 years from now
*
* This method also supports multi-language and will output in the current user's language, so long as the
* phrases in /wire/core/WireDateTime.php are translated in the language pack.
*
* @param int|string $ts Unix timestamp or date string
* @param bool|int|array $abbreviate Whether to use abbreviations for shorter strings.
* - Specify boolean TRUE for abbreviations (abbreviated where common, not always different from non-abbreviated)
* - Specify integer 1 for extra short abbreviations (all terms abbreviated into shortest possible string)
* - Specify boolean FALSE or omit for no abbreviations.
* - Specify associative array of key=value pairs of terms to use for abbreviations. The possible keys are:
* just now, ago, from now, never, second, minute, hour, day, week, month, year, decade, seconds, minutes,
* hours, days, weeks, months, years, decades
* @param bool $useTense Whether to append a tense like "ago" or "from now".
* - May be ok to disable in situations where all times are assumed in future or past.
* - In abbreviate=1 (shortest) mode, this removes the leading "+" or "-" from the string.
* @return string Formatted relative time string
*
*/
public function ___relativeTimeStr($ts, $abbreviate = false, $useTense = true) {
// Originally based upon: <http://www.php.net/manual/en/function.time.php#89415>
if(empty($ts)) {
if(is_array($abbreviate) && isset($abbreviate['never'])) return $abbreviate['never'];
return $this->_('Never');
}
$justNow = $this->_('just now');
$ago = $this->_('ago');
$prependAgo = '';
$fromNow = $this->_('from now');
$prependFromNow = '';
$space = ' ';
if($abbreviate === 1) {
// extra short abbreviations
$justNow = $this->_('now');
$ago = '';
$prependAgo = '-';
$fromNow = '';
$prependFromNow = '+';
$space = '';
} else if($abbreviate === true) {
// standard abbreviations
$justNow = $this->_('now');
$fromNow = '';
$prependFromNow = $this->_('in') . ' ';
} else if(is_array($abbreviate)) {
// user substitutions
if(isset($abbreviate['just now'])) $justNow = $abbreviate['just now'];
if(isset($abbreviate['from now'])) $fromNow = $abbreviate['from now'];
if(isset($abbreviate['ago'])) $ago = $abbreviate['ago'];
}
$lengths = array("60","60","24","7","4.35","12","10");
$now = time();
if(!ctype_digit("$ts")) $ts = strtotime($ts);
if(empty($ts)) return "";
// is it future date or past date
if($now > $ts) {
$difference = $now - $ts;
$tense = $ago;
$prepend = $prependAgo;
} else {
$difference = $ts - $now;
$tense = $fromNow;
$prepend = $prependFromNow;
}
if(!$useTense) {
$prepend = '';
$tense = '';
}
for($j = 0; $difference >= $lengths[$j] && $j < count($lengths)-1; $j++) {
$difference /= $lengths[$j];
}
$difference = round($difference);
if(!$difference) return $justNow;
$periods = $difference != 1 ? $this->getPeriods($abbreviate, true) : $this->getPeriods($abbreviate, false);
$period = $periods[$j];
// return sprintf('%s%d%s%s %s', $prepend, (int) $difference, $space, $period, $tense); // i.e. 2 days ago (d=qty, 2=period, 3=tense)
$quantity = $prepend . $difference . $space;
$format = $this->_('Q P T'); // Relative time order: Q=Quantity, P=Period, T=Tense (i.e. 2 Days Ago)
$format = str_replace(array('Q', 'P', 'T'), array('{Q}', '{P}', '{T}'), $format);
$out = str_replace(array('{Q}', '{P}', '{T}'), array(" $quantity", " $period", " $tense"), $format);
if($abbreviate === 1) {
$out = str_replace(" ", "", $out); // no space when extra-abbreviate is active
} else if(strpos($out, ' ') !== false) {
$out = preg_replace('/\s\s+/', ' ', $out);
}
return trim($out);
}
/**
* Render an elapsed time string
*
* If the `$stop` argument is omitted then it is assumed to be the current time.
* The maximum period used is weeks, as months and years are not fixed length periods.
*
* ~~~~~
* $start = '2023-09-08 08:33:52';
* $stop = '2023-09-09 10:47:23';
*
* // Regular: 1 day 2 hours 13 minutes 31 seconds
* echo $datetime->elapsedTimeStr($start, $stop);
*
* // Abbreviated: 1 day 2 hrs 13 mins 31 secs
* echo $datetime->elapsedTimeStr($start, $stop, true);
*
* // Abbreviated with exclusions: 1 day 2 hrs
* echo $datetime->elapsedTimeStr($start, $stop, true, [ 'exclude' => 'minutes seconds' ]);
*
* // Optional 3.0.227+ usage and inclusions: 26 hours 13 minutes
* echo $datetime->elapsedTimeStr($start, [ 'stop' => $stop, 'include' => 'hours minutes' ]);
* ~~~~~
*
* @param int|string $start Starting timestamp or date/time string.
* @param int|string|array $stop Ending timestamp or date/time string, omit for “now”, or:
* - In 3.0.227+ you may optionally substitute the `$options` array argument here. When doing so,
* all remaining arguments are ignored and the `stop` and `abbreviate` (if needed) may be
* specified in the given options array, along with any other options.
* @param bool|int|array $abbreviate
* - Specify boolean FALSE for verbose elapsed time string without abbreviations (default).
* - Specify boolean TRUE for abbreviations (abbreviated where common, not always different from non-abbreviated).
* - Specify integer `1` for extra short abbreviations (all terms abbreviated into shortest possible string).
* - Specify integer `0` for digital elapsed time string like “00:01:12” referring to hours:minutes:seconds.
* - Note that when using `0` no options apply except except for `exclude[seconds]` option.
* @param array $options Additional options:
* - `delimiter` (string): String to separate time periods (default=' ').
* - `getArray` (bool): Return an array of integers indexed by period name (rather than a string)? 3.0.227+
* - `exclude` (array|string): Exclude these periods, one or more of: 'seconds', 'minutes', 'hours', 'days', 'weeks' (default=[])
* - `include` (array|string): Include only these periods, one or more of: 'seconds', 'minutes', 'hours', 'days', 'weeks' (default=[]) 3.0.227+
* - Note the exclude and include options should not be used together, and the include option requires 3.0.227+.
* @return string|array Returns array only if the `getArray` option is true, otherwise returns a string.
* @since 3.0.129
*
*/
public function elapsedTimeStr($start, $stop = null, $abbreviate = false, array $options = array()) {
$defaults = array(
'delimiter' => ' ',
'exclude' => array(),
'include' => array(),
'getArray' => false,
);
if(is_array($stop)) {
// options specified in $stop argument
$options = $stop;
$stop = isset($options['stop']) ? $options['stop'] : null;
$abbreviate = isset($options['abbreviate']) ? $options['abbreviate'] : false;
}
$options = array_merge($defaults, $options);
$periodNames = array('weeks', 'days', 'hours', 'minutes', 'seconds');
$usePeriods = array();
$negative = false;
foreach(array('exclude', 'include') as $key) {
if(is_string($options[$key])) {
$value = trim($options[$key]);
$options[$key] = $value ? explode(' ', $value) : array();
} else if(!is_array($options[$key])) {
$options[$key] = array();
}
foreach($options[$key] as $k => $v) {
if(!in_array($v, $periodNames)) unset($options[$key][$k]);
}
}
$include = count($options['include']) && $abbreviate !== 0 ? $options['include'] : null;
$exclude = count($options['exclude']) ? $options['exclude'] : null;
foreach($periodNames as $name) {
if($include && !in_array($name, $include)) continue;
if($exclude && in_array($name, $exclude)) continue;
$usePeriods[$name] = $name;
}
if($stop === null) $stop = time();
if(!ctype_digit("$start")) $start = strtotime($start);
if(!ctype_digit("$stop")) $stop = strtotime($stop);
if($start > $stop) {
list($start, $stop) = array($stop, $start);
$negative = true;
}
$times = array();
$seconds = $stop - $start;
if($seconds >= 604800 && $abbreviate !== 0 && isset($usePeriods['weeks'])) {
$weeks = floor($seconds / 604800);
$seconds = $seconds - ($weeks * 604800);
$key = $weeks == 1 ? 'week' : 'weeks';
$times[$key] = $weeks;
} else {
$times['weeks'] = 0;
}
if($seconds >= 86400 && $abbreviate !== 0 && isset($usePeriods['days'])) {
$days = floor($seconds / 86400);
$seconds = $seconds - ($days * 86400);
$key = $days == 1 ? 'day' : 'days';
$times[$key] = $days;
} else {
$times['days'] = 0;
}
if($seconds >= 3600 && isset($usePeriods['hours'])) {
$hours = floor($seconds / 3600);
$seconds = $seconds - ($hours * 3600);
$key = $hours == 1 ? 'hour' : 'hours';
$times[$key] = $hours;
} else {
$times['hours'] = 0;
$hours = 0;
}
if($seconds >= 60 && isset($usePeriods['minutes'])) {
$minutes = floor($seconds / 60);
$seconds = $seconds - ($minutes * 60);
$key = $minutes == 1 ? 'minute' : 'minutes';
$times[$key] = $minutes;
} else {
$times['minutes'] = 0;
$minutes = 0;
}
if(($seconds > 0 || empty($times)) && isset($usePeriods['seconds'])) {
$key = $seconds == 1 ? 'second' : 'seconds';
$times[$key] = $seconds;
} else {
$times['seconds'] = 0;
$seconds = 0;
}
if($abbreviate === 0 && !$options['getArray']) {
if(strlen($hours) < 2) $hours = "0$hours";
if(strlen($minutes) < 2) $minutes = "0$minutes";
if(strlen($seconds) < 2) $seconds = "0$seconds";
$value = "$hours:$minutes";
if(isset($usePeriods['seconds'])) $value .= ":$seconds";
} else {
$periods = $this->getPeriods($abbreviate);
$a = array();
$getArray = array();
foreach($times as $key => $qty) {
$pluralKey = rtrim($key, 's') . 's';
if(empty($qty) && ($pluralKey !== 'seconds' || count($a))) continue;
if(!isset($usePeriods[$pluralKey])) continue;
$sep = $abbreviate === 1 ? '' : ' ';
$str = $qty . $sep . $periods[$key];
$a[] = $str;
$getArray[$pluralKey] = $qty;
$getArray[$pluralKey . 'Text'] = $str;
}
$value = implode($options['delimiter'], $a);
if($negative) $value = "-$value";
if($options['getArray']) {
$getArray['negative'] = $negative;
$getArray['text'] = $value;
$value = $getArray;
}
}
return $value;
}
/**
* Get named time periods
*
* Returns regular array(s) of periods in this order:
* seconds, minutes, hours, days, weeks, months, years decades
*
* If $plural argument is null (or omitted) it instead returns an array
* indexed by period name including both singular and plural periods.
*
* @param $abbreviate
* - Specify 1 to get shortest possible abbreviations
* - Specify true to get standard/medium abbreviations
* - Specify false to get large/full terms (no abbreviations)
* - Specify associative array to get large/full terms and substitute your own
* @param null|true|int $plural
* - Specify true to get plural,
* - Specify false to get singular,
* - Specify 1 to get array where [ 0 => [singulars], 1 => [plurals] ]
* - Omit (or null) to get all in an indexed array
* @return array
*
*/
protected function getPeriods($abbreviate, $plural = null) {
static $definitions = array();
if(empty($definitions)) $definitions = array(
'keys-singular' => array(
'second',
'minute',
'hour',
'day',
'week',
'month',
'year',
'decade'
),
'keys-plural' => array(
'seconds',
'minutes',
'hours',
'days',
'weeks',
'months',
'years',
'decades'
),
'short-singular' => array(
$this->_("s"),
$this->_("m"),
$this->_("hr"),
$this->_("d"),
$this->_("wk"),
$this->_("mon"),
$this->_("yr"),
$this->_("decade")
),
'short-plural' => array(
$this->_("s"),
$this->_("m"),
$this->_("hr"),
$this->_("d"),
$this->_("wks"),
$this->_("mths"),
$this->_("yrs"),
$this->_("decades")
),
'medium-singular' => array(
$this->_("sec"),
$this->_("min"),
$this->_("hr"),
$this->_("day"),
$this->_("week"),
$this->_("month"),
$this->_("year"),
$this->_("decade")
),
'medium-plural' => array(
$this->_("secs"),
$this->_("mins"),
$this->_("hrs"),
$this->_("days"),
$this->_("weeks"),
$this->_("months"),
$this->_("years"),
$this->_("decades")
),
'large-singular' => array(
$this->_("second"),
$this->_("minute"),
$this->_("hour"),
$this->_("day"),
$this->_("week"),
$this->_("month"),
$this->_("year"),
$this->_("decade")
),
'large-plural' => array(
$this->_("seconds"),
$this->_("minutes"),
$this->_("hours"),
$this->_("days"),
$this->_("weeks"),
$this->_("months"),
$this->_("years"),
$this->_("decades")
),
);
if($abbreviate === 1) {
// extra short abbreviations
$periodsPlural = $definitions['short-plural'];
$periodsSingular = $definitions['short-singular'];
} else if($abbreviate === true) {
// standard abbreviations
$periodsPlural = $definitions['medium-plural'];
$periodsSingular = $definitions['medium-singular'];
} else {
// no abbreviations
$periodsPlural = $definitions['large-plural'];
$periodsSingular = $definitions['large-singular'];
// merge in any user-supplied abbreviations
if(is_array($abbreviate)) {
foreach($definitions['keys-plural'] as $key => $term) {
if(isset($abbreviate[$term])) $periodsPlural[$key] = $abbreviate[$term];
}
foreach($definitions['keys-singular'] as $key => $term) {
if(isset($abbreviate[$term])) $periodsSingular[$key] = $abbreviate[$term];
}
}
}
if($plural === true) return $periodsPlural;
if($plural === false) return $periodsSingular;
// get all indexed by term
$periods = array();
foreach($definitions['keys-plural'] as $key => $term) {
$periods[$term] = $periodsPlural[$key];
}
foreach($definitions['keys-singular'] as $key => $term) {
$periods[$term] = $periodsSingular[$key];
}
return $periods;
}
/**
* Localize a date's month and day names, when present
*
* @param string $value Date that is already formated with $format
* @param string $format Format that was used to format the date
* @return string
*
*/
protected function localizeDateText($value, $format) {
if(!$this->wire()->languages) return $value;
$findReplace = [];
$f = $format;
$keys = array();
$ltrKeys = array(
'F' => 'monthNames',
'M' => 'monthAbbrs',
'l' => 'dayNames',
'D' => 'dayAbbrs',
'a' => 'meridiums',
'A' => 'meridiums',
);
foreach($ltrKeys as $ltr => $key) {
$a = self::$dateConversion[$ltr];
if(strpos($f, $ltr) !== false || strpos($f, $a[0]) !== false || strpos($f, $a[1]) !== false) {
$keys[] = $key;
}
}
foreach($keys as $key) {
$findReplace = array_merge($findReplace, $this->dateWords($key));
}
if(count($findReplace)) {
$find = array_keys($findReplace);
$replace = array_values($findReplace);
if($find != $replace) {
$value = str_replace($find, $replace, $value);
}
}
return $value;
}
/**
* Get translated month/day words indexed by English words
*
* @param string $key One of: monthNames, monthAbbrs, dayNames, dayAbbrs, meridiums
* @return array
*
*/
protected function dateWords($key) {
$k = $key . $this->wire()->user->language->id;
switch($key) {
case 'monthNames':
if(empty($this->caches[$k])) $this->caches[$k] = array(
'January' => $this->_('January'),
'February' => $this->_('February'),
'March' => $this->_('March'),
'April' => $this->_('April'),
'May' => $this->_('May'),
'June' => $this->_('June'),
'July' => $this->_('July'),
'August' => $this->_('August'),
'September' => $this->_('September'),
'October' => $this->_('October'),
'November' => $this->_('November'),
'December' => $this->_('December'),
);
return $this->caches[$k];
case 'monthAbbrs':
if(empty($this->caches[$k])) $this->caches[$k] = array(
'Jan' => $this->_x('Jan', 'month-abbr'),
'Feb' => $this->_x('Feb', 'month-abbr'),
'Mar' => $this->_x('Mar', 'month-abbr'),
'Apr' => $this->_x('Apr', 'month-abbr'),
'May' => $this->_x('May', 'month-abbr'),
'Jun' => $this->_x('Jun', 'month-abbr'),
'Jul' => $this->_x('Jul', 'month-abbr'),
'Aug' => $this->_x('Aug', 'month-abbr'),
'Sep' => $this->_x('Sep', 'month-abbr'),
'Oct' => $this->_x('Oct', 'month-abbr'),
'Nov' => $this->_x('Nov', 'month-abbr'),
'Dec' => $this->_x('Dec', 'month-abbr'),
);
return $this->caches[$k];
case 'dayNames':
if(empty($this->caches[$k])) $this->caches[$k] = array(
'Monday' => $this->_('Monday'),
'Tuesday' => $this->_('Tuesday'),
'Wednesday' => $this->_('Wednesday'),
'Thursday' => $this->_('Thursday'),
'Friday' => $this->_('Friday'),
'Saturday' => $this->_('Saturday'),
'Sunday' => $this->_('Sunday'),
);
return $this->caches[$k];
case 'dayAbbrs':
if(empty($this->caches[$k])) $this->caches[$k] = array(
'Mon' => $this->_x('Mon', 'day-abbr'),
'Tue' => $this->_x('Tue', 'day-abbr'),
'Wed' => $this->_x('Wed', 'day-abbr'),
'Thu' => $this->_x('Thu', 'day-abbr'),
'Fri' => $this->_x('Fri', 'day-abbr'),
'Sat' => $this->_x('Sat', 'day-abbr'),
'Sun' => $this->_x('Sun', 'day-abbr'),
);
return $this->caches[$k];
case 'meridiums':
if(empty($this->caches[$k])) $this->caches[$k] = array(
'am' => $this->_x('am', 'ante-meridium-lowercase'),
'pm' => $this->_x('pm', 'post-meridium-lowercase'),
'AM' => $this->_x('AM', 'ante-meridium-uppercase'),
'PM' => $this->_x('PM', 'post-meridium-uppercase'),
);
return $this->caches[$k];
}
return array();
}
}