1111
1212namespace Time ;
1313
14- use function dd ;
15-
1614if (\PHP_VERSION_ID < 80600 ) {
1715 final class Duration
1816 {
@@ -38,15 +36,11 @@ final class Duration
3836 * @var non-empty-string
3937 */
4038 private const REGEXP_ISO8601 = '@^
41- (?<sign>[+-])?
4239 PT
43- (?=(?:\d+H|\d+M|\d+(?:[\.,]\d{1,9})? S)) # look ahead to avoid PT without argument
40+ (?=(?:\d+H|\d+M|\d+S)) # look ahead to avoid PT without argument
4441 (?:(?<hour>\d+)H)?
4542 (?:(?<minute>\d+)M)?
46- (?:
47- (?<second>\d+)
48- (?:[\.,](?<nanosecond>\d{1,9}))
49- ?S)?
43+ (?:(?<second>\d+)?S)?
5044 $@x ' ;
5145
5246 /**
@@ -56,43 +50,47 @@ private function __construct(
5650 public readonly int $ seconds ,
5751 public readonly int $ nanoseconds ,
5852 public readonly bool $ negative ,
59- ) {
53+ ) {}
54+
55+ /**
56+ * Create a duration representing $seconds seconds and $nanoseconds nanoseconds. Neither parameter
57+ * may be negative. $nanoseconds must be less than 1_000_000_000 (the number of nanoseconds in a
58+ * second).
59+ *
60+ * This constructor creates a Duration from its “atomic” components.
61+ */
62+ public static function fromSeconds (int $ seconds , int $ nanoseconds = 0 ): self
63+ {
6064 if ($ seconds < 0 ) {
61- throw new \ValueError (' $seconds must be a non negative integer. ' );
65+ throw new \ValueError (__METHOD__ . ' (): Argument #1 ( $seconds) must be greater than or equal to 0 ' );
6266 }
6367
6468 if ($ nanoseconds < 0 ) {
65- throw new \ValueError (' $nanoseconds must be a non negative integer. ' );
69+ throw new \ValueError (__METHOD__ . ' (): Argument #2 ( $nanoseconds) must be greater than or equal to 0 ' );
6670 }
6771
68- if ($ seconds >= self ::MAX_SECONDS ) {
69- throw new TimeException ('$seconds must be between 0 and ' . self :: MAX_SECONDS . ' seconds (roughly 292 years) ' );
72+ if ($ seconds > self ::MAX_SECONDS ) {
73+ throw new TimeException ('The maximum representable range is 9_223_372_035 seconds (roughly 292 years) ' );
7074 }
7175
7276 if ($ nanoseconds >= self ::NANOS_PER_SECOND ) {
73- throw new TimeException ( ' $nanoseconds must be between 0 and 999999999 nanoseconds. ' );
77+ throw new \ ValueError ( __METHOD__ . ' (): Argument #2 ( $nanoseconds) must be less than 1_000_000_000 ' );
7478 }
75- }
7679
77- /**
78- * Create a duration representing $seconds seconds and $nanoseconds nanoseconds. Neither parameter
79- * may be negative. $nanoseconds must be less than 1_000_000_000 (the number of nanoseconds in a
80- * second).
81- *
82- * This constructor creates a Duration from its “atomic” components.
83- */
84- public static function fromSeconds (int $ seconds , int $ nanoseconds = 0 ): self
85- {
8680 return new self ($ seconds , $ nanoseconds , false );
8781 }
8882
8983 /**
90- * Create a duration representing $nanoseconds nano-seconds . $nanoseconds must not be negative.
84+ * Create a duration representing $nanoseconds nanoseconds . $nanoseconds must not be negative.
9185 *
9286 * @param non-negative-int $nanoseconds
9387 */
9488 public static function fromNanoseconds (int $ nanoseconds ): self
9589 {
90+ if ($ nanoseconds < 0 ) {
91+ throw new \ValueError (__METHOD__ .'(): Argument #1 ($nanoseconds) must be greater than or equal to 0 ' );
92+ }
93+
9694 $ seconds = intdiv ($ nanoseconds , self ::NANOS_PER_SECOND );
9795 $ nanoseconds = $ nanoseconds % self ::NANOS_PER_SECOND ;
9896
@@ -108,7 +106,11 @@ public static function fromNanoseconds(int $nanoseconds): self
108106 */
109107 public static function fromMicroseconds (int $ microseconds ): self
110108 {
111- return self ::fromNanoseconds (self ::convertTo ($ microseconds , 1_000 , 'microseconds ' , 'nanoseconds ' ));
109+ if ($ microseconds < 0 ) {
110+ throw new \ValueError (__METHOD__ .'(): Argument #1 ($microseconds) must be greater than or equal to 0 ' );
111+ }
112+
113+ return self ::fromNanoseconds (self ::convertTo ($ microseconds , 1_000 , 'microseconds ' , 'nanoseconds ' , __METHOD__ ));
112114 }
113115
114116 /**
@@ -123,10 +125,10 @@ public static function fromMicroseconds(int $microseconds): self
123125 *
124126 * @return non-negative-int
125127 */
126- private static function convertTo (int $ value , int $ factor , string $ fromUnit , string $ toUnit ): int
128+ private static function convertTo (int $ value , int $ factor , string $ fromUnit , string $ toUnit, string $ method ): int
127129 {
128130 if ($ value < 0 ) {
129- throw new \ValueError ("$ value must be a non negative integer. " );
131+ throw new \ValueError ("$ method Argument #1 ( $ fromUnit ) must be greater than or equal to 0 " );
130132 }
131133
132134 if ($ value > intdiv (\PHP_INT_MAX , $ factor )) {
@@ -145,7 +147,11 @@ private static function convertTo(int $value, int $factor, string $fromUnit, str
145147 */
146148 public static function fromMilliseconds (int $ milliseconds ): self
147149 {
148- return self ::fromNanoseconds (self ::convertTo ($ milliseconds , 1_000_000 , 'milliseconds ' , 'nanoseconds ' ));
150+ if ($ milliseconds < 0 ) {
151+ throw new \ValueError (__METHOD__ .'(): Argument #1 ($milliseconds) must be greater than or equal to 0 ' );
152+ }
153+
154+ return self ::fromNanoseconds (self ::convertTo ($ milliseconds , 1_000_000 , 'milliseconds ' , 'nanoseconds ' , __METHOD__ ));
149155 }
150156
151157 /**
@@ -157,7 +163,11 @@ public static function fromMilliseconds(int $milliseconds): self
157163 */
158164 public static function fromMinutes (int $ minutes ): self
159165 {
160- return self ::fromSeconds (self ::convertTo ($ minutes , 60 , 'minutes ' , 'seconds ' ));
166+ if ($ minutes < 0 ) {
167+ throw new \ValueError (__METHOD__ .'(): Argument #1 ($minutes) must be greater than or equal to 0 ' );
168+ }
169+
170+ return self ::fromSeconds (self ::convertTo ($ minutes , 60 , 'minutes ' , 'seconds ' , __METHOD__ ));
161171 }
162172
163173 /**
@@ -169,7 +179,11 @@ public static function fromMinutes(int $minutes): self
169179 */
170180 public static function fromHours (int $ hours ): self
171181 {
172- return self ::fromSeconds (self ::convertTo ($ hours , 3_600 , 'hours ' , 'seconds ' ));
182+ if ($ hours < 0 ) {
183+ throw new \ValueError (__METHOD__ .'(): Argument #1 ($hours) must be greater than or equal to 0 ' );
184+ }
185+
186+ return self ::fromSeconds (self ::convertTo ($ hours , 3_600 , 'hours ' , 'seconds ' , __METHOD__ ));
173187 }
174188
175189 /**
@@ -178,22 +192,27 @@ public static function fromHours(int $hours): self
178192 */
179193 public static function fromIso8601DurationString (string $ specification ): self
180194 {
181- if (preg_match (self ::REGEXP_ISO8601 , $ specification , $ parts ) !== 1 ) {
182- throw new TimeException ("The submitted duration ` $ specification` is invalid or contains unsupported ISO 8601 duration components. " );
195+ if (!preg_match (self ::REGEXP_ISO8601 , $ specification , $ parts )) {
196+ if (!str_starts_with ($ specification , 'PT ' )) {
197+ throw new TimeException ("Failed to create a Time\Duration: The ISO8601 duration string may only contain the period (P) aspect " );
198+ }
199+
200+ if (strpbrk ($ specification , 'HMS ' ) === false ) {
201+ throw new TimeException ("Failed to create a Time\Duration: The ISO8601 duration string may only contain the time (T) aspect " );
202+ }
203+
204+ throw new TimeException ("Failed to create a Time\Duration: The ISO8601 duration string could not be parsed " );
183205 }
184206
185- $ seconds = self ::convertTo ((int ) ($ parts ['hour ' ] ?? 0 ), 3_600 , 'hours ' , 'seconds ' )
186- + self ::convertTo ((int )($ parts ['minute ' ] ?? 0 ), 60 , 'minutes ' , 'seconds ' )
207+ $ seconds = self ::convertTo ((int ) ($ parts ['hour ' ] ?? 0 ), 3_600 , 'hours ' , 'seconds ' , __METHOD__ )
208+ + self ::convertTo ((int )($ parts ['minute ' ] ?? 0 ), 60 , 'minutes ' , 'seconds ' , __METHOD__ )
187209 + (int ) ($ parts ['second ' ] ?? 0 );
188210
189211 if ($ seconds > self ::MAX_SECONDS ) {
190- throw new TimeException ('Can not convert ` ' . $ specification . ' ` specification; the resulting value exceeds the supported duration range. ' );
212+ throw new TimeException ('The maximum representable range is 9_223_372_035 seconds (roughly 292 years) ' );
191213 }
192214
193- $ nanoseconds = (int ) (str_pad ($ parts ['nanosecond ' ] ?? '0 ' , 9 , '0 ' , \STR_PAD_RIGHT ));
194- $ negative = '- ' === ($ parts ['sign ' ] ?? '' ) && ($ seconds !== 0 || $ nanoseconds !== 0 );
195-
196- return new self ($ seconds , $ nanoseconds , $ negative );
215+ return new self ($ seconds , 0 , false );
197216 }
198217
199218 /**
@@ -226,28 +245,43 @@ public function absolute(): self
226245 */
227246 public function add (self $ duration ): self
228247 {
229- /* (+x) + (-y) == (+x) - (+y) */
230- if (!$ this ->negative && $ duration ->negative ) {
231- return $ this ->sub ($ duration ->negate ());
248+ $ signedParts = static function (Duration $ duration ): array {
249+ $ seconds = $ duration ->seconds ;
250+ $ nanoseconds = $ duration ->nanoseconds ;
251+ if ($ duration ->negative ) {
252+ $ seconds *= -1 ;
253+ $ nanoseconds *= -1 ;
254+ }
255+
256+ return [$ seconds , $ nanoseconds ];
257+ };
258+
259+ [$ thisSeconds , $ thisNanoseconds ] = $ signedParts ($ this );
260+ [$ thatSeconds , $ thatNanoseconds ] = $ signedParts ($ duration );
261+
262+ $ seconds = $ thisSeconds + $ thatSeconds ;
263+ $ nanoseconds = $ thisNanoseconds + $ thatNanoseconds ;
264+
265+ if ($ nanoseconds >= self ::NANOS_PER_SECOND || $ nanoseconds <= -self ::NANOS_PER_SECOND ) {
266+ $ seconds += intdiv ($ nanoseconds , self ::NANOS_PER_SECOND );
267+ $ nanoseconds %= self ::NANOS_PER_SECOND ;
232268 }
233269
234- /* (-x) + (+y) == (+y) - (+x) */
235- if ( $ this -> negative && ! $ duration -> negative ) {
236- return $ duration -> sub ( $ this -> negate ()) ;
270+ if ( $ seconds > 0 && $ nanoseconds < 0 ) {
271+ -- $ seconds ;
272+ $ nanoseconds += self :: NANOS_PER_SECOND ;
237273 }
238274
239- /* (-x) + (-y) = -((+x) + (+y)) */
240- if ( $ this -> negative ) {
241- return $ this -> negate ()-> add ( $ duration -> negate ())-> negate () ;
275+ if ( $ seconds < 0 && $ nanoseconds > 0 ) {
276+ ++ $ seconds ;
277+ $ nanoseconds -= self :: NANOS_PER_SECOND ;
242278 }
243279
244- $ seconds = $ this ->seconds + $ duration ->seconds ;
245- $ nanoseconds = $ this ->nanoseconds + $ duration ->nanoseconds ;
246-
247- $ seconds += intdiv ($ nanoseconds , self ::NANOS_PER_SECOND );
248- $ nanoseconds = $ nanoseconds % self ::NANOS_PER_SECOND ;
249-
250- return self ::fromSeconds ($ seconds , $ nanoseconds );
280+ return new self (
281+ abs ($ seconds ),
282+ abs ($ nanoseconds ),
283+ ($ seconds < 0 || (0 === $ seconds && $ nanoseconds < 0 ))
284+ );
251285 }
252286
253287 /**
@@ -257,34 +291,7 @@ public function add(self $duration): self
257291 */
258292 public function sub (self $ duration ): self
259293 {
260- /* (+x) - (-y) == (+x) + (+y) */
261- if (!$ this ->negative && $ duration ->negative ) {
262- return $ this ->add ($ duration ->negate ());
263- }
264-
265- /* (-x) - (+y) == -((+x) + (+y)) */
266- if ($ this ->negative && !$ duration ->negative ) {
267- return $ this ->negate ()->add ($ duration )->negate ();
268- }
269-
270- /* (-x) - (-y) == (-x) + (+y) */
271- if ($ this ->negative ) {
272- return $ this ->add ($ duration ->negate ());
273- }
274-
275- if (self ::compare ($ this , $ duration ) < 0 ) {
276- /* (+x) - (+y) = -((+y) - (+x)) */
277- return $ duration ->sub ($ this )->negate ();
278- }
279-
280- $ seconds = $ this ->seconds - $ duration ->seconds ;
281- $ nanoseconds = $ this ->nanoseconds - $ duration ->nanoseconds ;
282- if ($ nanoseconds < 0 ) {
283- $ nanoseconds += self ::NANOS_PER_SECOND ;
284- $ seconds --;
285- }
286-
287- return self ::fromSeconds ($ seconds , $ nanoseconds );
294+ return $ this ->add ($ duration ->negate ());
288295 }
289296
290297 /**
@@ -298,6 +305,14 @@ public function multiplyBy(int $factor): self
298305 throw new \ValueError ('$factor must not be negative ' );
299306 }
300307
308+ if ($ factor === 0 ) {
309+ return new self (0 , 0 , false );
310+ }
311+
312+ if ($ factor === 1 ) {
313+ return $ this ;
314+ }
315+
301316 $ seconds = $ this ->seconds * $ factor ;
302317 $ nanoseconds = $ this ->nanoseconds * $ factor ;
303318
@@ -320,6 +335,10 @@ public function divideBy(int $divisor): self
320335 throw new \ValueError ('$divisor must be positive ' );
321336 }
322337
338+ if ($ divisor === 1 ) {
339+ return $ this ;
340+ }
341+
323342 $ seconds = intdiv ($ this ->seconds , $ divisor );
324343 $ nanoseconds = $ this ->nanoseconds + (($ this ->seconds % $ divisor ) * self ::NANOS_PER_SECOND );
325344 $ nanoseconds = intdiv ($ nanoseconds , $ divisor );
0 commit comments