1111
1212namespace Time ;
1313
14+ use function str_contains ;
15+
1416if (\PHP_VERSION_ID < 80600 ) {
1517 final class Duration
1618 {
1719 /** @var int */
1820 private const NANOS_PER_SECOND = 1_000_000_000 ;
1921
20- /**
21- * Maximum seconds accepted by Duration.
22- *
23- * One second is reserved to avoid overflowing when arithmetic operations
24- * temporarily require carrying nanoseconds into the seconds component.
25- *
26- * @var int
27- */
28- private const MAX_SECONDS = 9_223_372_035 ;
22+ private const AVERAGE_GREGORIAN_YEAR = 31_556_952 ;
2923
3024 /**
3125 * Regular expression to parse iso-8601 duration string.
@@ -37,7 +31,6 @@ final class Duration
3731 */
3832 private const REGEXP_ISO8601 = '@^
3933 PT
40- (?=(?:\d+H|\d+M|\d+S)) # look ahead to avoid PT without argument
4134 (?:(?<hour>\d+)H)?
4235 (?:(?<minute>\d+)M)?
4336 (?:(?<second>\d+)?S)?
@@ -52,6 +45,22 @@ private function __construct(
5245 public readonly bool $ negative ,
5346 ) {}
5447
48+ /**
49+ * Maximum seconds accepted by Duration.
50+ * The value depends on the PHP-build
51+ *
52+ * One second is reserved to avoid overflowing when arithmetic operations
53+ * temporarily require carrying nanoseconds into the seconds component.
54+ *
55+ * @return int
56+ */
57+ private static function maxSeconds (): int
58+ {
59+ return \PHP_INT_SIZE === 4
60+ ? \PHP_INT_MAX
61+ : intdiv (\PHP_INT_MAX , self ::NANOS_PER_SECOND ) - 1 ;
62+ }
63+
5564 /**
5665 * Create a duration representing $seconds seconds and $nanoseconds nanoseconds. Neither parameter
5766 * may be negative. $nanoseconds must be less than 1_000_000_000 (the number of nanoseconds in a
@@ -69,8 +78,13 @@ public static function fromSeconds(int $seconds, int $nanoseconds = 0): self
6978 throw new \ValueError (__METHOD__ .'(): Argument #2 ($nanoseconds) must be greater than or equal to 0 ' );
7079 }
7180
72- if ($ seconds > self ::MAX_SECONDS ) {
73- throw new TimeException ('The maximum representable range is 9_223_372_035 seconds (roughly 292 years) ' );
81+ $ maxSeconds = self ::maxSeconds ();
82+ if ($ seconds > $ maxSeconds ) {
83+ throw new TimeException (sprintf (
84+ 'The maximum representable range is %s seconds (roughly %d years) ' ,
85+ number_format ($ maxSeconds , 0 , '. ' , '_ ' ),
86+ intdiv ($ maxSeconds , self ::AVERAGE_GREGORIAN_YEAR )
87+ ));
7488 }
7589
7690 if ($ nanoseconds >= self ::NANOS_PER_SECOND ) {
@@ -192,24 +206,53 @@ public static function fromHours(int $hours): self
192206 */
193207 public static function fromIso8601DurationString (string $ specification ): self
194208 {
209+ if ($ specification === '' || $ specification === 'P ' || $ specification === 'PT ' ) {
210+ throw new TimeException ('The ISO 8601 duration string could not be parsed ' );
211+ }
212+
195213 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 " );
214+ $ realSpec = trim ($ specification );
215+ if ($ realSpec === '' || $ realSpec === 'P ' || $ realSpec === 'PT ' ) {
216+ throw new TimeException ('The ISO 8601 duration string could not be parsed ' );
198217 }
199218
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" );
219+ if (! str_contains ( $ realSpec , 'P ' ) ) {
220+ throw new TimeException (' The ISO 8601 duration string is missing the period (P ) aspect ' );
202221 }
203222
204- throw new TimeException ("Failed to create a Time\Duration: The ISO8601 duration string could not be parsed " );
223+ if (!str_starts_with ($ specification , 'P ' )) {
224+ throw new TimeException ("The ISO 8601 duration string may only contain the period (P) aspect " );
225+ }
226+
227+ $ containsMonthComponent = static function (string $ data ): bool {
228+ $ monthPosition = strpos ($ data , 'M ' );
229+ if (false === $ monthPosition ) {
230+ return false ;
231+ }
232+
233+ $ timePosition = strpos ($ data , 'T ' );
234+
235+ return false === $ timePosition || $ monthPosition < $ timePosition ;
236+ };
237+
238+ if (strpbrk ($ specification , 'YDW ' ) !== false || $ containsMonthComponent ($ specification )) {
239+ throw new TimeException ("The ISO 8601 duration string may only contain the time (T) aspect " );
240+ }
241+
242+ throw new TimeException ("The ISO 8601 duration string could not be parsed " );
205243 }
206244
207245 $ seconds = self ::convertTo ((int ) ($ parts ['hour ' ] ?? 0 ), 3_600 , 'hours ' , 'seconds ' , __METHOD__ )
208246 + self ::convertTo ((int )($ parts ['minute ' ] ?? 0 ), 60 , 'minutes ' , 'seconds ' , __METHOD__ )
209247 + (int ) ($ parts ['second ' ] ?? 0 );
210248
211- if ($ seconds > self ::MAX_SECONDS ) {
212- throw new TimeException ('The maximum representable range is 9_223_372_035 seconds (roughly 292 years) ' );
249+ $ maxSeconds = self ::maxSeconds ();
250+ if ($ seconds > $ maxSeconds ) {
251+ throw new TimeException (sprintf (
252+ 'The maximum representable range is %s seconds (roughly %d years) ' ,
253+ number_format ($ maxSeconds , 0 , '. ' , '_ ' ),
254+ intdiv ($ maxSeconds , self ::AVERAGE_GREGORIAN_YEAR )
255+ ));
213256 }
214257
215258 return new self ($ seconds , 0 , false );
@@ -238,60 +281,51 @@ public function absolute(): self
238281 return $ this ;
239282 }
240283
284+ /**
285+ * Subtract the given duration from the duration.
286+ *
287+ * @return self $this - $duration
288+ */
289+ public function sub (self $ duration ): self
290+ {
291+ return $ this ->add ($ duration ->negate ());
292+ }
293+
241294 /**
242295 * Add the given duration to the duration.
243296 *
297+ * @throws TimeException
298+ *
244299 * @return self $this + $duration
245300 */
246301 public function add (self $ duration ): self
247302 {
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 );
303+ [$ thisSeconds , $ thisNanoseconds ] = self ::signedParts ($ this );
304+ [$ thatSeconds , $ thatNanoseconds ] = self ::signedParts ($ duration );
261305
262306 $ seconds = $ thisSeconds + $ thatSeconds ;
263307 $ nanoseconds = $ thisNanoseconds + $ thatNanoseconds ;
308+ $ seconds += intdiv ($ nanoseconds , self ::NANOS_PER_SECOND );
309+ $ nanoseconds %= self ::NANOS_PER_SECOND ;
264310
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 ;
268- }
269-
270- if ($ seconds > 0 && $ nanoseconds < 0 ) {
271- --$ seconds ;
272- $ nanoseconds += self ::NANOS_PER_SECOND ;
273- }
274-
275- if ($ seconds < 0 && $ nanoseconds > 0 ) {
276- ++$ seconds ;
277- $ nanoseconds -= self ::NANOS_PER_SECOND ;
278- }
279-
280- return new self (
281- abs ($ seconds ),
282- abs ($ nanoseconds ),
283- ($ seconds < 0 || (0 === $ seconds && $ nanoseconds < 0 ))
284- );
311+ return self ::fromDurationParts ($ seconds , $ nanoseconds );
285312 }
286313
287314 /**
288- * Subtract the given duration from the duration.
315+ * @param Duration $ duration
289316 *
290- * @return self $this - $duration
317+ * @return array{0: int, 1: int}
291318 */
292- public function sub ( self $ duration ): self
319+ private static function signedParts ( Duration $ duration ): array
293320 {
294- return $ this ->add ($ duration ->negate ());
321+ $ seconds = $ duration ->seconds ;
322+ $ nanoseconds = $ duration ->nanoseconds ;
323+ if ($ duration ->negative ) {
324+ $ seconds *= -1 ;
325+ $ nanoseconds *= -1 ;
326+ }
327+
328+ return [$ seconds , $ nanoseconds ];
295329 }
296330
297331 /**
@@ -313,13 +347,18 @@ public function multiplyBy(int $factor): self
313347 return $ this ;
314348 }
315349
316- $ seconds = $ this ->seconds * $ factor ;
317- $ nanoseconds = $ this ->nanoseconds * $ factor ;
350+ $ maxSeconds = self ::maxSeconds ();
351+ if ($ this ->seconds > intdiv ($ maxSeconds , $ factor )) {
352+ throw new TimeException (sprintf (
353+ 'The maximum representable range is %s seconds (roughly %d years) ' ,
354+ number_format ($ maxSeconds , 0 , '. ' , '_ ' ),
355+ intdiv ($ maxSeconds , self ::AVERAGE_GREGORIAN_YEAR )
356+ ));
357+ }
318358
319- $ seconds += intdiv ($ nanoseconds , self ::NANOS_PER_SECOND );
320- $ nanoseconds = $ nanoseconds % self ::NANOS_PER_SECOND ;
359+ [$ seconds , $ nanoseconds ] = self ::signedParts ($ this );
321360
322- return new self ($ seconds , $ nanoseconds, $ this -> negative );
361+ return self :: fromDurationParts ($ seconds * $ factor , $ nanoseconds * $ factor );
323362 }
324363
325364 /**
@@ -332,18 +371,21 @@ public function multiplyBy(int $factor): self
332371 public function divideBy (int $ divisor ): self
333372 {
334373 if ($ divisor <= 0 ) {
335- throw new \ValueError ( ' $divisor must be positive ' );
374+ throw new \DivisionByZeroError ( ' Division by zero ' );
336375 }
337376
338- if ($ divisor === 1 ) {
377+ if ($ divisor === 1 || ( $ this -> seconds === 0 && $ this -> nanoseconds === 0 ) ) {
339378 return $ this ;
340379 }
341380
342- $ seconds = intdiv ($ this -> seconds , $ divisor );
343- $ nanoseconds = $ this -> nanoseconds + (( $ this -> seconds % $ divisor ) * self :: NANOS_PER_SECOND );
344- $ nanoseconds = intdiv ( $ nanoseconds , $ divisor) ;
381+ [ $ seconds, $ nanoseconds ] = self :: signedParts ($ this );
382+
383+ $ remainder = $ seconds % $ divisor ;
345384
346- return new self ($ seconds , $ nanoseconds , negative: $ this ->negative );
385+ return self ::fromDurationParts (
386+ intdiv ($ seconds , $ divisor ),
387+ intdiv ($ nanoseconds + ($ remainder * self ::NANOS_PER_SECOND ), $ divisor )
388+ );
347389 }
348390
349391 /**
@@ -362,5 +404,38 @@ public static function compare(self $a, self $b): int
362404
363405 return $ a ->negative ? -$ comparison : $ comparison ;
364406 }
407+
408+ /**
409+ * @throws TimeException
410+ */
411+ private static function fromDurationParts (int $ seconds , int $ nanoseconds ): Duration
412+ {
413+ $ seconds += intdiv ($ nanoseconds , self ::NANOS_PER_SECOND );
414+ $ nanoseconds %= self ::NANOS_PER_SECOND ;
415+
416+ if ($ seconds > 0 && $ nanoseconds < 0 ) {
417+ --$ seconds ;
418+ $ nanoseconds += self ::NANOS_PER_SECOND ;
419+ } elseif ($ seconds < 0 && $ nanoseconds > 0 ) {
420+ ++$ seconds ;
421+ $ nanoseconds -= self ::NANOS_PER_SECOND ;
422+ }
423+
424+ $ maxSeconds = self ::maxSeconds ();
425+
426+ if ($ seconds > $ maxSeconds || $ seconds < -$ maxSeconds ) {
427+ throw new TimeException (sprintf (
428+ 'The maximum representable range is %s seconds (roughly %d years) ' ,
429+ number_format ($ maxSeconds , 0 , '. ' , '_ ' ),
430+ intdiv ($ maxSeconds , self ::AVERAGE_GREGORIAN_YEAR )
431+ ));
432+ }
433+
434+ return new self (
435+ abs ($ seconds ),
436+ abs ($ nanoseconds ),
437+ $ seconds < 0 || ($ seconds === 0 && $ nanoseconds < 0 )
438+ );
439+ }
365440 }
366441}
0 commit comments