Skip to content

Commit 559d15c

Browse files
committed
Fix Undocumented Behavious
- Move classes to correct namespaces - Fix undocumented behaviour with timezones - Fix existing unit tests to cover this behaviour, and update documentation to reflect correct behaviour - Add timezone support to createFromTimestamp() and add new unit tests for this
1 parent 87416fe commit 559d15c

5 files changed

+92
-30
lines changed

README.md

+26-8
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,37 @@ two interfaces ([`DateInterface`](src/DateInterface.php) and
2222
- **DTI**'s `createFromObject(\DateTimeInterface $datetime): static` is the
2323
same as `\DateTimeImmutable::createFromMutable()` but works with any object
2424
that implements `\DateTimeInterface`.
25-
- **DTI**'s `createFromTimestamp(int $timestamp): static` creates a **DTI**
26-
object from an integer timestamp.
25+
- **DTI**'s `createFromTimestamp(int $timestamp, ?\DateTimeZone $tz): static`
26+
creates a **DTI** object from an integer timestamp (and optional timezone,
27+
meaning identical timestamps using different timezones will result in
28+
differing date strings).
2729

2830
On top of these, **DTI** objects can be stringified into a format appropriate to
2931
the calling class, and when JSON-enoded returns that string instead of
3032
`{"date","timezone_type","timezone"}` objects. The string formats are as
3133
follows:
3234

33-
| Class | Format | Example |
34-
|---------------|------------------|----------------------------|
35-
| `Date` | `Y-m-d` | `2018-12-19` |
36-
| `DateTime` | `Y-m-d\TH:i:sP` | `2018-12-19T14:03:24+0100` |
37-
| `UtcDateTime` | `Y-m-d\TH:i:s\Z` | `2018-12-19T13:03:24Z` |
35+
| Class | Format | Example |
36+
|---------------|------------------|-----------------------------|
37+
| `Date` | `Y-m-d` | `2018-12-19` |
38+
| `DateTime` | `Y-m-d\TH:i:sP` | `2018-12-19T14:03:24+01:00` |
39+
| `UtcDateTime` | `Y-m-d\TH:i:s\Z` | `2018-12-19T13:03:24Z` |
40+
41+
The `DateTime` object will handle automatic conversions between timezones. For
42+
example, if you supply a datetime string which includes a timezone (such as
43+
`2019-01-09T12:34:56+04:30` which is Afghanistan time) and supply a differing
44+
timezone object (such as `Australia/Perth`) the date will automatically get
45+
converted correctly into the timezone of the supplied object (in this example,
46+
`2019-01-09T16:04:56+08:00`).
47+
48+
```php
49+
<?php
50+
51+
$dateString = '2019-01-09T12:34:56+04:30';
52+
$timezone = new \DateTimeZone('Australia/Perth');
53+
$datetime = new DateTime($dateString, $timezone);
54+
echo $datetime; // string(24) "2019-01-09T16:04:56+08:00"
55+
```
3856

3957
## Brief Example
4058

@@ -49,7 +67,7 @@ try {
4967
'Wednesday, 9th May, 2018 (3:34pm)',
5068
new \DateTimeZone('Australia/Perth')
5169
);
52-
var_dump($datetime); // string(20) "2018-05-09T07:34:00Z"
70+
echo $datetime; // string(20) "2018-05-09T07:34:00Z"
5371
} catch (\InvalidArgumentException $e) {
5472
exit('Could not construct object; value does not conform to date format.');
5573
}

src/DateTime.php

+7-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ class DateTime extends \DateTimeImmutable implements DateTimeInterface
88
public function __construct(?string $datetime = null, ?\DateTimeZone $timezone = null)
99
{
1010
$datetime = new \DateTime($datetime ?: 'now', $timezone);
11-
parent::__construct($datetime->format(DateTimeInterface::NO_TIMEZONE), $timezone);
11+
if ($timezone instanceof \DateTimeZone) {
12+
$datetime->setTimezone($timezone);
13+
}
14+
parent::__construct($datetime->format(DateTimeInterface::NO_TIMEZONE), $timezone ?? $datetime->getTimezone());
1215
}
1316

