Skip to content

Commit

Permalink
Merge pull request #8378 from Sesquipedalian/3.0/timeinterval
Browse files Browse the repository at this point in the history
Improvements to SMF\TimeInterval
  • Loading branch information
Sesquipedalian authored Dec 26, 2024
2 parents 67a1f29 + 2841c29 commit d64c717
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 28 deletions.
12 changes: 12 additions & 0 deletions Sources/Time.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
251 changes: 223 additions & 28 deletions Sources/TimeInterval.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
****************/
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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 <unit>" 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 <unit>"
// 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.
*
Expand All @@ -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
***********************/
Expand All @@ -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;
}
}

?>

0 comments on commit d64c717

Please sign in to comment.