diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/date-time.iml b/.idea/date-time.iml new file mode 100644 index 0000000..58c557c --- /dev/null +++ b/.idea/date-time.iml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..8310ca9 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..28a804d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f8654f4 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..343ce1f --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml new file mode 100644 index 0000000..4f8104c --- /dev/null +++ b/.idea/phpunit.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/LocalDateRange.php b/src/LocalDateRange.php index 58ca387..a7ed588 100644 --- a/src/LocalDateRange.php +++ b/src/LocalDateRange.php @@ -244,6 +244,25 @@ public function jsonSerialize(): string return (string) $this; } + /** + * Converts this LocalDateRange to Interval instance. + * + * The result is Interval from 00:00 start date and 00:00 end date + one day (because end in Interval is exclude) + * in the given time-zone. + */ + public function toInterval(TimeZone $timeZone): Interval + { + $startZonedDateTime = $this->getStart() + ->atTime(LocalTime::min()) + ->atTimeZone($timeZone); + $endZonedDateTime = $this->getEnd() + ->plusDays(1) + ->atTime(LocalTime::min()) + ->atTimeZone($timeZone); + + return $startZonedDateTime->getIntervalTo($endZonedDateTime); + } + /** * Converts this LocalDateRange to a native DatePeriod object. * diff --git a/src/UtcDateTime.php b/src/UtcDateTime.php new file mode 100644 index 0000000..16a7e99 --- /dev/null +++ b/src/UtcDateTime.php @@ -0,0 +1,173 @@ +isEqualTo(TimeZone::utc())) { + throw new InvalidArgumentException('Create UtcDateTime with not UTC timezone is not supported'); + } + + /** @var UtcDateTime $result */ + $result = parent::of($dateTime, $timeZone); + + return $result; + } + + public static function ofInstant(Instant $instant, TimeZone $timeZone = null): UtcDateTime + { + if ($timeZone === null) { + $timeZone = TimeZone::utc(); + } + if (! $timeZone->isEqualTo(TimeZone::utc())) { + throw new InvalidArgumentException('Create UtcDateTime with not UTC timezone is not supported'); + } + + /** @var UtcDateTime $result */ + $result = parent::ofInstant($instant, $timeZone); + + return $result; + } + + public static function now(TimeZone $timeZone = null, ?Clock $clock = null): UtcDateTime + { + if ($timeZone === null) { + $timeZone = TimeZone::utc(); + } + if (! $timeZone->isEqualTo(TimeZone::utc())) { + throw new InvalidArgumentException('Create UtcDateTime with not UTC timezone is not supported'); + } + + /** @var UtcDateTime $result */ + $result = parent::now($timeZone, $clock); + + return $result; + } + + public static function from(DateTimeParseResult $result): UtcDateTime + { + $methodResult = parent::from($result); + if (! $methodResult->getTimeZone()->isEqualTo(TimeZone::utc())) { + $methodResult = $methodResult->withTimeZoneSameInstant(TimeZone::utc()); + } + + /** @var UtcDateTime $methodResult */ + return $methodResult; + } + + public static function parse(string $text, ?DateTimeParser $parser = null): UtcDateTime + { + $result = parent::parse($text, $parser); + if (! $result->getTimeZone()->isEqualTo(TimeZone::utc())) { + $result = $result->withTimeZoneSameInstant(TimeZone::utc()); + } + + /** @var UtcDateTime $result */ + return $result; + } + + /** + * @deprecated please use fromNativeDateTime instead + */ + public static function fromDateTime(DateTimeInterface $dateTime): UtcDateTime + { + return self::fromNativeDateTime($dateTime); + } + + public static function fromNativeDateTime(DateTimeInterface $dateTime): UtcDateTime + { + $result = parent::fromNativeDateTime($dateTime); + if (! $result->getTimeZone()->isEqualTo(TimeZone::utc())) { + $result = $result->withTimeZoneSameInstant(TimeZone::utc()); + } + + /** @var UtcDateTime $result */ + return $result; + } + + /** + * @param string $input Format "Y-m-d H:i:s.u" or "Y-m-d H:i:s". + */ + public static function fromSqlFormat(string $input, TimeZone $timeZone = null): UtcDateTime + { + if ($timeZone === null) { + $timeZone = TimeZone::utc(); + } + if (! $timeZone->isEqualTo(TimeZone::utc())) { + throw new InvalidArgumentException('Create UtcDateTime with not UTC timezone is not supported'); + } + + /** @var UtcDateTime $result */ + $result = parent::fromSqlFormat($input, $timeZone); + + return $result; + } + + /** + * Convert to RFC 3339 compatible format (2022-03-30T21:00:00.000000Z). + */ + public function toCanonicalFormat(int $precision = 6): string + { + if ($precision < 0 || $precision > 9) { + throw new InvalidArgumentException( + 'Incorrect precision. Expected value between 0 and 9, got: ' . $precision + ); + } + $result = $this->toNativeFormat('Y-m-d\TH:i:s'); + + if ($precision > 0) { + $nano = str_pad((string) $this->getNano(), 9, '0', STR_PAD_LEFT); + $result .= '.' . substr($nano, 0, $precision); + } + $result .= 'Z'; + + return $result; + } +} diff --git a/tests/LocalDateRangeTest.php b/tests/LocalDateRangeTest.php index 0bb6421..a6f1839 100644 --- a/tests/LocalDateRangeTest.php +++ b/tests/LocalDateRangeTest.php @@ -8,6 +8,7 @@ use Brick\DateTime\LocalDate; use Brick\DateTime\LocalDateRange; use Brick\DateTime\Parser\DateTimeParseException; +use Brick\DateTime\TimeZone; use function array_map; use function iterator_count; @@ -240,6 +241,28 @@ public function providerToNativeDatePeriod(): array ]; } + /** + * @dataProvider providerToInterval + */ + public function testToInterval(string $range, string $timeZone, string $expectedInterval): void + { + $actualResult = LocalDateRange::parse($range)->toInterval(TimeZone::parse($timeZone)); + self::assertSame($expectedInterval, (string) $actualResult); + } + + public function providerToInterval(): array + { + return [ + ['2010-01-01/2010-01-01', 'UTC', '2010-01-01T00:00Z/2010-01-02T00:00Z'], + ['2010-01-01/2020-12-31', 'UTC', '2010-01-01T00:00Z/2021-01-01T00:00Z'], + ['2022-03-20/2022-03-26', 'Europe/London', '2022-03-20T00:00Z/2022-03-27T00:00Z'], + ['2022-03-20/2022-03-27', 'Europe/London', '2022-03-20T00:00Z/2022-03-27T23:00Z'], + ['2022-03-20/2022-03-26', 'Europe/Berlin', '2022-03-19T23:00Z/2022-03-26T23:00Z'], + ['2022-03-20/2022-03-27', 'Europe/Berlin', '2022-03-19T23:00Z/2022-03-27T22:00Z'], + ['2022-01-01/2022-12-31', 'Europe/Berlin', '2021-12-31T23:00Z/2022-12-31T23:00Z'], + ]; + } + /** * @dataProvider providerIntersectsWith */