1417
/**
@@ -17,12 +20,10 @@ public function __construct(?string $datetime = null, ?\DateTimeZone $timezone =
1720
*/
1821
public static function createFromFormat($format, $time, ?\DateTimeZone $timezone = null): self
1922
{
20-
// DateTimeImmutable's createFromFormat() method returns instances of DateTimeImmutable rather than static
21-
// (child class), so we'll unfortunately have to replicate some of the constructor logic here.
2223
if (!\is_object($datetime = \DateTime::createFromFormat($format, $time, $timezone))) {
2324
throw new \InvalidArgumentException('Value not compatible with date format.');
2425
}
25-
return new static($datetime->format(DateTimeInterface::NO_TIMEZONE), $datetime->getTimezone());
26+
return new static($datetime->format(DateTimeInterface::RFC3339), $timezone ?? $datetime->getTimezone());
2627
}
2728

2829
/** {@inheritdoc} */
@@ -38,9 +39,9 @@ public static function createFromObject(\DateTimeInterface $datetime): DateTimeI
3839
}
3940

4041
/** {@inheritdoc} */
41-
public static function createFromTimestamp(int $timestamp): DateTimeInterface
42+
public static function createFromTimestamp(int $timestamp, ?\DateTimeZone $timezone = null): DateTimeInterface
4243
{
43-
return static::createFromFormat('U', (string) $timestamp);
44+
return static::createFromFormat('U', (string) $timestamp, $timezone);
4445
}
4546

4647
/** {@inheritdoc} */

src/DateTimeInterface.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ interface DateTimeInterface extends \DateTimeInterface, \JsonSerializable
1515
public static function createFromObject(\DateTimeInterface $datetime): self;
1616

1717
/**
18-
* @param int $timestamp
18+
* @param integer $timestamp
19+
* @param \DateTimeZone|null $timezone
1920
* @return \Darsyn\DateTime\DateTimeInterface
2021
*/
21-
public static function createFromTimestamp(int $timestamp): self;
22+
public static function createFromTimestamp(int $timestamp, ?\DateTimeZone $timezone = null): self;
2223

2324
/**
2425
* @return string

tests/DateTimeTest.php

+55-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php declare(strict_types=1);
22

3-
namespace App\Tests\Model;
3+
namespace Darsyn\DateTime\Tests;
44

55
use Darsyn\DateTime\DateTime;
66
use Darsyn\DateTime\DateTimeInterface;
@@ -11,42 +11,63 @@ class DateTimeTest extends Test
1111
public function dataProviderValidConstructionValues(): array
1212
{
1313
return [
14-
['2018-05-09T14:18:00+02:00', 'America/Los_Angeles', '2018-05-09T14:18:00+02:00'],
14+
// Like most central African countries, Nairobi has never observed daylight-savings so this test *should*
15+
// always be correct.
16+
['2018-05-09T14:18:00+02:00', 'Africa/Nairobi', '2018-05-09T15:18:00+03:00', 'Africa/Nairobi'],
1517
// Bangkok does not have daylight savings time so this test *should* always be correct.
16-
['2018-01-01T00:00:00', 'Asia/Bangkok', '2018-01-01T00:00:00+07:00'],
17-
['2018-01-01T00:00:00-05:00', 'Asia/Bangkok', '2018-01-01T00:00:00-05:00'],
18-
['2018-01-01T00:00:00-05:00', null, '2018-01-01T00:00:00-05:00'],
19-
18+
['2018-01-01T00:00:00', 'Asia/Bangkok', '2018-01-01T00:00:00+07:00', 'Asia/Bangkok'],
19+
['2018-01-01T00:00:00-05:00', 'Asia/Bangkok', '2018-01-01T12:00:00+07:00', 'Asia/Bangkok'],
20+
['2018-01-01T00:00:00-05:00', null, '2018-01-01T00:00:00-05:00', '-05:00'],
2021
];
2122
}
2223

2324
public function dataProviderValidFormatValues(): array
2425
{
2526
return [
26-
['Y-m-d\TH:i:sP', '2018-05-09T14:18:00+02:00', 'America/Los_Angeles', '2018-05-09T14:18:00+02:00'],
27+
['Y-m-d\TH:i:sP', '2018-05-09T14:18:59+02:00', 'America/Los_Angeles', '2018-05-09T05:18:59-07:00', 'America/Los_Angeles'],
2728
// Perth does not have daylight savings time so this test *should* always be correct.
28-
['l, jS F, Y (g:ia)', 'Wednesday, 9th May, 2018 (3:34pm)', 'Australia/Perth', '2018-05-09T15:34:00+08:00'],
29+
['l, jS F, Y (g:ia)', 'Wednesday, 9th May, 2018 (3:34pm)', 'Australia/Perth', '2018-05-09T15:34:00+08:00', 'Australia/Perth'],
30+
// We must specify the timezone in the string, else the timezone will be taken from the php.ini settings
31+
// which would differ from environment to environment and fail the test.
32+
['l, jS F, Y (g:ia) P', 'Wednesday, 9th May, 2018 (3:34pm) -11:00', null, '2018-05-09T15:34:00-11:00', '-11:00'],
33+
];
34+
}
35+
36+
public function dataProviderValidTimestampValues(): array
37+
{
38+
return [
39+
[1548075139, 'Africa/Nairobi', '2019-01-21T15:52:19+03:00', 'Africa/Nairobi'],
40+
[1500000000, 'Australia/Perth', '2017-07-14T10:40:00+08:00', 'Australia/Perth'],
41+
[1500000000, 'Asia/Bangkok', '2017-07-14T09:40:00+07:00', 'Asia/Bangkok'],
42+
[1400000000, null, null, null],
2943
];
3044
}
3145

3246
/** @dataProvider dataProviderValidConstructionValues */
33-
public function testValidDates(string $value, ?string $timezone, string $expected): void
47+
public function testValidDates(string $value, ?string $timezone, string $expectedDate, string $expectedTz): void
3448
{
3549
$timezone = is_string($timezone)
3650
? new \DateTimeZone($timezone)
3751
: null;
3852
$datetime = new DateTime($value, $timezone);
39-
Test::assertSame($expected, (string) $datetime);
53+
Test::assertSame($expectedDate, (string) $datetime);
54+
Test::assertSame($expectedTz, $datetime->getTimezone()->getName());
4055
}
4156

