Skip to content

Commit 477bf3d

Browse files
Adds support to SMF\TimeInterval for all \DateInterval methods
This is tricker than one might expect, because of some very quirky behaviour in \DateInterval and how it handles its properties. In particular, calling parent::__construct() from a child class will clobber certain properties values and make them read-only. But not calling the parent constructor means that we must implement all of the parent's methods and properties in the child class. Signed-off-by: Jon Stovell <jonstovell@gmail.com>
1 parent 67a1f29 commit 477bf3d

File tree

1 file changed

+167
-28
lines changed

1 file changed

+167
-28
lines changed

Sources/TimeInterval.php

Lines changed: 167 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,88 @@
2020
*/
2121
class TimeInterval extends \DateInterval implements \Stringable
2222
{
23+
/*******************
24+
* Public properties
25+
*******************/
26+
27+
/**
28+
* @var int
29+
*
30+
* Number of years.
31+
*/
32+
public int $y;
33+
34+
/**
35+
* @var int
36+
*
37+
* Number of months.
38+
*/
39+
public int $m;
40+
41+
/**
42+
* @var int
43+
*
44+
* Number of days.
45+
*/
46+
public int $d;
47+
48+
/**
49+
* @var int
50+
*
51+
* Number of hours.
52+
*/
53+
public int $h;
54+
55+
/**
56+
* @var int
57+
*
58+
* Number of minutes.
59+
*/
60+
public int $i;
61+
62+
/**
63+
* @var int
64+
*
65+
* Number of seconds.
66+
*/
67+
public int $s;
68+
69+
/**
70+
* @var float
71+
*
72+
* Number of microseconds, as a fraction of a second.
73+
*/
74+
public float $f;
75+
76+
/**
77+
* @var int
78+
*
79+
* 1 if the interval represents a negative time period and 0 otherwise.
80+
*/
81+
public int $invert;
82+
83+
/**
84+
* @var mixed
85+
*
86+
* If the object was created by Time::diff(), then this is the total number
87+
* of full days between the start and end dates. Otherwise, false.
88+
*/
89+
public mixed $days;
90+
91+
/**
92+
* @var bool
93+
*
94+
* Whether the object was created by TimeInterval::createFromDateString().
95+
*/
96+
public bool $from_string;
97+
98+
/**
99+
* @var string
100+
*
101+
* The string used as argument to TimeInterval::createFromDateString().
102+
*/
103+
public string $date_string;
104+
23105
/****************
24106
* Public methods
25107
****************/
@@ -128,36 +210,28 @@ public function __construct(string $duration)
128210
$can_be_fractional = false;
129211
}
130212

131-
if (!isset($frac['prop'])) {
132-
// If we have no fractional values, construction is easy.
133-
parent::__construct($duration);
134-
} else {
135-
// Rebuild $duration without the fractional value.
136-
$duration = 'P';
137-
138-
foreach (array_reverse($props) as $prop => $info) {
139-
if ($prop === 'h') {
140-
$duration .= 'T';
141-
}
142-
143-
if (!empty($matches[$prop])) {
144-
$duration .= $matches[$prop] . $info['unit'];
145-
}
146-
}
147-
148-
// Construct.
149-
parent::__construct(rtrim($duration, 'PT'));
150-
151-
// Finally, set the fractional value.
213+
// Add the fractional value where appropriate.
214+
if (isset($frac['prop'])) {
152215
$this->{$frac['prop']} += $frac['value'];
153216
}
217+
218+
// Set our properties.
219+
$this->y = $matches['y'] ?? 0;
220+
$this->m = $matches['m'] ?? 0;
221+
$this->d = $matches['d'] ?? 0;
222+
$this->h = $matches['h'] ?? 0;
223+
$this->i = $matches['i'] ?? 0;
224+
$this->s = $matches['s'] ?? 0;
225+
$this->f = $matches['f'] ?? 0.0;
226+
$this->days = false;
227+
$this->from_string = false;
154228
}
155229

156230
/**
157-
* Formats the object as a string so it can be reconstructed later.
231+
* Formats the interval as a string so it can be reconstructed later.
158232
*
159233
* @return string A ISO 8601 duration string suitable for reconstructing
160-
* this object.
234+
* this interval.
161235
*/
162236
public function __toString(): string
163237
{
@@ -187,10 +261,11 @@ public function __toString(): string
187261
}
188262

