diff --git a/Sources/Time.php b/Sources/Time.php index 0b028ec262..f094333bcf 100644 --- a/Sources/Time.php +++ b/Sources/Time.php @@ -439,6 +439,18 @@ public function __isset(string $prop): bool } } + /** + * Like \DateTime::diff(), but returns a TimeInterval object. + * + * @param \DateTimeInterface $target The date to compare to. + * @param bool $absolute Whether to force the interval to be positive. + * @return TimeInterval The difference between the two dates. + */ + public function diff(\DateTimeInterface $target, bool $absolute = false): TimeInterval + { + return TimeInterval::createFromDateInterval(parent::diff($target, $absolute)); + } + /** * Like DateTime::format(), except that it can accept either DateTime format * specifiers or strftime format specifiers (but not both at once). diff --git a/Sources/TimeInterval.php b/Sources/TimeInterval.php index 0131cc2f2d..bfd4856300 100644 --- a/Sources/TimeInterval.php +++ b/Sources/TimeInterval.php @@ -20,6 +20,88 @@ */ class TimeInterval extends \DateInterval implements \Stringable { + /******************* + * Public properties + *******************/ + + /** + * @var int + * + * Number of years. + */ + public int $y; + + /** + * @var int + * + * Number of months. + */ + public int $m; + + /** + * @var int + * + * Number of days. + */ + public int $d; + + /** + * @var int + * + * Number of hours. + */ + public int $h; + + /** + * @var int + * + * Number of minutes. + */ + public int $i; + + /** + * @var int + * + * Number of seconds. + */ + public int $s; + + /** + * @var float + * + * Number of microseconds, as a fraction of a second. + */ + public float $f; + + /** + * @var int + * + * 1 if the interval represents a negative time period and 0 otherwise. + */ + public int $invert; + + /** + * @var mixed + * + * If the object was created by Time::diff(), then this is the total number + * of full days between the start and end dates. Otherwise, false. + */ + public mixed $days; + + /** + * @var bool + * + * Whether the object was created by TimeInterval::createFromDateString(). + */ + public bool $from_string; + + /** + * @var string + * + * The string used as argument to TimeInterval::createFromDateString(). + */ + public string $date_string; + /**************** * Public methods ****************/ @@ -128,36 +210,28 @@ public function __construct(string $duration) $can_be_fractional = false; } - if (!isset($frac['prop'])) { - // If we have no fractional values, construction is easy. - parent::__construct($duration); - } else { - // Rebuild $duration without the fractional value. - $duration = 'P'; - - foreach (array_reverse($props) as $prop => $info) { - if ($prop === 'h') { - $duration .= 'T'; - } - - if (!empty($matches[$prop])) { - $duration .= $matches[$prop] . $info['unit']; - } - } - - // Construct. - parent::__construct(rtrim($duration, 'PT')); - - // Finally, set the fractional value. + // Add the fractional value where appropriate. + if (isset($frac['prop'])) { $this->{$frac['prop']} += $frac['value']; } + + // Set our properties. + $this->y = $matches['y'] ?? 0; + $this->m = $matches['m'] ?? 0; + $this->d = $matches['d'] ?? 0; + $this->h = $matches['h'] ?? 0; + $this->i = $matches['i'] ?? 0; + $this->s = $matches['s'] ?? 0; + $this->f = $matches['f'] ?? 0.0; + $this->days = false; + $this->from_string = false; } /** - * Formats the object as a string so it can be reconstructed later. + * Formats the interval as a string so it can be reconstructed later. * * @return string A ISO 8601 duration string suitable for reconstructing - * this object. + * this interval. */ public function __toString(): string { @@ -187,10 +261,11 @@ public function __toString(): string } /** - * Formats the object as a string that can be parsed by strtotime(). + * Formats this interval as a string that can be parsed by + * TimeInterval::createFromDateString(). * - * @return string A strtotime parsable string suitable for reconstructing - * this object. + * @return string A parsable string suitable for reconstructing + * this interval. */ public function toParsable(): string { @@ -230,6 +305,71 @@ public function toParsable(): string return implode(' ', $result); } + /** + * Formats the interval as a human-readable string in the current user's + * language. + * + * @param array $format_chars Properties to include in the output. + * Allowed values in this array: 'y', 'm', 'd', 'h', 'i', 's', 'f', 'a'. + * Note that when 'f' is included, it will always be combined with 's' in + * order to produce a single float value in the output. + * @return string A human-readable string. + */ + public function localize(array $format_chars = ['y', 'm', 'd']): string + { + $result = []; + + $txt_keys = [ + 'y' => 'number_of_years', + 'm' => 'number_of_months', + 'd' => 'number_of_days', + 'a' => 'number_of_days', + 'h' => 'number_of_hours', + 'i' => 'number_of_minutes', + 's' => 'number_of_seconds', + 'f' => 'number_of_seconds', + ]; + + foreach ($format_chars as $c) { + // Don't include a bunch of useless "0 " substrings. + if (empty($this->{$c}) || !isset($txt_keys[$c])) { + continue; + } + + switch ($c) { + case 'f': + if (!in_array('s', $format_chars)) { + $result[] = Lang::getTxt($txt_keys[$c], [(float) $this->s + (float) $this->f]); + } + break; + + case 's': + if (in_array('f', $format_chars)) { + $result[] = Lang::getTxt($txt_keys[$c], [(float) $this->s + (float) $this->f]); + } else { + $result[] = Lang::getTxt($txt_keys[$c], [$this->s]); + } + break; + + default: + $result[] = Lang::getTxt($txt_keys[$c], [$this->{$c}]); + break; + } + } + + // If all requested properties were empty, output a single "0 " + // for the smallest unit requested. + if (empty($result)) { + foreach ($txt_keys as $c => $k) { + if (in_array($c, $format_chars)) { + $result = [Lang::getTxt($txt_keys[$c], [0])]; + } + } + } + + return Lang::sentenceList($result); + } + /** * Converts this interval to a number of seconds. * @@ -252,6 +392,37 @@ public function toSeconds(\DateTimeInterface $when): int|float return ($later->format($fmt) - $when->format($fmt)); } + /** + * Formats the interval using an arbitrary format string. + * + * @param string $format The format string. Accepts all the same format + * specifiers that \DateInterval::format() accepts. + * @return string The formatted interval. + */ + public function format(string $format): string + { + return strtr($format, [ + '%Y' => sprintf('%02d', $this->y), + '%y' => sprintf('%01d', $this->y), + '%M' => sprintf('%02d', $this->m), + '%m' => sprintf('%01d', $this->m), + '%D' => sprintf('%02d', $this->d), + '%d' => sprintf('%01d', $this->d), + '%a' => is_int($this->days) ? sprintf('%01d', $this->days) : '(unknown)', + '%H' => sprintf('%02d', $this->h), + '%h' => sprintf('%01d', $this->h), + '%I' => sprintf('%02d', $this->i), + '%i' => sprintf('%01d', $this->i), + '%S' => sprintf('%02d', $this->s), + '%s' => sprintf('%01d', $this->s), + '%F' => substr(sprintf('%06f', $this->f), 2), + '%f' => ltrim(sprintf('%06f', $this->f), '0.'), + '%R' => $this->invert ? '-' : '+', + '%r' => $this->invert ? '-' : '', + '%%' => '%', + ]); + } + /*********************** * Public static methods ***********************/ @@ -266,12 +437,36 @@ public static function createFromDateInterval(\DateInterval $object): static { $new = new TimeInterval('P0D'); - foreach (['y', 'm', 'd', 'h', 'i', 's', 'f', 'invert'] as $prop) { - $new->{$prop} = $object->{$prop}; + foreach (get_object_vars($object) as $prop => $value) { + $new->{$prop} = $value; } return $new; } + + /** + * Creates a TimeInterval from the relative parts of a date string. + * + * @param string $datetime A date with relative parts. + * @return TimeInterval A TimeInterval object. + */ + public static function createFromDateString(string $datetime): static + { + $object = parent::createFromDateString($datetime); + + $new = self::createFromDateInterval($object); + + $new->y = (int) $object->format('%y'); + $new->m = (int) $object->format('%m'); + $new->d = abs((int) $object->format('%d')); + $new->h = (int) $object->format('%h'); + $new->i = (int) $object->format('%i'); + $new->s = (int) $object->format('%s'); + $new->f = (float) ('0.' . $object->format('%F')); + $new->invert = (int) ($object->format('%d') < 0); + + return $new; + } } ?> \ No newline at end of file