4257
/** @dataProvider dataProviderValidFormatValues */
43-
public function testValidFormats(string $format, string $value, ?string $timezone, string $expected): void
44-
{
58+
public function testValidFormats(
59+
string $format,
60+
string $value,
61+
?string $timezone,
62+
string $expectedDate,
63+
string $expectedTz
64+
): void {
4565
$timezone = is_string($timezone)
4666
? new \DateTimeZone($timezone)
4767
: null;
4868
$datetime = DateTime::createFromFormat($format, $value, $timezone);
49-
Test::assertSame($expected, (string) $datetime);
69+
Test::assertSame($expectedDate, (string) $datetime);
70+
Test::assertSame($expectedTz, $datetime->getTimezone()->getName());
5071
}
5172

5273
public function testNoConstructorArgumentsIndicateCurrentDate(): void
@@ -75,4 +96,25 @@ public function testDateTimeSerialisesIntoJsonNicelyUnlikeThePhpVersionWhichDoes
7596
// Ensure that it's in standardized UTC format.
7697
Test::assertSame(json_encode($datetime->format(DateTimeInterface::RFC3339)), json_encode($datetime));
7798
}
99+
100+
/** @dataProvider dataProviderValidTimestampValues */
101+
public function testDateIsCorrectWhenTimezoneSuppliedWithTimestamp(
102+
int $timestamp,
103+
?string $timezone,
104+
?string $expectedDate,
105+
?string $expectedTz
106+
): void {
107+
$timezone = \is_string($timezone)
108+
? new \DateTimeZone($timezone)
109+
: null;
110+
$datetime = DateTime::createFromTimestamp($timestamp, $timezone);
111+
Test::assertSame($timestamp, $datetime->getTimestamp());
112+
// A timestamp cannot contain any timezone information, if no timezone was set then the timezone defined in
113+
// php.ini is used, and as it differs depending on the environment we cannot test for it (or the resulting
114+
// date string which is dependant on the timezone).
115+
if ($timezone !== null) {
116+
Test::assertSame($expectedDate, (string) $datetime);
117+
Test::assertSame($expectedTz, $datetime->getTimezone()->getName());
118+
}
119+
}
78120
}

tests/UtcDateTimeTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php declare(strict_types=1);
22

3-
namespace App\Tests\Model;
3+
namespace Darsyn\DateTime\Tests;
44

55
use Darsyn\DateTime\DateTimeInterface;
66
use Darsyn\DateTime\UtcDateTime;

0 commit comments

Comments
 (0)