Skip to content

Commit 47be87b

Browse files
committed
Updating the php-src phpt tests
1 parent 737d7a5 commit 47be87b

35 files changed

Lines changed: 805 additions & 269 deletions

src/Time/Resources/stubs/Time/Duration.php

Lines changed: 142 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,15 @@
1111

1212
namespace Time;
1313

14+
use function str_contains;
15+
1416
if (\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
}

tests/Time/phpt/clone.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Time\Duration: clone
33
--FILE--
44
<?php
55

6-
require __DIR__ . '/format.inc';
6+
require __DIR__ . '/helper.inc';
77

88
function fc(Time\Duration $d): string {
99
return f(clone($d));

tests/Time/phpt/format.inc

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/Time/phpt/helper.inc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
function f(Time\Duration $d, bool $pad = true, bool $plus_sign = true): string {
4+
$result = sprintf("%s%d.%09d", ($d->negative ? "-" : ($plus_sign ? "+" : " ")), $d->seconds, $d->nanoseconds);
5+
if ($pad) {
6+
$result = str_pad($result, 21, pad_type: STR_PAD_LEFT);
7+
}
8+
9+
return $result;
10+
}
11+
12+
function as_int(Time\Duration $d): int {
13+
return ($d->negative ? -1 : 1) * ($d->seconds * 1_000_000_000 + $d->nanoseconds);
14+
}
15+
16+
function negate(Time\Duration $d): Time\Duration {
17+
return $d->negate();
18+
}
19+
20+
function is_negative(Time\Duration $d): bool {
21+
return $d->negative;
22+
}
23+
24+
/**
25+
* @param Time\Duration[]
26+
*
27+
* @return Time\Duration[]
28+
*/
29+
function negate_all(array $d): array
30+
{
31+
return array_filter(array_map(negate(...), $d), is_negative(...));
32+
}

tests/Time/phpt/methods/absolute.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Time\Duration::absolute()
33
--FILE--
44
<?php
55

6-
require __DIR__ . '/../format.inc';
6+
require __DIR__ . '/../helper.inc';
77

88
echo f(Time\Duration::fromSeconds(0, 0)->absolute()), PHP_EOL;
99
echo f(Time\Duration::fromSeconds(0, 0)->negate()->absolute()), PHP_EOL;

0 commit comments

Comments
 (0)