@@ -272,6 +272,55 @@ public function testInstantWhenInvalidString(string $value): void
272272 Instant::fromString (value: $ value );
273273 }
274274
275+ #[DataProvider('validDatabaseStringsDataProvider ' )]
276+ public function testInstantFromDatabaseString (
277+ string $ value ,
278+ string $ expectedIso8601
279+ ): void {
280+ /** @Given a valid database date-time string in UTC */
281+ /** @When creating an Instant from the string */
282+ $ instant = Instant::fromString (value: $ value );
283+
284+ /** @Then the ISO 8601 representation should match the expected UTC value */
285+ self ::assertSame ($ expectedIso8601 , $ instant ->toIso8601 ());
286+ }
287+
288+ public function testInstantFromDatabaseStringPreservesMicroseconds (): void
289+ {
290+ /** @Given a database date-time string with microsecond precision */
291+ $ instant = Instant::fromString (value: '2026-02-17 08:27:21.106011 ' );
292+
293+ /** @When accessing the underlying DateTimeImmutable */
294+ $ dateTime = $ instant ->toDateTimeImmutable ();
295+
296+ /** @Then the microseconds should be preserved */
297+ self ::assertSame ('106011 ' , $ dateTime ->format ('u ' ));
298+ }
299+
300+ public function testInstantFromDatabaseStringWithoutMicrosecondsHasZeroMicroseconds (): void
301+ {
302+ /** @Given a database date-time string without microseconds */
303+ $ instant = Instant::fromString (value: '2026-02-17 08:27:21 ' );
304+
305+ /** @When accessing the underlying DateTimeImmutable */
306+ $ dateTime = $ instant ->toDateTimeImmutable ();
307+
308+ /** @Then the microseconds should be zero */
309+ self ::assertSame ('000000 ' , $ dateTime ->format ('u ' ));
310+ }
311+
312+ public function testInstantFromDatabaseStringIsInUtc (): void
313+ {
314+ /** @Given a database date-time string */
315+ $ instant = Instant::fromString (value: '2026-02-17 08:27:21.106011 ' );
316+
317+ /** @When converting to DateTimeImmutable */
318+ $ dateTime = $ instant ->toDateTimeImmutable ();
319+
320+ /** @Then the timezone should be UTC */
321+ self ::assertSame ('UTC ' , $ dateTime ->getTimezone ()->getName ());
322+ }
323+
275324 public static function validStringsDataProvider (): array
276325 {
277326 return [
@@ -351,19 +400,56 @@ public static function unixSecondsDataProvider(): array
351400 public static function invalidStringsDataProvider (): array
352401 {
353402 return [
354- 'Date only ' => ['value ' => '2026-02-17 ' ],
355- 'Time only ' => ['value ' => '10:30:00 ' ],
356- 'Plain text ' => ['value ' => 'not-a-date ' ],
357- 'Invalid day ' => ['value ' => '2026-02-30T10:30:00+00:00 ' ],
358- 'Empty string ' => ['value ' => '' ],
359- 'Invalid month ' => ['value ' => '2026-13-17T10:30:00+00:00 ' ],
360- 'Missing offset ' => ['value ' => '2026-02-17T10:30:00 ' ],
361- 'Truncated offset ' => ['value ' => '2026-02-17T10:30:00+00 ' ],
362- 'Slash-separated date ' => ['value ' => '2026/02/17T10:30:00+00:00 ' ],
363- 'Missing time separator ' => ['value ' => '2026-02-17 10:30:00+00:00 ' ],
364- 'Z suffix instead offset ' => ['value ' => '2026-02-17T10:30:00Z ' ],
365- 'With fractional seconds ' => ['value ' => '2026-02-17T10:30:00.123456+00:00 ' ],
366- 'Unix timestamp as string ' => ['value ' => '1771324200 ' ]
403+ 'Date only ' => ['value ' => '2026-02-17 ' ],
404+ 'Time only ' => ['value ' => '10:30:00 ' ],
405+ 'Plain text ' => ['value ' => 'not-a-date ' ],
406+ 'Invalid day ' => ['value ' => '2026-02-30T10:30:00+00:00 ' ],
407+ 'Empty string ' => ['value ' => '' ],
408+ 'Invalid month ' => ['value ' => '2026-13-17T10:30:00+00:00 ' ],
409+ 'Missing offset ' => ['value ' => '2026-02-17T10:30:00 ' ],
410+ 'Truncated offset ' => ['value ' => '2026-02-17T10:30:00+00 ' ],
411+ 'Slash-separated date ' => ['value ' => '2026/02/17T10:30:00+00:00 ' ],
412+ 'Missing time separator ' => ['value ' => '2026-02-17 10:30:00+00:00 ' ],
413+ 'Z suffix instead offset ' => ['value ' => '2026-02-17T10:30:00Z ' ],
414+ 'With fractional seconds ' => ['value ' => '2026-02-17T10:30:00.123456+00:00 ' ],
415+ 'Unix timestamp as string ' => ['value ' => '1771324200 ' ],
416+ 'Database format with invalid day ' => ['value ' => '2026-02-30 08:27:21.106011 ' ],
417+ 'Database format with T separator ' => ['value ' => '2026-02-17T08:27:21.106011 ' ],
418+ 'Database format with invalid month ' => ['value ' => '2026-13-17 08:27:21.106011 ' ]
419+ ];
420+ }
421+
422+ public static function validDatabaseStringsDataProvider (): array
423+ {
424+ return [
425+ 'End of day ' => [
426+ 'value ' => '2026-12-31 23:59:59.999999 ' ,
427+ 'expectedIso8601 ' => '2026-12-31T23:59:59+00:00 '
428+ ],
429+ 'Full microseconds ' => [
430+ 'value ' => '2026-02-17 08:27:21.106011 ' ,
431+ 'expectedIso8601 ' => '2026-02-17T08:27:21+00:00 '
432+ ],
433+ 'Midnight with zeros ' => [
434+ 'value ' => '2026-01-01 00:00:00.000000 ' ,
435+ 'expectedIso8601 ' => '2026-01-01T00:00:00+00:00 '
436+ ],
437+ 'Without microseconds ' => [
438+ 'value ' => '2026-02-17 08:27:21 ' ,
439+ 'expectedIso8601 ' => '2026-02-17T08:27:21+00:00 '
440+ ],
441+ 'Three digit fraction ' => [
442+ 'value ' => '2026-02-17 08:27:21.106 ' ,
443+ 'expectedIso8601 ' => '2026-02-17T08:27:21+00:00 '
444+ ],
445+ 'Single digit fraction ' => [
446+ 'value ' => '2026-02-17 08:27:21.1 ' ,
447+ 'expectedIso8601 ' => '2026-02-17T08:27:21+00:00 '
448+ ],
449+ 'Midnight without microseconds ' => [
450+ 'value ' => '2026-01-01 00:00:00 ' ,
451+ 'expectedIso8601 ' => '2026-01-01T00:00:00+00:00 '
452+ ]
367453 ];
368454 }
369455}
0 commit comments