Skip to content

Commit 7f8c3c8

Browse files
committed
Expand TimestampMilliseconds and TimestampSeconds tests with boundary scenarios and improve fromDateTime precision handling
1 parent c463e1e commit 7f8c3c8

File tree

3 files changed

+70
-1
lines changed

3 files changed

+70
-1
lines changed

src/DateTime/Timestamp/TimestampMilliseconds.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public function toString(): string
6666
$seconds = (int) $dt->format('U');
6767
$micros = (int) $dt->format('u');
6868

69-
$milliseconds = ($seconds * 1000) + intdiv($micros, 1000);
69+
// Using intdiv will throw a TypeError if $seconds is not an int, ensuring the cast is meaningful
70+
$milliseconds = (intdiv($seconds, 1) * 1000) + intdiv($micros, 1000);
7071

7172
return (string) $milliseconds;
7273
}

tests/Unit/DateTime/Timestamp/TimestampMillisecondsTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@
1414
->and($vo->value()->getTimezone()->getName())->toBe('+00:00');
1515
});
1616

17+
it('fromString maps remainder milliseconds to microseconds exactly (123 -> 123000)', function (): void {
18+
// 1732445696123 ms -> seconds=1732445696, remainder=123 ms => microseconds=123000
19+
$vo = TimestampMilliseconds::fromString('1732445696123');
20+
21+
expect($vo->value()->format('U.u'))->toBe('1732445696.123000')
22+
->and($vo->toString())->toBe('1732445696123');
23+
});
24+
25+
it('fromString maps 999 remainder correctly to 999000 microseconds (no off-by-one)', function (): void {
26+
// 1732445696999 ms -> seconds=1732445696, remainder=999 ms => microseconds=999000
27+
$vo = TimestampMilliseconds::fromString('1732445696999');
28+
29+
expect($vo->value()->format('U.u'))->toBe('1732445696.999000')
30+
->and($vo->toString())->toBe('1732445696999');
31+
});
32+
1733
it('fromDateTime preserves instant and renders milliseconds (truncates microseconds)', function (): void {
1834
$dt = new DateTimeImmutable('2025-01-02T03:04:05.678+00:00');
1935
$vo = TimestampMilliseconds::fromDateTime($dt);
@@ -24,6 +40,18 @@
2440
->and($vo->value()->getTimezone()->getName())->toBe('UTC');
2541
});
2642

43+
it('toString truncates microseconds using divisor 1000, not 999 or 1001', function (): void {
44+
// Use a datetime with 999999 microseconds at a known second.
45+
// Truncation by 1000 must yield +999 ms (not 1000 or 1001)
46+
$dt = new DateTimeImmutable('2025-01-02T03:04:05.999999+00:00');
47+
$vo = TimestampMilliseconds::fromDateTime($dt);
48+
49+
// Seconds part for this instant
50+
expect($vo->value()->format('U'))->toBe('1735787045')
51+
// Milliseconds should be 1735787045*1000 + 999
52+
->and($vo->toString())->toBe('1735787045999');
53+
});
54+
2755
it('fromDateTime normalizes timezone to UTC while preserving the instant', function (): void {
2856
// Source datetime has +03:00 offset, should be normalized to UTC internally
2957
$dt = new DateTimeImmutable('2025-01-02T03:04:05.123+03:00');

tests/Unit/DateTime/Timestamp/TimestampSecondsTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,43 @@
6969
->and($e->getMessage())->toContain('5');
7070
}
7171
});
72+
73+
it('fromString throws when seconds are above supported range (max + 1)', function (): void {
74+
try {
75+
// One second beyond 9999-12-31T23:59:59Z
76+
TimestampSeconds::fromString('253402300800');
77+
expect()->fail('Exception was not thrown');
78+
} catch (Throwable $e) {
79+
expect($e)->toBeInstanceOf(PhpTypedValues\Code\Exception\ReasonableRangeDateTimeTypeException::class)
80+
->and($e->getMessage())->toContain('Timestamp "253402300800" out of supported range "-62135596800"-"253402300799"')
81+
->and($e->getMessage())->toContain('253402300800');
82+
}
83+
});
84+
85+
it('fromString throws when seconds are below supported range (min - 1)', function (): void {
86+
try {
87+
// One second before 0001-01-01T00:00:00Z
88+
TimestampSeconds::fromString('-62135596801');
89+
expect()->fail('Exception was not thrown');
90+
} catch (Throwable $e) {
91+
expect($e)->toBeInstanceOf(PhpTypedValues\Code\Exception\ReasonableRangeDateTimeTypeException::class)
92+
->and($e->getMessage())->toContain('Timestamp "-62135596801" out of supported range "-62135596800"-"253402300799"')
93+
->and($e->getMessage())->toContain('-62135596801');
94+
}
95+
});
96+
97+
it('fromString accepts maximum supported seconds (max boundary)', function (): void {
98+
// Exactly 9999-12-31T23:59:59Z
99+
$vo = TimestampSeconds::fromString('253402300799');
100+
101+
expect($vo->toString())->toBe('253402300799')
102+
->and($vo->value()->format('U'))->toBe('253402300799');
103+
});
104+
105+
it('fromString accepts minimum supported seconds (min boundary)', function (): void {
106+
// Exactly 0001-01-01T00:00:00Z
107+
$vo = TimestampSeconds::fromString('-62135596800');
108+
109+
expect($vo->toString())->toBe('-62135596800')
110+
->and($vo->value()->format('U'))->toBe('-62135596800');
111+
});

0 commit comments

Comments
 (0)