Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}

?>
Loading