189263
/**
190-
* Formats the object as a string that can be parsed by strtotime().
264+
* Formats this interval as a string that can be parsed by
265+
* TimeInterval::createFromDateString().
191266
*
192-
* @return string A strtotime parsable string suitable for reconstructing
193-
* this object.
267+
* @return string A parsable string suitable for reconstructing
268+
* this interval.
194269
*/
195270
public function toParsable(): string
196271
{
@@ -252,6 +327,37 @@ public function toSeconds(\DateTimeInterface $when): int|float
252327
return ($later->format($fmt) - $when->format($fmt));
253328
}
254329

330+
/**
331+
* Formats the interval using an arbitrary format string.
332+
*
333+
* @param string $format The format string. Accepts all the same format
334+
* specifiers that \DateInterval::format() accepts.
335+
* @return string The formatted interval.
336+
*/
337+
public function format(string $format): string
338+
{
339+
return strtr($format, [
340+
'%Y' => sprintf('%02d', $this->y),
341+
'%y' => sprintf('%01d', $this->y),
342+
'%M' => sprintf('%02d', $this->m),
343+
'%m' => sprintf('%01d', $this->m),
344+
'%D' => sprintf('%02d', $this->d),
345+
'%d' => sprintf('%01d', $this->d),
346+
'%a' => is_int($this->days) ? sprintf('%01d', $this->days) : '(unknown)',
347+
'%H' => sprintf('%02d', $this->h),
348+
'%h' => sprintf('%01d', $this->h),
349+
'%I' => sprintf('%02d', $this->i),
350+
'%i' => sprintf('%01d', $this->i),
351+
'%S' => sprintf('%02d', $this->s),
352+
'%s' => sprintf('%01d', $this->s),
353+
'%F' => substr(sprintf('%06f', $this->f), 2),
354+
'%f' => ltrim(sprintf('%06f', $this->f), '0.'),
355+
'%R' => $this->invert ? '-' : '+',
356+
'%r' => $this->invert ? '-' : '',
357+
'%%' => '%',
358+
]);
359+
}
360+
255361
/***********************
256362
* Public static methods
257363
***********************/
@@ -266,12 +372,45 @@ public static function createFromDateInterval(\DateInterval $object): static
266372
{
267373
$new = new TimeInterval('P0D');
268374

269-
foreach (['y', 'm', 'd', 'h', 'i', 's', 'f', 'invert'] as $prop) {
270-
$new->{$prop} = $object->{$prop};
375+
foreach (get_object_vars($object) as $prop => $value) {
376+
$new->{$prop} = $value;
271377
}
272378

273379
return $new;
274380
}
381+
382+
/**
383+
* Creates a TimeInterval from the relative parts of a date string.
384+
*
385+
* Because of the very quirky way that \DateInterval handles its properties,
386+
* it is not possible for SMF\TimeInterval to implement the $from_string and
387+
* $date_string properties the way its parent class does. As a result, the
388+
* inherited $from_string property will always be false and the inherited
389+
* $date_string property will never be set. Instead, TimeInterval instances
390+
* created by this method will have the y, m, d, h, i, s, f, and invert
391+
* properties, just like instances created by the constructor. One could
392+
* argue that this is an improvement.
393+
*
394+
* @param string $datetime A date with relative parts.
395+
* @return TimeInterval A TimeInterval object.
396+
*/
397+
public static function createFromDateString(string $datetime): static
398+
{
399+
$object = parent::createFromDateString($datetime);
400+
401+
$new = self::createFromDateInterval($object);
402+
403+
$new->y = (int) $object->format('%y');
404+
$new->m = (int) $object->format('%m');
405+
$new->d = abs((int) $object->format('%d'));
406+
$new->h = (int) $object->format('%h');
407+
$new->i = (int) $object->format('%i');
408+
$new->s = (int) $object->format('%s');
409+
$new->f = (float) ('0.' . $object->format('%F'));
410+
$new->invert = (int) ($object->format('%d') < 0);
411+
412+
return $new;
413+
}
275414
}
276415

277416
?>

0 commit comments

Comments
 (0)