From 9e1fee7672d0a018bf568bb73f8f23d0020b8007 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 17 Jan 2026 16:48:31 +0100 Subject: [PATCH 1/2] typo --- blackbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blackbox.php b/blackbox.php index 1166d18..148aac1 100644 --- a/blackbox.php +++ b/blackbox.php @@ -15,7 +15,7 @@ static fn(Application $app) => $app->scenariiPerProof((int) \getenv('BLACKBOX_SET_SIZE')), ) ->when( - \get_env('ENABLE_COVERAGE') !== false, + \getenv('ENABLE_COVERAGE') !== false, static fn(Application $app) => $app ->scenariiPerProof(1) ->codeCoverage( From 8322daae3146ce3722f02e95f119b82ae353994e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 17 Jan 2026 17:18:37 +0100 Subject: [PATCH 2/2] copu innmind/time-continuum --- Makefile | 6 + README.md | 31 +- blackbox.php | 6 +- composer.json | 7 +- docs/assets/favicon.png | Bin 0 -> 2319 bytes docs/assets/fonts/MonaspaceNeon-Regular.woff | Bin 0 -> 47308 bytes docs/assets/logo.svg | 24 + docs/assets/stylesheets/extra.css | 113 +++ docs/getting-started/clocks.md | 88 ++ docs/getting-started/elapsed-period.md | 24 + docs/getting-started/formats.md | 88 ++ docs/getting-started/index.md | 7 + docs/getting-started/periods.md | 56 ++ docs/getting-started/points-in-time.md | 112 +++ docs/getting-started/time-offsets.md | 28 + docs/getting-started/timezones.md | 533 ++++++++++++ docs/index.md | 40 + docs/preface/philosophy.md | 13 + docs/preface/terminology.md | 72 ++ docs/upgrade/v3-to-v4.md | 155 ++++ fixtures/Period.php | 65 ++ fixtures/Point.php | 71 ++ fixtures/Timezone.php | 40 + mkdocs.yml | 104 +++ proofs/.gitkeep | 0 proofs/clock.php | 261 ++++++ proofs/elapsedPeriod.php | 174 ++++ proofs/move/endOfDay.php | 55 ++ proofs/move/endOfMonth.php | 108 +++ proofs/move/endOfYear.php | 55 ++ proofs/move/startOfDay.php | 55 ++ proofs/move/startOfMonth.php | 55 ++ proofs/move/startOfYear.php | 55 ++ proofs/period.php | 71 ++ proofs/point.php | 65 ++ proofs/point/highResolution.php | 40 + src/.gitkeep | 0 src/Calendar/Day.php | 53 ++ src/Calendar/Month.php | 68 ++ src/Clock.php | 104 +++ src/Clock/Implementation.php | 37 + src/Clock/Logger.php | 79 ++ src/Clock/OfFormat.php | 44 + src/Clock/Via.php | 99 +++ src/ElapsedPeriod.php | 77 ++ src/Format.php | 119 +++ src/Format/Custom.php | 15 + src/Move/EndOfDay.php | 27 + src/Move/EndOfMonth.php | 24 + src/Move/EndOfYear.php | 25 + src/Move/Month.php | 67 ++ src/Move/StartOfDay.php | 27 + src/Move/StartOfMonth.php | 23 + src/Move/StartOfYear.php | 23 + src/Offset.php | 108 +++ src/Period.php | 493 +++++++++++ src/Period/Value.php | 32 + src/Point.php | 323 +++++++ src/Point/Day.php | 72 ++ src/Point/HighResolution.php | 96 +++ src/Point/Hour.php | 48 ++ src/Point/Microsecond.php | 39 + src/Point/Millisecond.php | 39 + src/Point/Minute.php | 48 ++ src/Point/Month.php | 48 ++ src/Point/Second.php | 39 + src/Point/Year.php | 55 ++ src/Timezone.php | 34 + src/Timezone/Africa.php | 352 ++++++++ src/Timezone/America.php | 849 +++++++++++++++++++ src/Timezone/America/Argentina.php | 106 +++ src/Timezone/America/Indiana.php | 76 ++ src/Timezone/America/NorthDakota.php | 46 + src/Timezone/Antartica.php | 100 +++ src/Timezone/Arctic.php | 34 + src/Timezone/Asia.php | 574 +++++++++++++ src/Timezone/Atlantic.php | 100 +++ src/Timezone/Australia.php | 160 ++++ src/Timezone/Europe.php | 382 +++++++++ src/Timezone/Indian.php | 94 ++ src/Timezone/Pacific.php | 280 ++++++ src/Timezones.php | 105 +++ tests/Clock/FrozenTest.php | 52 ++ tests/Clock/LiveTest.php | 114 +++ tests/Clock/LoggerTest.php | 159 ++++ tests/ClockTest.php | 691 +++++++++++++++ tests/ElapsedPeriodTest.php | 82 ++ tests/Fixtures/PeriodTest.php | 109 +++ tests/Fixtures/PointTest.php | 69 ++ tests/FormatTest.php | 30 + tests/Move/EndOfDayTest.php | 35 + tests/Move/EndOfMonthTest.php | 37 + tests/Move/EndOfYearTest.php | 36 + tests/Move/MonthTest.php | 40 + tests/Move/StartOfDayTest.php | 35 + tests/Move/StartOfMonthTest.php | 34 + tests/Move/StartOfYearTest.php | 35 + tests/NowTest.php | 168 ++++ tests/Period.php | 84 ++ tests/Period/DayTest.php | 69 ++ tests/Period/HourTest.php | 95 +++ tests/Period/MillisecondTest.php | 118 +++ tests/Period/MinuteTest.php | 103 +++ tests/Period/MonthTest.php | 78 ++ tests/Period/SecondTest.php | 110 +++ tests/Period/YearTest.php | 52 ++ tests/Point/DayTest.php | 21 + tests/Point/HighResolutionTest.php | 75 ++ tests/Point/HourTest.php | 18 + tests/Point/MillisecondTest.php | 17 + tests/Point/MinuteTest.php | 18 + tests/Point/MonthTest.php | 19 + tests/Point/SecondTest.php | 17 + tests/Point/YearTest.php | 21 + tests/PointTest.php | 218 +++++ 115 files changed, 11174 insertions(+), 5 deletions(-) create mode 100644 Makefile create mode 100644 docs/assets/favicon.png create mode 100644 docs/assets/fonts/MonaspaceNeon-Regular.woff create mode 100644 docs/assets/logo.svg create mode 100644 docs/assets/stylesheets/extra.css create mode 100644 docs/getting-started/clocks.md create mode 100644 docs/getting-started/elapsed-period.md create mode 100644 docs/getting-started/formats.md create mode 100644 docs/getting-started/index.md create mode 100644 docs/getting-started/periods.md create mode 100644 docs/getting-started/points-in-time.md create mode 100644 docs/getting-started/time-offsets.md create mode 100644 docs/getting-started/timezones.md create mode 100644 docs/index.md create mode 100644 docs/preface/philosophy.md create mode 100644 docs/preface/terminology.md create mode 100644 docs/upgrade/v3-to-v4.md create mode 100644 fixtures/Period.php create mode 100644 fixtures/Point.php create mode 100644 fixtures/Timezone.php create mode 100644 mkdocs.yml delete mode 100644 proofs/.gitkeep create mode 100644 proofs/clock.php create mode 100644 proofs/elapsedPeriod.php create mode 100644 proofs/move/endOfDay.php create mode 100644 proofs/move/endOfMonth.php create mode 100644 proofs/move/endOfYear.php create mode 100644 proofs/move/startOfDay.php create mode 100644 proofs/move/startOfMonth.php create mode 100644 proofs/move/startOfYear.php create mode 100644 proofs/period.php create mode 100644 proofs/point.php create mode 100644 proofs/point/highResolution.php delete mode 100644 src/.gitkeep create mode 100644 src/Calendar/Day.php create mode 100644 src/Calendar/Month.php create mode 100644 src/Clock.php create mode 100644 src/Clock/Implementation.php create mode 100644 src/Clock/Logger.php create mode 100644 src/Clock/OfFormat.php create mode 100644 src/Clock/Via.php create mode 100644 src/ElapsedPeriod.php create mode 100644 src/Format.php create mode 100644 src/Format/Custom.php create mode 100644 src/Move/EndOfDay.php create mode 100644 src/Move/EndOfMonth.php create mode 100644 src/Move/EndOfYear.php create mode 100644 src/Move/Month.php create mode 100644 src/Move/StartOfDay.php create mode 100644 src/Move/StartOfMonth.php create mode 100644 src/Move/StartOfYear.php create mode 100644 src/Offset.php create mode 100644 src/Period.php create mode 100644 src/Period/Value.php create mode 100644 src/Point.php create mode 100644 src/Point/Day.php create mode 100644 src/Point/HighResolution.php create mode 100644 src/Point/Hour.php create mode 100644 src/Point/Microsecond.php create mode 100644 src/Point/Millisecond.php create mode 100644 src/Point/Minute.php create mode 100644 src/Point/Month.php create mode 100644 src/Point/Second.php create mode 100644 src/Point/Year.php create mode 100644 src/Timezone.php create mode 100644 src/Timezone/Africa.php create mode 100644 src/Timezone/America.php create mode 100644 src/Timezone/America/Argentina.php create mode 100644 src/Timezone/America/Indiana.php create mode 100644 src/Timezone/America/NorthDakota.php create mode 100644 src/Timezone/Antartica.php create mode 100644 src/Timezone/Arctic.php create mode 100644 src/Timezone/Asia.php create mode 100644 src/Timezone/Atlantic.php create mode 100644 src/Timezone/Australia.php create mode 100644 src/Timezone/Europe.php create mode 100644 src/Timezone/Indian.php create mode 100644 src/Timezone/Pacific.php create mode 100644 src/Timezones.php create mode 100644 tests/Clock/FrozenTest.php create mode 100644 tests/Clock/LiveTest.php create mode 100644 tests/Clock/LoggerTest.php create mode 100644 tests/ClockTest.php create mode 100644 tests/ElapsedPeriodTest.php create mode 100644 tests/Fixtures/PeriodTest.php create mode 100644 tests/Fixtures/PointTest.php create mode 100644 tests/FormatTest.php create mode 100644 tests/Move/EndOfDayTest.php create mode 100644 tests/Move/EndOfMonthTest.php create mode 100644 tests/Move/EndOfYearTest.php create mode 100644 tests/Move/MonthTest.php create mode 100644 tests/Move/StartOfDayTest.php create mode 100644 tests/Move/StartOfMonthTest.php create mode 100644 tests/Move/StartOfYearTest.php create mode 100644 tests/NowTest.php create mode 100644 tests/Period.php create mode 100644 tests/Period/DayTest.php create mode 100644 tests/Period/HourTest.php create mode 100644 tests/Period/MillisecondTest.php create mode 100644 tests/Period/MinuteTest.php create mode 100644 tests/Period/MonthTest.php create mode 100644 tests/Period/SecondTest.php create mode 100644 tests/Period/YearTest.php create mode 100644 tests/Point/DayTest.php create mode 100644 tests/Point/HighResolutionTest.php create mode 100644 tests/Point/HourTest.php create mode 100644 tests/Point/MillisecondTest.php create mode 100644 tests/Point/MinuteTest.php create mode 100644 tests/Point/MonthTest.php create mode 100644 tests/Point/SecondTest.php create mode 100644 tests/Point/YearTest.php create mode 100644 tests/PointTest.php diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6b5c6f1 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +# This command is intended to be run on your computer +serve-doc: + docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material + +build-doc: + docker run --rm -it -v ${PWD}:/docs squidfunk/mkdocs-material build diff --git a/README.md b/README.md index 41dc200..6e807d0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ [![codecov](https://codecov.io/gh/innmind/time/branch/develop/graph/badge.svg)](https://codecov.io/gh/innmind/time) [![Type Coverage](https://shepherd.dev/github/innmind/time/coverage.svg)](https://shepherd.dev/github/innmind/time) -Description +This library allows you to handle time down to the millisecond. The point was to also be explicit for every component of dates, this is why every php _magic strings_ have been converted into objects. + +**All objects are immutable.** ## Installation @@ -14,4 +16,29 @@ composer require innmind/time ## Usage -Todo +```php +use Innmind\Time\{ + Clock, + Point, + Format, +}; +use Innmind\Immutable\Attempt; + +$clock = Clock::live(); +$now = $clock->now(); // return an instance of Point +echo $now->toString(); // 2016-10-11T12:17:30.123456+02:00 + +$epoch = $clock->at( + '1970-01-01T00:00:00.000000+00:00', + Format::iso8601(), +); // Attempt +``` + +Here we reference 2 points in time, the first is the exact moment we call `now` down to the microsecond and the second one is the epoch time. + +The method `at()` accepts any string that is allowed by `\DateTimeImmutable`. + +## Documentation + +Full documentation is available at . + diff --git a/blackbox.php b/blackbox.php index 148aac1..f95d8f6 100644 --- a/blackbox.php +++ b/blackbox.php @@ -7,6 +7,7 @@ Application, Runner\Load, Runner\CodeCoverage, + PHPUnit, }; Application::new($argv) @@ -27,5 +28,8 @@ ->enableWhen(true), ), ) - ->tryToProve(Load::everythingIn(__DIR__.'/proofs/')) + ->tryToProve(static function() { + yield from Load::everythingIn(__DIR__.'/proofs/')(); + yield from PHPUnit\Load::testsAt(__DIR__.'/tests/'); + }) ->exit(); diff --git a/composer.json b/composer.json index a3cb5f2..9a4b144 100644 --- a/composer.json +++ b/composer.json @@ -15,11 +15,14 @@ "issues": "http://github.com/innmind/time/issues" }, "require": { - "php": "~8.4" + "php": "~8.4", + "innmind/immutable": "~6.0", + "psr/log": "~3.0" }, "autoload": { "psr-4": { - "Innmind\\Time\\": "src/" + "Innmind\\Time\\": "src/", + "Fixtures\\Innmind\\Time\\": "fixtures/" } }, "require-dev": { diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..08dee3a38bab0f4b6161a9ea6c575a347daaa737 GIT binary patch literal 2319 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

m)-Jo-MAPW+|=Jl-g@ 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..6a5d322 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 0000000..e4aa2d4 --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,113 @@ +@font-face { + font-family: "Monaspace Neon"; + font-weight: normal; + font-style: normal; + src: url("../fonts/MonaspaceNeon-Regular.woff"); +} + +:root { + --md-code-font: "Monaspace Neon"; +} + +:root { + --light-md-code-hl-number-color: #f76d47; + --light-md-code-hl-function-color: #6384b9; + --light-md-code-hl-operator-color: #39adb5; + --light-md-code-hl-constant-color: #7c4dff; + --light-md-code-hl-string-color: #9fc06f; + --light-md-code-hl-punctuation-color: #39adb5; + --light-md-code-hl-keyword-color: #7c4dff; + --light-md-code-hl-variable-color: #80cbc4; + --light-md-code-hl-comment-color: #ccd7da; + --light-md-code-bg-color: #fafafa; + --light-md-code-fg-color: #ffb62c; + --light-md-code-hl-variable-color: #6384b9; + --dark-md-code-hl-number-color: #f78c6c; + --dark-md-code-hl-function-color: #82aaff; + --dark-md-code-hl-operator-color: #89ddff; + --dark-md-code-hl-constant-color: #c792ea; + --dark-md-code-hl-string-color: #c3e88d; + --dark-md-code-hl-punctuation-color: #89ddff; + --dark-md-code-hl-keyword-color: #c792ea; + --dark-md-code-hl-variable-color: #e8f9f9; + --dark-md-code-hl-comment-color: #546e7a; + --dark-md-code-bg-color: #263238; + --dark-md-code-fg-color: #ffcb6b; + --dark-md-code-hl-variable-color: #82aaff; +} + +@media (prefers-color-scheme: light) { + .language-php > * { + --md-code-hl-number-color: var(--light-md-code-hl-number-color); + --md-code-hl-function-color: var(--light-md-code-hl-function-color); + --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); + --md-code-hl-string-color: var(--light-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); + --md-code-bg-color: var(--light-md-code-bg-color); + --md-code-fg-color: var(--light-md-code-fg-color); + } + + .language-php .na { + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + } +} + +[data-md-color-media="(prefers-color-scheme: light)"] .language-php > * { + --md-code-hl-number-color: var(--light-md-code-hl-number-color); + --md-code-hl-function-color: var(--light-md-code-hl-function-color); + --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); + --md-code-hl-string-color: var(--light-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); + --md-code-bg-color: var(--light-md-code-bg-color); + --md-code-fg-color: var(--light-md-code-fg-color); +} + +[data-md-color-media="(prefers-color-scheme: light)"] .language-php .na { + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); +} + +@media (prefers-color-scheme: dark) { + .language-php > * { + --md-code-hl-number-color: var(--dark-md-code-hl-number-color); + --md-code-hl-function-color: var(--dark-md-code-hl-function-color); + --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); + --md-code-hl-string-color: var(--dark-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); + --md-code-bg-color: var(--dark-md-code-bg-color); + --md-code-fg-color: var(--dark-md-code-fg-color); + } + + .language-php .na { + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + } +} + +[data-md-color-media="(prefers-color-scheme: dark)"] .language-php > * { + --md-code-hl-number-color: var(--dark-md-code-hl-number-color); + --md-code-hl-function-color: var(--dark-md-code-hl-function-color); + --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); + --md-code-hl-string-color: var(--dark-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); + --md-code-bg-color: var(--dark-md-code-bg-color); + --md-code-fg-color: var(--dark-md-code-fg-color); +} + +[data-md-color-media="(prefers-color-scheme: dark)"] .language-php .na { + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); +} diff --git a/docs/getting-started/clocks.md b/docs/getting-started/clocks.md new file mode 100644 index 0000000..9b59cdf --- /dev/null +++ b/docs/getting-started/clocks.md @@ -0,0 +1,88 @@ +# Clocks + +!!! success "Dependency injection" + A `Clock` should be treated as a singleton and instanciated once in your program and then passed as argument everywhere you need it in your program. + +## Live + +This is the clock you should use in your programs. By default it's set to the [UTC](https://en.wikipedia.org/wiki/UTC%2B00:00) [timezone](timezones.md) (no matter the configuration of your machine). + +To access the current time you would do: + +```php +use Innmind\TimeContinuum\{ + Clock, + PointInTime, +}; + +$clock = Clock::live(); +$point = $clock->now(); // instance of PointInTime +echo $point->toString(); // prints something like 2024-11-24T12:34:25+00:00 +``` + +And to build a [`PointInTime`](points-in-time.md) back from a `string`: + +```php +use Innmind\TimeContinuum\{ + Clock, + Format, + PointInTime, +}; +use Innmind\Immutable\Attempt; + +$time = '2024-11-24T12:34:25+00:00'; + +$clock = Clock::live(); +$at = $clock->at($time, Format::iso8601()); // instance of Attempt +$point = $at->match( + static fn(PointInTime $point) => $point, + static fn() => null, +); +``` + +The `at` method returns an [`Attempt` monad](https://innmind.org/Immutable/structures/attempt/) that may contain a `PointInTime`. This is in case the `#!php $time` variable contains a value that doesn't correspond to the specified format (here `ISO8601`). + +This means that the `#!php $point` variable here is an instance of `PointInTime` because the `#!php $time` value is valid. If it's invalid then `#!php $point` is `#!php null`. + +## Logger + +This clock will create a log everytime you call `#!php ->now()` or `#!php ->at()`. + +To build this clock you need another clock (typically a live one) and a [PSR logger](https://packagist.org/packages/psr/log): + +```php +use Innmind\TimeContinuum\Clock; +use Psr\Log\LoggerInterface; + +$clock = Clock::logger( + Clock::live(), + /* any instance of LoggerInterface (1) */ +); +``` + +1. Like [monolog](https://packagist.org/packages/monolog/monolog) for example. + +You can then use `#!php $clock` like any other clock. + +## Frozen + +This clock is only useful when testing your program. It allows to specify the point in time at which your programs run. + +This way you can test your program for special scenarii like a leap year, daylight saving time and so on... + +```php +use Innmind\TimeContinuum\{ + Clock, + Format, +}; + +$clock = Clock::live() + ->at('2024-11-24T12:34:25+00:00', Format::iso8601()) + ->match( + Clock::frozen(...), + static fn(\Throwable $e) => throw $e + ); +``` + +??? warning + Bear in mind that `#!php $clock->now()` will always return the same object. This means that if your program rely on calculating an [elapsed period](elapsed-period.md) it will always return `#!php 0`. If run in a loop you may end up with an inifinite one. diff --git a/docs/getting-started/elapsed-period.md b/docs/getting-started/elapsed-period.md new file mode 100644 index 0000000..b8a1710 --- /dev/null +++ b/docs/getting-started/elapsed-period.md @@ -0,0 +1,24 @@ +# Elapsed period + +This is the number of microseconds between two [points in time](points-in-time.md). + +```php +use Innmind\TimeContinuum\Clock; + +$clock = Clock::live(); +$start = $clock->now(); +// do some stuff +$end = $clock->now(); + +$elapsed = $end->elapsedSince($start); +``` + +`$elapsed` is an instance of `#!php Innmind\TimeContinuum\ElapsedPeriod`. + +This is especially useful when working with network I/O to check for timeouts. + +!!! success "" + This example uses a monotonic clock internally to avoid the problem where the server clock re-synchronize and jump back in time. In this case `$end` is _technically_ before `$start` but the elapsed period is still a positive `int`. + +??? info + Bear in mind that the monotonic clock only works on `PointInTime`s returned by `$clock->now()`. If `->elapsedSince()` is called on points returned by `$clock->at()` it will compare the number of microseconds since epoch. diff --git a/docs/getting-started/formats.md b/docs/getting-started/formats.md new file mode 100644 index 0000000..bdb5d57 --- /dev/null +++ b/docs/getting-started/formats.md @@ -0,0 +1,88 @@ +# Formats + +A `Format` is a representation on how to convert a [`PointInTime`](points-in-time.md) to a `string`, or vice versa. + +By default this library comes with these formats: + +```php +use Innmind\TimeContinuum\Format; + +Format::cookie(); +Format::iso8601(); +Format::rfc1036(); +Format::rfc1123(); +Format::rfc2822(); +Format::rfc822(); +Format::rfc850(); +Format::rss(); +``` + +Formats are wrapped in an object in order to give them a name. When used in your application you can reference these names instead of duplicating the strings everywhere. + +## Convert to a string + +```php +use Innmind\TimeContinuum\{ + Clock, + Format, +}; + +echo Clock::live() + ->now() + ->format(Format::iso8601()); +``` + +This would print something like `#!php '2024-11-24T14:50:00+00:00'`. + +## Convert from a string + +```php +use Innmind\TimeContinuum\{ + Clock, + Format, + PointInTime, +}; + +$point = Clock::live() + ->at('some string', Format::iso8601()) + ->match( + static fn(PointInTime $point) => $point, + static fn() => null, + ); +``` + +Here `#!php $point` is `#!php null` because `#!php 'some string'` is not a valid date. + +## Define your own format + +If you want to use your own format you can do this via `#!php Format::of('date format')`. The `string` can be anything accepted by [`#!php \DateTimeImmutable::format()`](https://www.php.net/manual/en/datetime.format.php). + +You're encouraged to statically define these formats somewhere in your program like this: + +=== "Static method" + ```php + use Innmind\TimeContinuum\Format; + + final class MyFormats + { + public static function iso8601WithMicroseconds(): Format + { + return Format::of('Y-m-dT:H:i:s.uP'); + } + } + ``` + +=== "Enum" + ```php + use Innmind\TimeContinuum\Format; + + enum MyFormats: string implements Format\Custom + { + case iso8601WithMicroseconds = 'Y-m-dT:H:i:s.uP'; + + public function normalize(): Format + { + return Format::of($this->value); + } + } + ``` diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000..4971c3d --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,7 @@ +# Getting started + +## Installation + +```sh +composer require innmind/time-continuum +``` diff --git a/docs/getting-started/periods.md b/docs/getting-started/periods.md new file mode 100644 index 0000000..3f15f7a --- /dev/null +++ b/docs/getting-started/periods.md @@ -0,0 +1,56 @@ +# Periods + +## Go forward in time + +```php +use Innmind\TimeContinuum\{ + Clock, + Period, +}; + +$future = Clock::live() + ->now() + ->goForward( + Period::day(1) + ->and(Period::hour(12)), + ); +``` + +`#!php $future` is now a day and a half ahead of the current time. + +## Go back in time + +```php +use Innmind\TimeContinuum\{ + Clock, + Period, +}; + +$past = Clock::live() + ->now() + ->goBack( + Period::day(1) + ->and(Period::hour(12)), + ); +``` + +`#!php $past` is now a day and a half behind of the current time. + +## Compare to an elapsed period + +```php +use Innmind\TimeContinuum\{ + Clock, + Period, +}; + +$clock = Clock::live(); +$start = $clock->now(); +// do some stuff +$clock + ->now() + ->elapsedSince($start) + ->longerThan( + Period::second(10)->asElapsedPeriod(), + ); // returns a bool +``` diff --git a/docs/getting-started/points-in-time.md b/docs/getting-started/points-in-time.md new file mode 100644 index 0000000..63968ba --- /dev/null +++ b/docs/getting-started/points-in-time.md @@ -0,0 +1,112 @@ +# Points in time + +See the [clocks](clocks.md) to learn to have access to these objects. + +All examples below use the `#!php $point` variable that reference an instance of `#!php Innmind\TimeContinuum\PointInTime`. + +## Year + +```php +$point->year()->toInt(); +``` + +This will return the year as an `int`. + +```php +$point->year()->numberOfDays(); +``` + +This returns `#!php 365` or `#!php 366` on leap years. + +## Month + +```php +$point->month()->ofYear(); +``` + +This returns a value from the enum `#!php Innmind\TimeContinuum\Calendar\Month`. + +```php +$point->month()->numberOfDays(); +``` + +This returns an `int` between `#!php 28` and `#!php 31`. + +## Day + +```php +$point->day()->ofYear(); +``` + +This returns an `int` between `#!php 0` and `#!php 365`. + +```php +$point->day()->ofMonth(); +``` + +This returns an `int` between `#!php 1` and `#!php 31`. + +```php +$point->day()->ofWeek(); +``` + +This returns a value from the enum `#!php Innmind\TimeContinuum\Calendar\Day`. + +## Hour + +```php +$point->hour()->toInt(); +``` + +This returns an `int` between `#!php 0` and `#!php 23`. + +## Minute + +```php +$point->minute()->toInt(); +``` + +This returns an `int` between `#!php 0` and `#!php 59`. + +## Second + +```php +$point->second()->toInt(); +``` + +This returns an `int` between `#!php 0` and `#!php 59`. + +## Millisecond + +```php +$point->millisecond()->toInt(); +``` + +This returns an `int` between `#!php 0` and `#!php 999`. + +## Microsecond + +```php +$point->microsecond()->toInt(); +``` + +This returns an `int` between `#!php 0` and `#!php 999`. + +## Offset + +```php +$offset = $point->offset(); +$hours = $offset->hours(); +$minutes = $offset->minutes(); +``` + +`$hours` is an `int` between `#!php -12` and `#!php 14`. `$minutes` is an `int` between `#!php 0` and `#!php 59` but usually is either `#!php 0`, `#!php 15`, `#!php 30` or `#!php 45`. + +## Comparing points + +```php +$point->aheadOf($anotherPoint); +$point->equals($anotherPoint); +``` + +Both methods return a `bool`. diff --git a/docs/getting-started/time-offsets.md b/docs/getting-started/time-offsets.md new file mode 100644 index 0000000..3672e0e --- /dev/null +++ b/docs/getting-started/time-offsets.md @@ -0,0 +1,28 @@ +# Time offsets + +This is a value retrieved from a [point in time](points-in-time.md) and is expressed in a number of hours and minutes. + +```php +use Innmind\TimeContinuum\Clock; + +$offset = Clock::live()->now()->offset(); +``` + +- `#!php $offset->hours()` returns an `int` between `#!php -12` and `#!php 14` +- `#!php $offset->minutes()` returns an `int` between `#!php 0` and `#!php 59` + +Via this object you cannot know in which [timezone](timezones.md) you're in. If you need to keep track of this you should model this in your program and not rely on this information. + +When you have access to a `PointInTime` you can change its offset like this: + +```php +use Innmind\TimeContinuum\{ + Clock, + Offset, +}; + +$utc = Clock::live()->now(); +$newYork = $utc->changeOffset(Offset::minus(5)); +``` + +If `#!php $utc` represents `#!php '2024-11-24T14:25:00+00:00'` then `#!php $newYork` represents `#!php '2024-11-24T09:25:00-05:00'`. diff --git a/docs/getting-started/timezones.md b/docs/getting-started/timezones.md new file mode 100644 index 0000000..fbfce16 --- /dev/null +++ b/docs/getting-started/timezones.md @@ -0,0 +1,533 @@ +# Timezones + +A timezone is a [time offset](time-offsets.md) referenced by a city name. + +As described in the [terminology](../preface/terminology.md#timezone) section the offset for a city may not always be the same depending when you are in time. + +Because it can change, a timezone is handled at the [clock](clocks.md) level. By default the clock offset is `+00:00` but you can change it like this: + +```php +use Innmind\TimeContinuum\{ + Clock, + Timezones, +}; + +$utc = Clock::live(); +$paris = $utc->switch( + static fn(Timezones $timezones) => $timezones + ->europe() + ->paris(), +); +``` + +We now have two clocks. `#!php $utc` is still at `+00:00` and `#!php $paris` will depend on when this code is called. If you run this the 24 november (of any year) it will use `+01:00` and if run the 1 july (of any year) it will use `+02:00`. Because France use daylight saving time and change its offset twice a year. + +!!! note + Once the timezone offset is applied to the returned clock it no longer changes. This is important if you have long running processes. + + For example if you started a process on the 26th of october 2024, switched to the Paris timezone, and let the process run for multiple days it will always be at `+02:00`. Even though Paris switch to `+01:00` on the 27th of october. + +## Available timezones + +`#!php $timezones` represents an intance of `Innmind\TimeContinuum\Timezones`. + +=== "Africa" + ```php + $timezones->africa()->lome(); + $timezones->africa()->ceuta(); + $timezones->africa()->elAaiun(); + $timezones->africa()->portoNovo(); + $timezones->africa()->djibouti(); + $timezones->africa()->windhoek(); + $timezones->africa()->algiers(); + $timezones->africa()->ouagadougou(); + $timezones->africa()->bamako(); + $timezones->africa()->harare(); + $timezones->africa()->bujumbura(); + $timezones->africa()->douala(); + $timezones->africa()->brazzaville(); + $timezones->africa()->tripoli(); + $timezones->africa()->casablanca(); + $timezones->africa()->niamey(); + $timezones->africa()->mbabane(); + $timezones->africa()->blantyre(); + $timezones->africa()->conakry(); + $timezones->africa()->khartoum(); + $timezones->africa()->luanda(); + $timezones->africa()->libreville(); + $timezones->africa()->maseru(); + $timezones->africa()->lusaka(); + $timezones->africa()->darEsSalaam(); + $timezones->africa()->nairobi(); + $timezones->africa()->banjul(); + $timezones->africa()->bissau(); + $timezones->africa()->nouakchott(); + $timezones->africa()->johannesburg(); + $timezones->africa()->timbuktu(); + $timezones->africa()->saoTome(); + $timezones->africa()->freetown(); + $timezones->africa()->kampala(); + $timezones->africa()->dakar(); + $timezones->africa()->lagos(); + $timezones->africa()->cairo(); + $timezones->africa()->mogadishu(); + $timezones->africa()->gaborone(); + $timezones->africa()->tunis(); + $timezones->africa()->kigali(); + $timezones->africa()->malabo(); + $timezones->africa()->abidjan(); + $timezones->africa()->accra(); + $timezones->africa()->asmera(); + $timezones->africa()->ndjamena(); + $timezones->africa()->lubumbashi(); + $timezones->africa()->juba(); + $timezones->africa()->monrovia(); + $timezones->africa()->maputo(); + $timezones->africa()->kinshasa(); + $timezones->africa()->asmara(); + $timezones->africa()->bangui(); + $timezones->africa()->addisAbaba(); + ``` + +=== "America" + ```php + $timezones->america()->argentina()->rioGallegos(); + $timezones->america()->argentina()->mendoza(); + $timezones->america()->argentina()->buenosAires(); + $timezones->america()->argentina()->ushuaia(); + $timezones->america()->argentina()->sanJuan(); + $timezones->america()->argentina()->laRioja(); + $timezones->america()->argentina()->salta(); + $timezones->america()->argentina()->sanLuis(); + $timezones->america()->argentina()->jujuy(); + $timezones->america()->argentina()->tucuman(); + $timezones->america()->argentina()->comodRivadavia(); + $timezones->america()->argentina()->catamarca(); + $timezones->america()->argentina()->cordoba(); + $timezones->america()->indiana()->vincennes(); + $timezones->america()->indiana()->marengo(); + $timezones->america()->indiana()->tellCity(); + $timezones->america()->indiana()->knox(); + $timezones->america()->indiana()->vevay(); + $timezones->america()->indiana()->indianapolis(); + $timezones->america()->indiana()->petersburg(); + $timezones->america()->indiana()->winamac(); + $timezones->america()->northDakota()->beulah(); + $timezones->america()->northDakota()->newSalem(); + $timezones->america()->northDakota()->center(); + $timezones->america()->montreal(); + $timezones->america()->guatemala(); + $timezones->america()->boaVista(); + $timezones->america()->portoAcre(); + $timezones->america()->winnipeg(); + $timezones->america()->santiago(); + $timezones->america()->virgin(); + $timezones->america()->moncton(); + $timezones->america()->noronha(); + $timezones->america()->recife(); + $timezones->america()->saintKitts(); + $timezones->america()->rankinInlet(); + $timezones->america()->jamaica(); + $timezones->america()->lima(); + $timezones->america()->rosario(); + $timezones->america()->cambridgeBay(); + $timezones->america()->coralHarbour(); + $timezones->america()->fortWayne(); + $timezones->america()->nassau(); + $timezones->america()->mazatlan(); + $timezones->america()->grandTurk(); + $timezones->america()->merida(); + $timezones->america()->ensenada(); + $timezones->america()->rainyRiver(); + $timezones->america()->bahiaBanderas(); + $timezones->america()->guadeloupe(); + $timezones->america()->cuiaba(); + $timezones->america()->scoresbysund(); + $timezones->america()->maceio(); + $timezones->america()->curacao(); + $timezones->america()->aruba(); + $timezones->america()->monterrey(); + $timezones->america()->hermosillo(); + $timezones->america()->guayaquil(); + $timezones->america()->managua(); + $timezones->america()->matamoros(); + $timezones->america()->losAngeles(); + $timezones->america()->tegucigalpa(); + $timezones->america()->monticello(); + $timezones->america()->nome(); + $timezones->america()->montevideo(); + $timezones->america()->gooseBay(); + $timezones->america()->boise(); + $timezones->america()->belem(); + $timezones->america()->atikokan(); + $timezones->america()->swiftCurrent(); + $timezones->america()->detroit(); + $timezones->america()->laPaz(); + $timezones->america()->chicago(); + $timezones->america()->creston(); + $timezones->america()->nipigon(); + $timezones->america()->costaRica(); + $timezones->america()->halifax(); + $timezones->america()->yellowknife(); + $timezones->america()->puertoRico(); + $timezones->america()->edmonton(); + $timezones->america()->mexicoCity(); + $timezones->america()->saoPaulo(); + $timezones->america()->yakutat(); + $timezones->america()->saintThomas(); + $timezones->america()->chihuahua(); + $timezones->america()->grenada(); + $timezones->america()->elSalvador(); + $timezones->america()->santoDomingo(); + $timezones->america()->montserrat(); + $timezones->america()->portoVelho(); + $timezones->america()->panama(); + $timezones->america()->antigua(); + $timezones->america()->santarem(); + $timezones->america()->dawson(); + $timezones->america()->saintBarthelemy(); + $timezones->america()->iqaluit(); + $timezones->america()->eirunepe(); + $timezones->america()->inuvik(); + $timezones->america()->anguilla(); + $timezones->america()->portOfSpain(); + $timezones->america()->araguaina(); + $timezones->america()->guyana(); + $timezones->america()->fortaleza(); + $timezones->america()->blancSablon(); + $timezones->america()->juneau(); + $timezones->america()->cayman(); + $timezones->america()->menominee(); + $timezones->america()->cayenne(); + $timezones->america()->pangnirtung(); + $timezones->america()->metlakatla(); + $timezones->america()->asuncion(); + $timezones->america()->saintLucia(); + $timezones->america()->saintVincent(); + $timezones->america()->martinique(); + $timezones->america()->kralendijk(); + $timezones->america()->newYork(); + $timezones->america()->vancouver(); + $timezones->america()->bogota(); + $timezones->america()->dominica(); + $timezones->america()->danmarkshavn(); + $timezones->america()->anchorage(); + $timezones->america()->marigot(); + $timezones->america()->rioBranco(); + $timezones->america()->paramaribo(); + $timezones->america()->caracas(); + $timezones->america()->resolute(); + $timezones->america()->godthab(); + $timezones->america()->catamarca(); + $timezones->america()->glaceBay(); + $timezones->america()->regina(); + $timezones->america()->toronto(); + $timezones->america()->barbados(); + $timezones->america()->santaIsabel(); + $timezones->america()->miquelon(); + $timezones->america()->havana(); + $timezones->america()->ojinaga(); + $timezones->america()->denver(); + $timezones->america()->cancun(); + $timezones->america()->thunderBay(); + $timezones->america()->adak(); + $timezones->america()->saintJohns(); + $timezones->america()->portAuPrince(); + $timezones->america()->whitehorse(); + $timezones->america()->louisville(); + $timezones->america()->manaus(); + $timezones->america()->lowerPrinces(); + $timezones->america()->sitka(); + $timezones->america()->thule(); + $timezones->america()->campoGrande(); + $timezones->america()->phoenix(); + $timezones->america()->shiprock(); + $timezones->america()->bahia(); + $timezones->america()->tortola(); + $timezones->america()->dawsonCreek(); + $timezones->america()->tijuana(); + $timezones->america()->belize(); + $timezones->america()->atka(); + ``` + +=== "Antartica" + ```php + $timezones->antartica()->davis(); + $timezones->antartica()->palmer(); + $timezones->antartica()->syowa(); + $timezones->antartica()->casey(); + $timezones->antartica()->troll(); + $timezones->antartica()->mcMurdo(); + $timezones->antartica()->vostok(); + $timezones->antartica()->rothera(); + $timezones->antartica()->mawson(); + $timezones->antartica()->macquarie(); + $timezones->antartica()->southPole(); + $timezones->antartica()->dumontDUrville(); + ``` + +=== "Arctic" + ```php + $timezones->arctic()->longyearbyen(); + ``` + +=== "Asia" + ```php + $timezones->asia()->manila(); + $timezones->asia()->baghdad(); + $timezones->asia()->ulaanbaatar(); + $timezones->asia()->almaty(); + $timezones->asia()->samarkand(); + $timezones->asia()->ustNera(); + $timezones->asia()->pontianak(); + $timezones->asia()->tehran(); + $timezones->asia()->saigon(); + $timezones->asia()->krasnoyarsk(); + $timezones->asia()->hebron(); + $timezones->asia()->kuching(); + $timezones->asia()->katmandu(); + $timezones->asia()->shanghai(); + $timezones->asia()->calcutta(); + $timezones->asia()->jayapura(); + $timezones->asia()->muscat(); + $timezones->asia()->omsk(); + $timezones->asia()->aqtau(); + $timezones->asia()->khandyga(); + $timezones->asia()->riyadh(); + $timezones->asia()->bangkok(); + $timezones->asia()->thimphu(); + $timezones->asia()->aden(); + $timezones->asia()->yekaterinburg(); + $timezones->asia()->oral(); + $timezones->asia()->novokuznetsk(); + $timezones->asia()->bishkek(); + $timezones->asia()->macau(); + $timezones->asia()->qyzylorda(); + $timezones->asia()->seoul(); + $timezones->asia()->irkutsk(); + $timezones->asia()->aqtobe(); + $timezones->asia()->chongqing(); + $timezones->asia()->kabul(); + $timezones->asia()->thimbu(); + $timezones->asia()->karachi(); + $timezones->asia()->jakarta(); + $timezones->asia()->harbin(); + $timezones->asia()->novosibirsk(); + $timezones->asia()->dili(); + $timezones->asia()->colombo(); + $timezones->asia()->ashkhabad(); + $timezones->asia()->dacca(); + $timezones->asia()->ashgabat(); + $timezones->asia()->ujungPandang(); + $timezones->asia()->qatar(); + $timezones->asia()->tokyo(); + $timezones->asia()->macao(); + $timezones->asia()->tashkent(); + $timezones->asia()->baku(); + $timezones->asia()->pyongyang(); + $timezones->asia()->tbilisi(); + $timezones->asia()->amman(); + $timezones->asia()->vladivostok(); + $timezones->asia()->damascus(); + $timezones->asia()->bahrain(); + $timezones->asia()->vientiane(); + $timezones->asia()->hovd(); + $timezones->asia()->kuwait(); + $timezones->asia()->magadan(); + $timezones->asia()->ulanBator(); + $timezones->asia()->nicosia(); + $timezones->asia()->telAviv(); + $timezones->asia()->choibalsan(); + $timezones->asia()->brunei(); + $timezones->asia()->kualaLumpur(); + $timezones->asia()->kathmandu(); + $timezones->asia()->srednekolymsk(); + $timezones->asia()->dubai(); + $timezones->asia()->yakutsk(); + $timezones->asia()->beirut(); + $timezones->asia()->gaza(); + $timezones->asia()->singapore(); + $timezones->asia()->rangoon(); + $timezones->asia()->sakhalin(); + $timezones->asia()->phnomPenh(); + $timezones->asia()->kamchatka(); + $timezones->asia()->yerevan(); + $timezones->asia()->chungking(); + $timezones->asia()->hoChiMinh(); + $timezones->asia()->chita(); + $timezones->asia()->istanbul(); + $timezones->asia()->hongKong(); + $timezones->asia()->dhaka(); + $timezones->asia()->jerusalem(); + $timezones->asia()->makassar(); + $timezones->asia()->kolkata(); + $timezones->asia()->taipei(); + $timezones->asia()->dushanbe(); + $timezones->asia()->anadyr(); + ``` + +=== "Atlantic" + ```php + $timezones->atlantic()->faroe(); + $timezones->atlantic()->southGeorgia(); + $timezones->atlantic()->capeVerde(); + $timezones->atlantic()->faeroe(); + $timezones->atlantic()->bermuda(); + $timezones->atlantic()->janMayen(); + $timezones->atlantic()->reykjavik(); + $timezones->atlantic()->saintHelena(); + $timezones->atlantic()->canary(); + $timezones->atlantic()->madeira(); + $timezones->atlantic()->azores(); + $timezones->atlantic()->stanley(); + ``` + +=== "Australia" + ```php + $timezones->australia()->lindeman(); + $timezones->australia()->currie(); + $timezones->australia()->victoria(); + $timezones->australia()->adelaide(); + $timezones->australia()->perth(); + $timezones->australia()->brisbane(); + $timezones->australia()->west(); + $timezones->australia()->australianCapitalTerritory(); + $timezones->australia()->north(); + $timezones->australia()->eucla(); + $timezones->australia()->lordeHoweIsland(); + $timezones->australia()->newSouthWales(); + $timezones->australia()->queensland(); + $timezones->australia()->south(); + $timezones->australia()->melbourne(); + $timezones->australia()->yancowinna(); + $timezones->australia()->canberra(); + $timezones->australia()->sydney(); + $timezones->australia()->darwin(); + $timezones->australia()->hobart(); + $timezones->australia()->brokenHill(); + $timezones->australia()->tasmania(); + ``` + +=== "Europe" + ```php + $timezones->europe()->uzhgorod(); + $timezones->europe()->riga(); + $timezones->europe()->paris(); + $timezones->europe()->guernsey(); + $timezones->europe()->samara(); + $timezones->europe()->athens(); + $timezones->europe()->tirane(); + $timezones->europe()->london(); + $timezones->europe()->helsinki(); + $timezones->europe()->oslo(); + $timezones->europe()->podgorica(); + $timezones->europe()->minsk(); + $timezones->europe()->monaco(); + $timezones->europe()->lisbon(); + $timezones->europe()->tallinn(); + $timezones->europe()->berlin(); + $timezones->europe()->gibraltar(); + $timezones->europe()->prague(); + $timezones->europe()->stockholm(); + $timezones->europe()->moscow(); + $timezones->europe()->bucharest(); + $timezones->europe()->andorra(); + $timezones->europe()->vilnius(); + $timezones->europe()->rome(); + $timezones->europe()->kiev(); + $timezones->europe()->copenhagen(); + $timezones->europe()->belgrade(); + $timezones->europe()->isleOfMan(); + $timezones->europe()->budapest(); + $timezones->europe()->tiraspol(); + $timezones->europe()->vaduz(); + $timezones->europe()->sarajevo(); + $timezones->europe()->amsterdam(); + $timezones->europe()->mariehamn(); + $timezones->europe()->skopje(); + $timezones->europe()->kaliningrad(); + $timezones->europe()->bratislava(); + $timezones->europe()->sanMarino(); + $timezones->europe()->busingen(); + $timezones->europe()->zaporozhye(); + $timezones->europe()->chisinau(); + $timezones->europe()->brussels(); + $timezones->europe()->luxembourg(); + $timezones->europe()->belfast(); + $timezones->europe()->vienna(); + $timezones->europe()->ljubljana(); + $timezones->europe()->simferopol(); + $timezones->europe()->dublin(); + $timezones->europe()->nicosia(); + $timezones->europe()->zagreb(); + $timezones->europe()->jersey(); + $timezones->europe()->madrid(); + $timezones->europe()->vatican(); + $timezones->europe()->istanbul(); + $timezones->europe()->zurich(); + $timezones->europe()->sofia(); + $timezones->europe()->volgograd(); + $timezones->europe()->malta(); + $timezones->europe()->warsaw(); + ``` + +=== "Indian" + ```php + $timezones->indian()->cocos(); + $timezones->indian()->antananarivo(); + $timezones->indian()->reunion(); + $timezones->indian()->chagos(); + $timezones->indian()->comoro(); + $timezones->indian()->mayotte(); + $timezones->indian()->maldives(); + $timezones->indian()->mauritius(); + $timezones->indian()->mahe(); + $timezones->indian()->kerguelen(); + $timezones->indian()->christmas(); + ``` + +=== "Pacific" + ```php + $timezones->pacific()->kosrae(); + $timezones->pacific()->enderbury(); + $timezones->pacific()->apia(); + $timezones->pacific()->noumea(); + $timezones->pacific()->chatham(); + $timezones->pacific()->wake(); + $timezones->pacific()->wallis(); + $timezones->pacific()->johnston(); + $timezones->pacific()->saipan(); + $timezones->pacific()->tarawa(); + $timezones->pacific()->pitcairn(); + $timezones->pacific()->niue(); + $timezones->pacific()->ponape(); + $timezones->pacific()->guam(); + $timezones->pacific()->auckland(); + $timezones->pacific()->pagoPago(); + $timezones->pacific()->chuuk(); + $timezones->pacific()->kwajalein(); + $timezones->pacific()->fakaofo(); + $timezones->pacific()->majuro(); + $timezones->pacific()->guadalcanal(); + $timezones->pacific()->efate(); + $timezones->pacific()->tongatapu(); + $timezones->pacific()->pohnpei(); + $timezones->pacific()->honolulu(); + $timezones->pacific()->bougainville(); + $timezones->pacific()->galapagos(); + $timezones->pacific()->gambier(); + $timezones->pacific()->palau(); + $timezones->pacific()->midway(); + $timezones->pacific()->marquesas(); + $timezones->pacific()->funafuti(); + $timezones->pacific()->norfolk(); + $timezones->pacific()->portMoresby(); + $timezones->pacific()->tahiti(); + $timezones->pacific()->fiji(); + $timezones->pacific()->kiritimati(); + $timezones->pacific()->truk(); + $timezones->pacific()->easter(); + $timezones->pacific()->rarotonga(); + $timezones->pacific()->yap(); + $timezones->pacific()->nauru(); + ``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..c802b1c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,40 @@ +--- +hide: + - navigation + - toc +--- + +# Welcome to TimeContinnum + +TimeContinuum is a time abstraction library. + +The goal is to provide a safe way to manipulate time in your programs. + +It achieves this via: + +- an expressive object oriented API to avoid _magic strings_ +- a clear separation between what's mutable and immutable + +??? example "Sneak peek" + ```php + use Innmind\TimeContinuum\{ + Clock, + Format, + }; + + $clock = Clock::live(); + $start = $clock->now(); + // do some stuff + $end = $clock->now(); + $took = $end + ->elapsedSince($start) + ->asPeriod(); + + \printf( + 'The script ended at %s and it took %s second(s), %s millisecond(s) %s microsecond(s).', + $end->format(Format::iso8601()), + $took->seconds(), + $took->milliseconds(), + $took->microseconds(), + ); + ``` diff --git a/docs/preface/philosophy.md b/docs/preface/philosophy.md new file mode 100644 index 0000000..203aed5 --- /dev/null +++ b/docs/preface/philosophy.md @@ -0,0 +1,13 @@ +# Philosophy + +TimeContinuum is designed around these objectives: + +- an expressive API through a clear naming +- to handle time with precision +- simplify common use cases + +Time is a tricky business full of gotchas. It's easy to make mistake that go undetected for a while. + +The first step to avoid mistakes is to provide a good naming. Here, each component of time is accessible via expressive methods to give the least possible room to error. + +One of the hardest aspect of time is to distinguish what can change from what can't. Once again naming helps but it's not enough. This package creates a _frontier_ between these two worlds. This is expressed via an `immutable` annotation (understood by [Psalm](https://psalm.dev)) on what cannot change, and the rest may change. diff --git a/docs/preface/terminology.md b/docs/preface/terminology.md new file mode 100644 index 0000000..bcaca6e --- /dev/null +++ b/docs/preface/terminology.md @@ -0,0 +1,72 @@ +# Terminology + +## Clock + +A `Clock` is the root object to access time. Like a real clock, each time you demand the time it will provide a new value. This is part of the world that can change. + +Unlike real clocks, in a program we often have the use case to convert a `string` back to a [time](#point-in-time) representation (1). Clocks provide a method for this. You can think of it as a factory. Even though this is done by a clock this method is determinist, meaning that you'll always have the same result for the same inputs. +{.annotate} + +1. Such as reading a value from a database. + +## Point in time + +As its name suggest it represents a fixed point in time on Earth. It represents time down to the microsecond. It also stores the time offset from [UTC](https://en.wikipedia.org/wiki/UTC%2B00:00). + +These objects are generated by clocks. + +Once an object is created it can no longer change. But you can still create objects relative to it by specifying a [period](#period) and the direction. + +## Timezone + +A timezone represents a time offset from [UTC](https://en.wikipedia.org/wiki/UTC%2B00:00) for a set of cities on Earth (1). +{.annotate} + +1. For example `Europe/Paris`. + +Due to politics a city may change its offset at any time (1). And for economic reasons some countries change their offset each year, a process called "daylight saving time" (2). +{.annotate} + +1. This means some points in time don't exist in certain countries or exist twice. +2. Usually they increase/decrease their offset by 1 or 2 hours. + +Because asking the offset for a timezone will yield different results depending on when you ask for it, it's part of the world that changes. This means that timezones are handled at the [clock](#clock) level. + +## Period + +It represents the time between 2 points in time. It can be defined by a number of: + +- years +- months +- days +- hours +- minutes +- seconds +- milliseconds +- microseconds + +Each of this component has to be a positive integer. It's when you apply it to a point in time that you decide if you want to go forward or back in time. + +Since these objects are not tied to a particular point in time they're immutable. + +## Elapsed period + +It represents the number of microseconds between two points in time. + +Once an object is created it cannot change. + +This is useful to compute the time your program took to accomplish a task. Operation often used when dealing with network I/O to handle timeouts. + +## Format + +A format defines a way to represent a point in time as a `string` (1). +{.annotate} + +1. Usually to store the value to a database or print it in a user interface. + +It's declared as an object to give meaning to the string format it encapsulate. The intent is to declare the string format once in an object in your program and reuse this object everywhere. This allows to attach a _name_ to the format. + +This is an attempt to solve the problem (1) where the same format is duplicated everywhere in a program. +{.annotate} + +1. We can see this practive in a lot of programs. diff --git a/docs/upgrade/v3-to-v4.md b/docs/upgrade/v3-to-v4.md new file mode 100644 index 0000000..b01ce78 --- /dev/null +++ b/docs/upgrade/v3-to-v4.md @@ -0,0 +1,155 @@ +# From v3 to v4 + +All namespaces are relative to `\Innmind\TimeContinuum\`. + +These are the main changes, for an extensive list of changes go to the [changelog](https://github.com/Innmind/TimeContinuum/blob/develop/CHANGELOG.md). + +## Clocks + +### Live clock + +=== "Before" + ```php + $clock = new Earth\Clock; + ``` + +=== "After" + ```php + $clock = Clock::live(); + ``` + +### Logger + +=== "Before" + ```php + $clock = new Logger\Clock( + new Earth\Clock, + /* an instance of Psr\Log\LoggerInterface */ + ); + ``` + +=== "After" + ```php + $clock = Clock::logger( + Clock::live(), + /* an instance of Psr\Log\LoggerInterface */ + ); + ``` + +### Frozen + +=== "Before" + ```php + $clock = new Earth\FrozenClock( + new Earth\PointInTime\PointInTime('some date'), + ); + ``` + +=== "After" + ```php + $clock = Clock::live() + ->at('some date', Format::iso8601()) + ->match( + Clock::frozen(...), + static fn() => throw new \LogicException('Use a valid date'), + ); + ``` + +## Timezones + +=== "Before" + ```php + $clock = new Earth\Clock( + new Earth\Timezone\Europe\Paris, + ); + ``` + +=== "After" + ```php + $clock = Clock::live()->switch( + static fn(Timezones $timezones) => $timezones + ->europe() + ->paris(), + ); + ``` + +## Access point in time offset + +=== "Before" + ```php + $clock->now()->timezone()->hours(); + $clock->now()->timezone()->minutes(); + ``` + +=== "After" + ```php + $clock->now()->offset()->hours(); + $clock->now()->offset()->minutes(); + ``` + +## Modify point in time offset + +=== "Before" + ```php + $clock->now()->changeTimezone(new Earth\Timezone\UTC(2, 0)); + ``` + +=== "After" + ```php + $clock->now()->changeOffset(Offset::plus(2, 0)); + ``` + +## Periods + +=== "Before" + ```php + new Earth\Period\Year(42); + new Earth\Period\Month(42); + new Earth\Period\Day(42); + new Earth\Period\Hour(42); + new Earth\Period\Minute(42); + new Earth\Period\Second(42); + new Earth\Period\Millisecond(42); + ``` + +=== "After" + ```php + Period::year(42); + Period::month(42); + Period::day(42); + Period::hour(42); + Period::minute(42); + Period::second(42); + Period::millisecond(42); + ``` + +## Formats + +=== "Before" + ```php + $clock->now()->format(new Earth\Format\ISO8601); + ``` + +=== "After" + ```php + $clock->now()->format(Format::iso8601()); + ``` + +## Elapsed periods + +=== "Before" + ```php + $elapsed = /* any instance of ElapsedPeriod */ + $milliseconds = $elapsed->milliseconds(); + ``` + +=== "After" + ```php + $elapsed = /* any instance of ElapsedPeriod */ + $period = $elapsed->asPeriod(); + $milliseconds = Period\Value::day->seconds($period->days()); + $milliseconds += Period\Value::hour->seconds($period->hours()); + $milliseconds += Period\Value::minute->seconds($period->minutes()); + $milliseconds *= 1_000; + $milliseconds += $period->milliseconds(); + ``` diff --git a/fixtures/Period.php b/fixtures/Period.php new file mode 100644 index 0000000..f06dbb4 --- /dev/null +++ b/fixtures/Period.php @@ -0,0 +1,65 @@ + + */ + public static function any(): Set + { + return Set::compose( + Model::of(...), + Set::integers()->between(0, 9999), + Set::integers()->between(0, 12), + Set::integers()->between(0, 31), + Set::integers()->between(0, 23), + Set::integers()->between(0, 59), + Set::integers()->between(0, 59), + Set::integers()->between(0, 999), + Set::integers()->between(0, 999), + )->toSet(); + } + + /** + * @return Set + */ + public static function anyNumberOfYear(): Set + { + return Set::integers() + ->between(0, 9999) + ->map(Model::year(...)); + } + + /** + * @return Set + */ + public static function lessThanAYear(): Set + { + return Set::compose( + static function($day, $hour, $minute, $second, $millisecond, $microsecond): Model { + return Model::of( + 0, + 0, + $day, + $hour, + $minute, + $second, + $millisecond, + $microsecond, + ); + }, + Set::integers()->between(0, 364), + Set::integers()->between(0, 23), + Set::integers()->between(0, 59), + Set::integers()->between(0, 59), + Set::integers()->between(0, 999), + Set::integers()->between(0, 999), + )->toSet(); + } +} diff --git a/fixtures/Point.php b/fixtures/Point.php new file mode 100644 index 0000000..1892f22 --- /dev/null +++ b/fixtures/Point.php @@ -0,0 +1,71 @@ + + */ + public static function any(): Set + { + return self::yearRange(0, 9999); + } + + /** + * @param string $point String representation of a date + * + * @return Set + */ + public static function after(string $point): Set + { + $lower = Model::at(new \DateTimeImmutable($point)); + + return self::yearRange($lower->year()->toInt(), 9999) + ->filter(static fn($point) => $point->aheadOf($lower)); + } + + /** + * @param string $point String representation of a date + * @return Set + */ + public static function before(string $point): Set + { + $upper = Model::at(new \DateTimeImmutable($point)); + + return self::yearRange(0, $upper->year()->toInt()) + ->filter(static fn($point) => $upper->aheadOf($point)); + } + + /** + * @return Set + */ + private static function yearRange(int $lowerBound, int $upperBound): Set + { + return Set::compose( + static function( + int|string ...$components, + ): Model { + return Model::at(new \DateTimeImmutable(\sprintf( + '%02s-%02d-%02dT%02d:%02d:%02d.%03d%03d%s%02d:%s', + ...$components, + ))); + }, + Set::integers()->between($lowerBound, $upperBound), + Set::integers()->between(1, 12), + Set::integers()->between(1, 31), + Set::integers()->between(0, 23), + Set::integers()->between(0, 59), + Set::integers()->between(0, 59), + Set::integers()->between(0, 999), + Set::integers()->between(0, 999), + Set::of('-', '+'), + Set::integers()->between(0, 12), + Set::of('00', '15', '30', '45'), + )->toSet(); + } +} diff --git a/fixtures/Timezone.php b/fixtures/Timezone.php new file mode 100644 index 0000000..d2f982e --- /dev/null +++ b/fixtures/Timezone.php @@ -0,0 +1,40 @@ + + */ + public static function any(): Set\Provider + { + return Set::compose( + static fn($offset, $daylight) => static fn(Timezones $timezones) => Model::of( + $offset, + $daylight, + ), + Set::either( + Set::compose( + Offset::plus(...), + Set::integers()->between(0, 14), + Set::of(0, 15, 30, 45), + ), + Set::compose( + Offset::minus(...), + Set::integers()->between(0, 12), + Set::of(0, 15, 30, 45), + ), + ), + Set::of(true, false), + ); + } +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..05ec14f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,104 @@ +site_name: Innmind/TimeContinuum +repo_name: Innmind/TimeContinuum + +nav: + - Home: index.md + - Preface: + - Philosophy: preface/philosophy.md + - Terminology: preface/terminology.md + - Getting started: + - getting-started/index.md + - Clocks: getting-started/clocks.md + - Points in time: getting-started/points-in-time.md + - Elapsed period: getting-started/elapsed-period.md + - Time offsets: getting-started/time-offsets.md + - Timezones: getting-started/timezones.md + - Formats: getting-started/formats.md + - Periods: getting-started/periods.md + - Upgrade: + - From v3 to v4: upgrade/v3-to-v4.md + +theme: + name: material + logo: assets/logo.svg + favicon: assets/favicon.png + font: false + features: + - content.code.copy + - content.code.annotate + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.expand + - navigation.indexes + - navigation.top + - navigation.footer + - search.suggest + - search.highlight + - content.action.edit + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + primary: blue + accent: deep orange + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + primary: blue + accent: deep orange + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + primary: blue + accent: deep orange + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + extend_pygments_lang: + - name: php + lang: php + options: + startinline: true + - pymdownx.inlinehilite + - pymdownx.snippets + - attr_list + - md_in_html + - pymdownx.superfences + - abbr + - admonition + - pymdownx.details: + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + - footnotes + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +extra_css: + - assets/stylesheets/extra.css + +plugins: + - search + - privacy + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/Innmind/TimeContinuum + - icon: fontawesome/brands/x-twitter + link: https://twitter.com/Baptouuuu diff --git a/proofs/.gitkeep b/proofs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/proofs/clock.php b/proofs/clock.php new file mode 100644 index 0000000..3f33fc0 --- /dev/null +++ b/proofs/clock.php @@ -0,0 +1,261 @@ + Clock::live()->now()), + )), + static function($assert, $point) { + $assert + ->number($point->day()->ofYear()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(365); + $assert + ->number($point->day()->ofMonth()) + ->int() + ->greaterThanOrEqual(1) + ->lessThanOrEqual(31); + $assert + ->number($point->hour()->toInt()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(23); + $assert + ->number($point->millisecond()->toInt()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(999); + $assert + ->number($point->microsecond()->toInt()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(999); + $assert + ->number($point->minute()->toInt()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(59); + $assert + ->number($point->second()->toInt()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(59); + $assert + ->number($point->month()->numberOfDays()) + ->int() + ->greaterThanOrEqual(28) + ->lessThanOrEqual(31); + $assert + ->number($point->year()->numberOfDays()) + ->int() + ->greaterThanOrEqual(365) + ->lessThanOrEqual(366); + }, + ); + + yield proof( + 'Point in times precision is down to the microsecond', + given(PointInTime::any()), + static function($assert, $point) { + $assert->false( + $point->equals( + $point->goBack(Period::microsecond(1)), + ), + ); + $assert->false( + $point->equals( + $point->goForward(Period::microsecond(1)), + ), + ); + + $assert->true( + $point->goForward(Period::microsecond(1))->aheadOf( + $point, + ), + ); + $assert->true( + $point->aheadOf( + $point->goBack(Period::microsecond(1)), + ), + ); + }, + ); + + yield proof( + 'Clock::at() returns nothing for invalid strings', + given( + Set::strings()->unicode(), + Set::of( + Format::cookie(), + Format::iso8601(), + Format::rfc1036(), + Format::rfc1123(), + Format::rfc2822(), + Format::rfc822(), + Format::rfc850(), + Format::rss(), + Format::w3c(), + ), + ), + static fn($assert, $string, $format) => $assert->null( + Clock::live()->at($string, $format)->match( + static fn($point) => $point, + static fn() => null, + ), + ), + ); + + yield proof( + 'Clock::ofFormat()->at()', + given( + PointInTime::any(), + Set::of( + Format::iso8601(), + new class implements Format\Custom { + public function normalize(): Format + { + return Format::iso8601(); + } + }, + ), + ), + static function($assert, $point, $format) { + $parsed = Clock::live() + ->ofFormat($format) + ->at($point->format($format)) + ->match( + static fn($point) => $point->format($format), + static fn() => null, + ); + + $assert->not()->null($parsed); + $assert->same( + Clock::live() + ->at($point->format($format), $format) + ->match( + static fn($point) => $point->format($format), + static fn() => null, + ), + $parsed, + ); + }, + ); + + yield proof( + 'Each call to Clock::now() is ahead of the previous', + given( + Set::of( + Clock::live(), + Clock::via( // to make sure the now is not memoized + static fn() => Clock::live()->now(), + ), + ), + Set::integers() + ->between(1, 2_000_000) // up to 2 seconds + ->nullable(), + ), + static function($assert, $clock, $microsecond) { + $start = $clock->now(); + + if (\is_int($microsecond)) { + \usleep($microsecond); + } + + $assert->true( + $clock->now()->aheadOf( + $start, + ), + ); + }, + ); + + yield test( + 'Clock::now() is equal to iteself', + static function($assert) { + $clock = Clock::live(); + $now = $clock->now(); + + $assert->true($now->equals($now)); + }, + ); + + yield test( + 'Clock::via()->switch()->now() applies the offset at the provided now date', + static function($assert) { + $clock = Clock::via( + static fn() => Point::at( + new DateTimeImmutable('2025-11-01 12:00:00'), + ), + )->switch(static fn($timezones) => $timezones->europe()->paris()); + + $assert->same( + '+01:00', + $clock->now()->offset()->toString(), + ); + + $clock = Clock::via( + static fn() => Point::at( + new DateTimeImmutable('2025-10-25 12:00:00'), + ), + )->switch(static fn($timezones) => $timezones->europe()->paris()); + + $assert->same( + '+02:00', + $clock->now()->offset()->toString(), + ); + }, + ); + + yield test( + 'Clock::frozen()->switch()->now() applies the offset at the provided now date', + static function($assert) { + $clock = Clock::frozen( + Point::at( + new DateTimeImmutable('2025-11-01 12:00:00'), + ), + )->switch(static fn($timezones) => $timezones->europe()->paris()); + + $assert->same( + '+01:00', + $clock->now()->offset()->toString(), + ); + + $clock = Clock::frozen( + Point::at( + new DateTimeImmutable('2025-10-25 12:00:00'), + ), + )->switch(static fn($timezones) => $timezones->europe()->paris()); + + $assert->same( + '+02:00', + $clock->now()->offset()->toString(), + ); + }, + ); + + yield proof( + 'Timezone fixtures', + given(Timezone::any()), + static function($assert, $timezone) { + $clock = Clock::live()->switch($timezone); + + $assert->string($clock->now()->toString()); + }, + ); +}; diff --git a/proofs/elapsedPeriod.php b/proofs/elapsedPeriod.php new file mode 100644 index 0000000..47ae6ed --- /dev/null +++ b/proofs/elapsedPeriod.php @@ -0,0 +1,174 @@ +above(0), + Set::integers()->above(0), + Set::integers()->between(0, 999_999_999), + Set::integers()->between(0, 999_999_999), + )->filter(static fn($start, $end) => $end > $start), + static function( + $assert, + $startSeconds, + $endSeconds, + $startNanoseconds, + $endNanoseconds, + ) { + $start = HighResolution::of($startSeconds, $startNanoseconds); + $end = HighResolution::of($endSeconds, $endNanoseconds); + + $assert->true( + $start + ->elapsedSince($start) + ->equals( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + $assert->true( + $end + ->elapsedSince($start) + ->longerThan( + Period::microsecond(1)->asElapsedPeriod(), + ), + ); + }, + ); + + yield proof( + 'High resolution elapsed period within same second', + given( + Set::integers()->between(0, 999_999_999), + Set::integers()->between(0, 999_999_999), + )->filter(static fn($start, $end) => $end > $start && ($end - $start) > 1_000), + static function( + $assert, + $startNanoseconds, + $endNanoseconds, + ) { + $start = HighResolution::of(0, $startNanoseconds); + $end = HighResolution::of(0, $endNanoseconds); + + $assert->true( + $start + ->elapsedSince($start) + ->equals( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + $assert->true( + $end + ->elapsedSince($start) + ->longerThan( + Period::microsecond(1)->asElapsedPeriod(), + ), + ); + }, + ); + + yield proof( + 'Elapsed period', + given( + Fixtures\Point::any(), + Set::integers()->above(1), + ), + static function($assert, $start, $microsecond) { + $assert->true( + $start + ->elapsedSince($start) + ->equals( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + $assert->true( + $start + ->goForward(Period::microsecond($microsecond)) + ->elapsedSince($start) + ->equals( + Period::microsecond($microsecond)->asElapsedPeriod(), + ), + ); + $assert->true( + $start + ->goForward(Period::microsecond($microsecond)) + ->elapsedSince($start) + ->longerThan( + Period::microsecond(0)->asElapsedPeriod(), + ), + ); + }, + ); + + yield proof( + 'Negative elapsed periods throws', + given( + Fixtures\Point::any(), + Set::integers()->above(1), + ), + static function($assert, $start, $microsecond) { + $assert->throws( + static fn() => $start + ->goBack(Period::microsecond($microsecond)) + ->elapsedSince($start), + ); + }, + ); + + yield proof( + 'Negative high resolution elapsed periods throws', + given( + Set::integers()->above(0), + Set::integers()->above(0), + Set::integers()->between(0, 999_999_999), + Set::integers()->between(0, 999_999_999), + )->filter(static fn($start, $end) => $end > $start), + static function( + $assert, + $startSeconds, + $endSeconds, + $startNanoseconds, + $endNanoseconds, + ) { + $start = HighResolution::of($startSeconds, $startNanoseconds); + $end = HighResolution::of($endSeconds, $endNanoseconds); + + $assert->throws( + static fn() => $start->elapsedSince($end), + ); + }, + ); + + yield test( + 'Regression elapsed period', + static function($assert) { + $before = ElapsedPeriod::of(1, 987, 564); + $threshold = ElapsedPeriod::of(2, 0, 0); + $after = ElapsedPeriod::of(2, 40, 375); + + $assert->false($before->longerThan($threshold)); + $assert->true($after->longerThan($threshold)); + + $before = ElapsedPeriod::of(1, 0, 0); + $threshold = ElapsedPeriod::of(2, 0, 0); + $after = ElapsedPeriod::of(2, 0, 1); + + $assert->false($before->longerThan($threshold)); + $assert->true($after->longerThan($threshold)); + + $threshold = ElapsedPeriod::of(2, 2, 0); + $after = ElapsedPeriod::of(2, 1, 1); + + $assert->false($after->longerThan($threshold)); + }, + ); +}; diff --git a/proofs/move/endOfDay.php b/proofs/move/endOfDay.php new file mode 100644 index 0000000..c12618d --- /dev/null +++ b/proofs/move/endOfDay.php @@ -0,0 +1,55 @@ + Clock::live()->now()), + )), + static function($assert, $point) { + $endOfDay = (new EndOfDay)($point); + + $assert->same( + $point->year()->toInt(), + $endOfDay->year()->toInt(), + ); + $assert->same( + $point->month()->ofYear(), + $endOfDay->month()->ofYear(), + ); + $assert->same( + $point->day()->ofMonth(), + $endOfDay->day()->ofMonth(), + ); + $assert->same( + 23, + $endOfDay->hour()->toInt(), + ); + $assert->same( + 59, + $endOfDay->minute()->toInt(), + ); + $assert->same( + 59, + $endOfDay->second()->toInt(), + ); + $assert->same( + 999, + $endOfDay->millisecond()->toInt(), + ); + $assert->same( + 999, + $endOfDay->microsecond()->toInt(), + ); + }, + ); +}; diff --git a/proofs/move/endOfMonth.php b/proofs/move/endOfMonth.php new file mode 100644 index 0000000..6181efd --- /dev/null +++ b/proofs/move/endOfMonth.php @@ -0,0 +1,108 @@ + Clock::live()->now()), + )), + static function($assert, $point) { + $endOfMonth = (new EndOfMonth)($point); + + $assert->same( + $point->year()->toInt(), + $endOfMonth->year()->toInt(), + ); + $assert->same( + $point->month()->ofYear(), + $endOfMonth->month()->ofYear(), + ); + $assert->same( + $point->month()->numberOfDays(), + $endOfMonth->day()->ofMonth(), + ); + $assert->same( + 23, + $endOfMonth->hour()->toInt(), + ); + $assert->same( + 59, + $endOfMonth->minute()->toInt(), + ); + $assert->same( + 59, + $endOfMonth->second()->toInt(), + ); + $assert->same( + 999, + $endOfMonth->millisecond()->toInt(), + ); + $assert->same( + 999, + $endOfMonth->microsecond()->toInt(), + ); + }, + ); + + yield test( + 'End of month regression', + static function($assert) { + $point = Clock::live() + ->at( + '0100-02-27T18:35:40.134853+01:00', + Format::of('Y-m-d\TH:i:s.uP'), + ) + ->match( + static fn($point) => $point, + static fn() => null, + ); + + $assert->object($point); + + $endOfMonth = (new EndOfMonth)($point); + + $assert->same( + $point->year()->toInt(), + $endOfMonth->year()->toInt(), + ); + $assert->same( + $point->month()->ofYear(), + $endOfMonth->month()->ofYear(), + ); + $assert->same( + $point->month()->numberOfDays(), + $endOfMonth->day()->ofMonth(), + ); + $assert->same( + 23, + $endOfMonth->hour()->toInt(), + ); + $assert->same( + 59, + $endOfMonth->minute()->toInt(), + ); + $assert->same( + 59, + $endOfMonth->second()->toInt(), + ); + $assert->same( + 999, + $endOfMonth->millisecond()->toInt(), + ); + $assert->same( + 999, + $endOfMonth->microsecond()->toInt(), + ); + }, + ); +}; diff --git a/proofs/move/endOfYear.php b/proofs/move/endOfYear.php new file mode 100644 index 0000000..352ec35 --- /dev/null +++ b/proofs/move/endOfYear.php @@ -0,0 +1,55 @@ + Clock::live()->now()), + )), + static function($assert, $point) { + $endOfYear = (new EndOfYear)($point); + + $assert->same( + $point->year()->toInt(), + $endOfYear->year()->toInt(), + ); + $assert->same( + 12, + $endOfYear->month()->ofYear()->toInt(), + ); + $assert->same( + 31, + $endOfYear->day()->ofMonth(), + ); + $assert->same( + 23, + $endOfYear->hour()->toInt(), + ); + $assert->same( + 59, + $endOfYear->minute()->toInt(), + ); + $assert->same( + 59, + $endOfYear->second()->toInt(), + ); + $assert->same( + 999, + $endOfYear->millisecond()->toInt(), + ); + $assert->same( + 999, + $endOfYear->microsecond()->toInt(), + ); + }, + ); +}; diff --git a/proofs/move/startOfDay.php b/proofs/move/startOfDay.php new file mode 100644 index 0000000..17245e9 --- /dev/null +++ b/proofs/move/startOfDay.php @@ -0,0 +1,55 @@ + Clock::live()->now()), + )), + static function($assert, $point) { + $startOfDay = (new StartOfDay)($point); + + $assert->same( + $point->year()->toInt(), + $startOfDay->year()->toInt(), + ); + $assert->same( + $point->month()->ofYear(), + $startOfDay->month()->ofYear(), + ); + $assert->same( + $point->day()->ofMonth(), + $startOfDay->day()->ofMonth(), + ); + $assert->same( + 0, + $startOfDay->hour()->toInt(), + ); + $assert->same( + 0, + $startOfDay->minute()->toInt(), + ); + $assert->same( + 0, + $startOfDay->second()->toInt(), + ); + $assert->same( + 0, + $startOfDay->millisecond()->toInt(), + ); + $assert->same( + 0, + $startOfDay->microsecond()->toInt(), + ); + }, + ); +}; diff --git a/proofs/move/startOfMonth.php b/proofs/move/startOfMonth.php new file mode 100644 index 0000000..c693212 --- /dev/null +++ b/proofs/move/startOfMonth.php @@ -0,0 +1,55 @@ + Clock::live()->now()), + )), + static function($assert, $point) { + $startOfMonth = (new StartOfMonth)($point); + + $assert->same( + $point->year()->toInt(), + $startOfMonth->year()->toInt(), + ); + $assert->same( + $point->month()->ofYear(), + $startOfMonth->month()->ofYear(), + ); + $assert->same( + 1, + $startOfMonth->day()->ofMonth(), + ); + $assert->same( + 0, + $startOfMonth->hour()->toInt(), + ); + $assert->same( + 0, + $startOfMonth->minute()->toInt(), + ); + $assert->same( + 0, + $startOfMonth->second()->toInt(), + ); + $assert->same( + 0, + $startOfMonth->millisecond()->toInt(), + ); + $assert->same( + 0, + $startOfMonth->microsecond()->toInt(), + ); + }, + ); +}; diff --git a/proofs/move/startOfYear.php b/proofs/move/startOfYear.php new file mode 100644 index 0000000..4242729 --- /dev/null +++ b/proofs/move/startOfYear.php @@ -0,0 +1,55 @@ + Clock::live()->now()), + )), + static function($assert, $point) { + $startOfYear = (new StartOfYear)($point); + + $assert->same( + $point->year()->toInt(), + $startOfYear->year()->toInt(), + ); + $assert->same( + 1, + $startOfYear->month()->ofYear()->toInt(), + ); + $assert->same( + 1, + $startOfYear->day()->ofMonth(), + ); + $assert->same( + 0, + $startOfYear->hour()->toInt(), + ); + $assert->same( + 0, + $startOfYear->minute()->toInt(), + ); + $assert->same( + 0, + $startOfYear->second()->toInt(), + ); + $assert->same( + 0, + $startOfYear->millisecond()->toInt(), + ); + $assert->same( + 0, + $startOfYear->microsecond()->toInt(), + ); + }, + ); +}; diff --git a/proofs/period.php b/proofs/period.php new file mode 100644 index 0000000..964a682 --- /dev/null +++ b/proofs/period.php @@ -0,0 +1,71 @@ +between(0, 10_000), // year + Set::integers()->between(0, 10_000), // month + Set::integers()->between(0, 100_000), // day + Set::integers()->between(0, 1_000_000), // hour + Set::integers()->between(0, 1_000_000_000), // minute + Set::integers()->above(0), // second + Set::integers()->above(0), // millisecond + Set::integers()->above(0), // microsecond + ); + + yield proof( + 'Periods components are always within bounds', + given(Set::either( + $period, + Set::compose( + static fn($p1, $p2) => $p1->add($p2), + $period, + $period, + ), + )), + static function($assert, $period) { + $assert + ->number($period->years()) + ->int() + ->greaterThanOrEqual(0); + $assert + ->number($period->months()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(11); + $assert + ->number($period->days()) + ->int() + ->greaterThanOrEqual(0); + $assert + ->number($period->hours()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(23); + $assert + ->number($period->minutes()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(59); + $assert + ->number($period->seconds()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(59); + $assert + ->number($period->milliseconds()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(999); + $assert + ->number($period->microseconds()) + ->int() + ->greaterThanOrEqual(0) + ->lessThanOrEqual(999); + }, + ); +}; diff --git a/proofs/point.php b/proofs/point.php new file mode 100644 index 0000000..1c75707 --- /dev/null +++ b/proofs/point.php @@ -0,0 +1,65 @@ +above(0)->map(Period::microsecond(...)), + Set::integers()->above(0)->map(Period::millisecond(...)), + ), + ), + static function($assert, $point, $period) { + $assert->true( + $point + ->goForward($period) + ->goBack($period) + ->equals($point), + ); + $assert->true( + $point + ->goBack($period) + ->goForward($period) + ->equals($point), + ); + $assert->false( + $point + ->goBack($period) + ->equals($point), + ); + $assert->false( + $point + ->goForward($period) + ->equals($point), + ); + }, + ); + + yield proof( + 'Point::aheadOf()', + given( + Fixtures\Point::any(), + Fixtures\Period::any()->exclude( + static fn($period) => $period->equals(Period::microsecond(0)), + ), + ), + static function($assert, $point, $period) { + $assert->true( + $point + ->goForward($period) + ->aheadOf($point), + ); + $assert->false( + $point + ->goBack($period) + ->aheadOf($point), + ); + }, + ); +}; diff --git a/proofs/point/highResolution.php b/proofs/point/highResolution.php new file mode 100644 index 0000000..7180f0a --- /dev/null +++ b/proofs/point/highResolution.php @@ -0,0 +1,40 @@ +above(0), + Set::integers()->above(0), + Set::integers()->between(0, 999_999_999), + Set::integers()->between(0, 999_999_999), + )->filter(static fn($start, $end) => $start < $end), + static function($assert, $start, $end, $startNanoseconds, $endNanoseconds) { + $start = HighResolution::of($start, $startNanoseconds); + $end = HighResolution::of($end, $endNanoseconds); + + $assert->true($end->aheadOf($start)); + $assert->false($start->aheadOf($end)); + }, + ); + + yield proof( + 'HighResolution::aheadOf() in same second', + given( + Set::integers()->above(0), + Set::integers()->between(0, 999_999_999), + Set::integers()->between(0, 999_999_999), + )->filter(static fn($_, $start, $end) => $start < $end), + static function($assert, $second, $start, $end) { + $start = HighResolution::of($second, $start); + $end = HighResolution::of($second, $end); + + $assert->true($end->aheadOf($start)); + $assert->false($start->aheadOf($end)); + }, + ); +}; diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Calendar/Day.php b/src/Calendar/Day.php new file mode 100644 index 0000000..d1e1da3 --- /dev/null +++ b/src/Calendar/Day.php @@ -0,0 +1,53 @@ + self::monday, + 2 => self::tuesday, + 3 => self::wednesday, + 4 => self::thursday, + 5 => self::friday, + 6 => self::saturday, + 0 => self::sunday, + }; + } + + /** + * @return int<0, 6> + */ + #[\NoDiscard] + public function toInt(): int + { + return match ($this) { + self::monday => 1, + self::tuesday => 2, + self::wednesday => 3, + self::thursday => 4, + self::friday => 5, + self::saturday => 6, + self::sunday => 0, + }; + } +} diff --git a/src/Calendar/Month.php b/src/Calendar/Month.php new file mode 100644 index 0000000..d802d65 --- /dev/null +++ b/src/Calendar/Month.php @@ -0,0 +1,68 @@ + self::january, + 2 => self::february, + 3 => self::march, + 4 => self::april, + 5 => self::may, + 6 => self::june, + 7 => self::july, + 8 => self::august, + 9 => self::september, + 10 => self::october, + 11 => self::november, + 12 => self::december, + }; + } + + /** + * @return int<1, 12> + */ + #[\NoDiscard] + public function toInt(): int + { + return match ($this) { + self::january => 1, + self::february => 2, + self::march => 3, + self::april => 4, + self::may => 5, + self::june => 6, + self::july => 7, + self::august => 8, + self::september => 9, + self::october => 10, + self::november => 11, + self::december => 12, + }; + } +} diff --git a/src/Clock.php b/src/Clock.php new file mode 100644 index 0000000..65526bd --- /dev/null +++ b/src/Clock.php @@ -0,0 +1,104 @@ + $point, + $point->offset(), + )); + } + + #[\NoDiscard] + public static function logger(self $clock, LoggerInterface $logger): self + { + return new self(new Logger( + $clock->implementation, + $logger, + )); + } + + /** + * @internal + * + * @param callable(): Point $now + */ + #[\NoDiscard] + public static function via(callable $now): self + { + return new self(new Via( + \Closure::fromCallable($now), + Offset::utc(), + )); + } + + #[\NoDiscard] + public function now(): Point + { + return $this->implementation->now(); + } + + /** + * @param callable(Timezones): Timezone $changeTimezone + */ + #[\NoDiscard] + public function switch(callable $changeTimezone): self + { + return new self($this->implementation->switch($changeTimezone)); + } + + /** + * @psalm-mutation-free + * + * @param non-empty-string $date + * + * @return Attempt + */ + #[\NoDiscard] + public function at(string $date, Format|Format\Custom $format): Attempt + { + if ($format instanceof Format\Custom) { + $format = $format->normalize(); + } + + return $this->implementation->at($date, $format); + } + + /** + * @psalm-mutation-free + */ + #[\NoDiscard] + public function ofFormat(Format|Format\Custom $format): OfFormat + { + if ($format instanceof Format\Custom) { + $format = $format->normalize(); + } + + return OfFormat::new($this, $format); + } +} diff --git a/src/Clock/Implementation.php b/src/Clock/Implementation.php new file mode 100644 index 0000000..5abb745 --- /dev/null +++ b/src/Clock/Implementation.php @@ -0,0 +1,37 @@ + + */ + #[\NoDiscard] + public function at(string $date, Format $format): Attempt; +} diff --git a/src/Clock/Logger.php b/src/Clock/Logger.php new file mode 100644 index 0000000..096e245 --- /dev/null +++ b/src/Clock/Logger.php @@ -0,0 +1,79 @@ +clock->switch($changeTimezone), + $this->logger, + ); + } + + #[\Override] + public function now(): Point + { + $now = $this->clock->now(); + $this->logger->debug('Current time is {point}', [ + 'point' => $now->format(Format::iso8601()), + ]); + + return $now; + } + + /** + * @psalm-mutation-free + * + * @param non-empty-string $date + * + * @return Attempt + */ + #[\Override] + public function at(string $date, Format $format): Attempt + { + return $this + ->clock + ->at($date, $format) + ->map(fn($point) => $this->log($point, $date, $format)); + } + + #[\NoDiscard] + private function log( + Point $point, + string $date, + Format $format, + ): Point { + /** + * @psalm-suppress ImpureVariable + * @psalm-suppress ImpurePropertyFetch + * @psalm-suppress ImpureMethodCall + */ + $this->logger->debug('Asked time {date} ({format}) resolved to {point}', [ + 'date' => $date, + 'format' => $format->toString(), + 'point' => $point->format(Format::iso8601()), + ]); + + return $point; + } +} diff --git a/src/Clock/OfFormat.php b/src/Clock/OfFormat.php new file mode 100644 index 0000000..ed53199 --- /dev/null +++ b/src/Clock/OfFormat.php @@ -0,0 +1,44 @@ + + */ + #[\NoDiscard] + public function at(string $date): Attempt + { + return $this->clock->at($date, $this->format); + } +} diff --git a/src/Clock/Via.php b/src/Clock/Via.php new file mode 100644 index 0000000..973b100 --- /dev/null +++ b/src/Clock/Via.php @@ -0,0 +1,99 @@ +now(); + /** @var callable(non-empty-string): Timezone */ + $of = static function(string $zone) use ($now): Timezone { + /** @var non-empty-string $zone */ + $now = (new \DateTimeImmutable($now->format(Format::iso8601())))->setTimezone(new \DateTimeZone($zone)); + + return Timezone::of( + Offset::from($now->format('P')), + (bool) (int) $now->format('I'), + ); + }; + + $offset = $changeTimezone(Timezones::new($of))->offset(); + + return new self( + $this->now, + $offset, + ); + } + + #[\Override] + public function now(): Point + { + return ($this->now)()->changeOffset($this->offset); + } + + /** + * @psalm-mutation-free + * + * @param non-empty-string $date + * + * @return Attempt + */ + #[\Override] + public function at(string $date, Format $format): Attempt + { + try { + $datetime = \DateTimeImmutable::createFromFormat($format->toString(), $date); + } catch (\Throwable $e) { + /** @var Attempt */ + return Attempt::error($e); + } + + if ($datetime === false) { + $lastErrors = \DateTimeImmutable::getLastErrors(); + $warnings = \implode(', ', \array_values($lastErrors['warnings'] ?? [])); + $errors = \implode(', ', \array_values($lastErrors['errors'] ?? [])); + $message = << */ + return Attempt::error(new \RuntimeException($message)); + } + + if ($datetime->format($format->toString()) !== $date) { + /** @var Attempt */ + return Attempt::error(new \RuntimeException(\sprintf( + 'Date "%s" is not exactly of the expected format "%s"', + $date, + $format->toString(), + ))); + } + + return Attempt::result(Point::at($datetime)->changeOffset($this->offset)); + } +} diff --git a/src/ElapsedPeriod.php b/src/ElapsedPeriod.php new file mode 100644 index 0000000..843c68e --- /dev/null +++ b/src/ElapsedPeriod.php @@ -0,0 +1,77 @@ + $seconds + * @param int<0, 999> $milliseconds + * @param int<0, 999> $microseconds + */ + private function __construct( + private int $seconds, + private int $milliseconds, + private int $microseconds, + ) { + } + + /** + * @psalm-pure + * @internal + * + * @param int<0, max> $seconds + * @param int<0, 999> $milliseconds + * @param int<0, 999> $microseconds + */ + #[\NoDiscard] + public static function of( + int $seconds, + int $milliseconds, + int $microseconds, + ): self { + return new self($seconds, $milliseconds, $microseconds); + } + + #[\NoDiscard] + public function longerThan(self $period): bool + { + if ($this->seconds > $period->seconds) { + return true; + } + + if ($period->seconds > $this->seconds) { + return false; + } + + if ($this->milliseconds > $period->milliseconds) { + return true; + } + + if ($period->milliseconds > $this->milliseconds) { + return false; + } + + return $this->microseconds > $period->microseconds; + } + + #[\NoDiscard] + public function equals(self $period): bool + { + return $this->seconds === $period->seconds && + $this->milliseconds === $period->milliseconds && + $this->microseconds === $period->microseconds; + } + + #[\NoDiscard] + public function asPeriod(): Period + { + return Period::second($this->seconds) + ->add(Period::millisecond($this->milliseconds)) + ->add(Period::microsecond($this->microseconds)); + } +} diff --git a/src/Format.php b/src/Format.php new file mode 100644 index 0000000..cf834ad --- /dev/null +++ b/src/Format.php @@ -0,0 +1,119 @@ +value; + } +} diff --git a/src/Format/Custom.php b/src/Format/Custom.php new file mode 100644 index 0000000..76eb0e4 --- /dev/null +++ b/src/Format/Custom.php @@ -0,0 +1,15 @@ +goForward( + Period::hour(23 - $point->hour()->toInt()) + ->add(Period::minute(59 - $point->minute()->toInt())) + ->add(Period::second(59 - $point->second()->toInt())) + ->add(Period::millisecond(999 - $point->millisecond()->toInt())) + ->add(Period::microsecond(999 - $point->microsecond()->toInt())), + ); + } +} diff --git a/src/Move/EndOfMonth.php b/src/Move/EndOfMonth.php new file mode 100644 index 0000000..c5f9c0e --- /dev/null +++ b/src/Move/EndOfMonth.php @@ -0,0 +1,24 @@ +goForward( + Period::day($point->month()->numberOfDays() - $point->day()->ofMonth()), + ); + } +} diff --git a/src/Move/EndOfYear.php b/src/Move/EndOfYear.php new file mode 100644 index 0000000..76c3299 --- /dev/null +++ b/src/Move/EndOfYear.php @@ -0,0 +1,25 @@ +goForward( + Period::month(12 - $point->month()->ofYear()->toInt()), + ); + + return (new EndOfMonth)($point); + } +} diff --git a/src/Move/Month.php b/src/Move/Month.php new file mode 100644 index 0000000..159a5b4 --- /dev/null +++ b/src/Move/Month.php @@ -0,0 +1,67 @@ +{$this->direction}($this->months); + $newPoint = $newPoint->goForward( + Period::hour($point->hour()->toInt()) + ->add(Period::minute($point->minute()->toInt())) + ->add(Period::second($point->second()->toInt())) + ->add(Period::millisecond($point->millisecond()->toInt())) + ->add(Period::microsecond($point->microsecond()->toInt())), + ); + + if ($newPoint->month()->numberOfDays() < $point->day()->ofMonth()) { + return $newPoint->goForward( + Period::day($newPoint->month()->numberOfDays() - 1), + ); + } + + return $newPoint->goForward( + Period::day($point->day()->ofMonth() - 1), + ); + } + + /** + * @psalm-pure + * + * @param int<1, max> $months + */ + #[\NoDiscard] + public static function forward(int $months): self + { + return new self('goForward', Period::month($months)); + } + + /** + * @psalm-pure + * + * @param int<1, max> $months + */ + #[\NoDiscard] + public static function backward(int $months): self + { + return new self('goBack', Period::month($months)); + } +} diff --git a/src/Move/StartOfDay.php b/src/Move/StartOfDay.php new file mode 100644 index 0000000..3c5245c --- /dev/null +++ b/src/Move/StartOfDay.php @@ -0,0 +1,27 @@ +goBack( + Period::hour($point->hour()->toInt()) + ->add(Period::minute($point->minute()->toInt())) + ->add(Period::second($point->second()->toInt())) + ->add(Period::millisecond($point->millisecond()->toInt())) + ->add(Period::microsecond($point->microsecond()->toInt())), + ); + } +} diff --git a/src/Move/StartOfMonth.php b/src/Move/StartOfMonth.php new file mode 100644 index 0000000..b57bd42 --- /dev/null +++ b/src/Move/StartOfMonth.php @@ -0,0 +1,23 @@ +goBack( + Period::day($point->day()->ofMonth() - 1), + ); + } +} diff --git a/src/Move/StartOfYear.php b/src/Move/StartOfYear.php new file mode 100644 index 0000000..3adecc7 --- /dev/null +++ b/src/Move/StartOfYear.php @@ -0,0 +1,23 @@ +goBack( + Period::month($point->month()->ofYear()->toInt() - 1), + ); + } +} diff --git a/src/Offset.php b/src/Offset.php new file mode 100644 index 0000000..0805ddf --- /dev/null +++ b/src/Offset.php @@ -0,0 +1,108 @@ + $hours + * @param int<0, 59> $minutes + */ + private function __construct( + private int $hours, + private int $minutes, + private bool $plus, + ) { + } + + /** + * @psalm-pure + */ + #[\NoDiscard] + public static function utc(): self + { + return self::plus(0, 0); + } + + /** + * @psalm-pure + * + * @param int<0, 14> $hours + * @param int<0, 59> $minutes + */ + #[\NoDiscard] + public static function plus(int $hours, int $minutes = 0): self + { + return new self($hours, $minutes, true); + } + + /** + * @psalm-pure + * + * @param int<0, 12> $hours + * @param int<0, 59> $minutes + */ + #[\NoDiscard] + public static function minus(int $hours, int $minutes = 0): self + { + return new self(-$hours, $minutes, false); + } + + /** + * @psalm-pure + * @internal + */ + #[\NoDiscard] + public static function from(string $string): self + { + [$hours, $minutes] = \explode(':', $string); + + /** @psalm-suppress ArgumentTypeCoercion */ + return new self( + (int) $hours, + (int) $minutes, + \str_starts_with($string, '+'), + ); + } + + /** + * @return int<-12, 14> + */ + #[\NoDiscard] + public function hours(): int + { + return $this->hours; + } + + /** + * @return int<0, 59> + */ + #[\NoDiscard] + public function minutes(): int + { + return $this->minutes; + } + + /** + * @return non-empty-string + */ + #[\NoDiscard] + public function toString(): string + { + if ($this->hours === 0 && $this->minutes === 0) { + return 'Z'; + } + + /** @var non-empty-string */ + return \sprintf( + '%s%02d:%02d', + $this->plus ? '+' : '-', + \abs($this->hours), + $this->minutes, + ); + } +} diff --git a/src/Period.php b/src/Period.php new file mode 100644 index 0000000..e33169a --- /dev/null +++ b/src/Period.php @@ -0,0 +1,493 @@ + $year + * @param int<0, 11> $month + * @param int<0, max> $day + * @param int<0, 23> $hour + * @param int<0, 59> $minute + * @param int<0, 59> $second + * @param int<0, 999> $millisecond + * @param int<0, 999> $microsecond + */ + private function __construct( + private int $year, + private int $month, + private int $day, + private int $hour, + private int $minute, + private int $second, + private int $millisecond, + private int $microsecond, + ) { + } + + /** + * @psalm-pure + * + * @param int<0, max> $year + * @param int<0, 11> $month + * @param int<0, max> $day + * @param int<0, 23> $hour + * @param int<0, 59> $minute + * @param int<0, 59> $second + * @param int<0, 999> $millisecond + * @param int<0, 999> $microsecond + */ + #[\NoDiscard] + public static function of( + int $year, + int $month, + int $day, + int $hour, + int $minute, + int $second, + int $millisecond, + int $microsecond, + ): self { + return new self( + $year, + $month, + $day, + $hour, + $minute, + $second, + $millisecond, + $microsecond, + ); + } + + /** + * @psalm-pure + * + * @param int<0, max> $year + * @param int<0, max> $month + * @param int<0, max> $day + * @param int<0, max> $hour + * @param int<0, max> $minute + * @param int<0, max> $second + * @param int<0, max> $millisecond + * @param int<0, max> $microsecond + */ + #[\NoDiscard] + public static function composite( + int $year, + int $month, + int $day, + int $hour, + int $minute, + int $second, + int $millisecond, + int $microsecond, + ): self { + return self::microsecond($microsecond) + ->add(self::millisecond($millisecond)) + ->add(self::second($second)) + ->add(self::minute($minute)) + ->add(self::hour($hour)) + ->add(self::day($day)) + ->add(self::month($month)) + ->add(self::year($year)); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $year + */ + #[\NoDiscard] + public static function year(int $year): self + { + return new self( + $year, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $month + */ + #[\NoDiscard] + public static function month(int $month): self + { + if ($month < 12) { + return new self(0, $month, 0, 0, 0, 0, 0, 0); + } + + /** @var int<0, max> */ + $year = (int) ($month / 12); + $month = $month % 12; + + return new self( + $year, + $month, + 0, + 0, + 0, + 0, + 0, + 0, + ); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $day + */ + #[\NoDiscard] + public static function day(int $day): self + { + return new self( + 0, + 0, + $day, + 0, + 0, + 0, + 0, + 0, + ); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $hour + */ + #[\NoDiscard] + public static function hour(int $hour): self + { + if ($hour < 24) { + return new self(0, 0, 0, $hour, 0, 0, 0, 0); + } + + /** @var int<0, max> */ + $day = (int) ($hour / 24); + $hour = $hour % 24; + + return new self( + 0, + 0, + $day, + $hour, + 0, + 0, + 0, + 0, + ); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $minute + */ + #[\NoDiscard] + public static function minute(int $minute): self + { + if ($minute < 60) { + return new self(0, 0, 0, 0, $minute, 0, 0, 0); + } + + /** @var int<0, max> */ + $hour = (int) ($minute / 60); + $hour = self::hour($hour); + $minute = $minute % 60; + + return new self( + $hour->years(), + $hour->months(), + $hour->days(), + $hour->hours(), + $minute, + 0, + 0, + 0, + ); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $second + */ + #[\NoDiscard] + public static function second(int $second): self + { + if ($second < 60) { + return new self(0, 0, 0, 0, 0, $second, 0, 0); + } + + /** @var int<0, max> */ + $minute = (int) ($second / 60); + $minute = self::minute($minute); + $second = $second % 60; + + return new self( + $minute->years(), + $minute->months(), + $minute->days(), + $minute->hours(), + $minute->minutes(), + $second, + 0, + 0, + ); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $millisecond + */ + #[\NoDiscard] + public static function millisecond(int $millisecond): self + { + if ($millisecond < 1_000) { + return new self(0, 0, 0, 0, 0, 0, $millisecond, 0); + } + + /** @var int<0, max> */ + $second = (int) ($millisecond / 1000); + $second = self::second($second); + $millisecond = $millisecond % 1000; + + return new self( + $second->years(), + $second->months(), + $second->days(), + $second->hours(), + $second->minutes(), + $second->seconds(), + $millisecond, + 0, + ); + } + + /** + * @psalm-pure + * + * @param 0|positive-int $microsecond + */ + #[\NoDiscard] + public static function microsecond(int $microsecond): self + { + if ($microsecond < 1_000) { + return new self(0, 0, 0, 0, 0, 0, 0, $microsecond); + } + + /** @var int<0, max> */ + $millisecond = (int) ($microsecond / 1000); + $millisecond = self::millisecond($millisecond); + $microsecond = $microsecond % 1000; + + return new self( + $millisecond->years(), + $millisecond->months(), + $millisecond->days(), + $millisecond->hours(), + $millisecond->minutes(), + $millisecond->seconds(), + $millisecond->milliseconds(), + $microsecond, + ); + } + + /** + * @return int<0, max> + */ + #[\NoDiscard] + public function years(): int + { + return $this->year; + } + + /** + * @return int<0, 11> + */ + #[\NoDiscard] + public function months(): int + { + return $this->month; + } + + /** + * @return int<0, max> + */ + #[\NoDiscard] + public function days(): int + { + return $this->day; + } + + /** + * @return int<0, 23> + */ + #[\NoDiscard] + public function hours(): int + { + return $this->hour; + } + + /** + * @return int<0, 59> + */ + #[\NoDiscard] + public function minutes(): int + { + return $this->minute; + } + + /** + * @return int<0, 59> + */ + #[\NoDiscard] + public function seconds(): int + { + return $this->second; + } + + /** + * @return int<0, 999> + */ + #[\NoDiscard] + public function milliseconds(): int + { + return $this->millisecond; + } + + /** + * @return int<0, 999> + */ + #[\NoDiscard] + public function microseconds(): int + { + return $this->microsecond; + } + + #[\NoDiscard] + public function equals(self $period): bool + { + return $this->year === $period->years() && + $this->month === $period->months() && + $this->day === $period->days() && + $this->hour === $period->hours() && + $this->minute === $period->minutes() && + $this->second === $period->seconds() && + $this->millisecond === $period->milliseconds() && + $this->microsecond === $period->microseconds(); + } + + #[\NoDiscard] + public function add(self $period): self + { + $microsecond = self::microsecond($this->microsecond + $period->microseconds()); + $millisecond = self::millisecond( + $this->millisecond + + $period->milliseconds() + + $microsecond->milliseconds(), + ); + $second = self::second( + $this->second + + $period->seconds() + + $millisecond->seconds() + + $microsecond->seconds(), + ); + $minute = self::minute( + $this->minute + + $period->minutes() + + $second->minutes() + + $millisecond->minutes() + + $microsecond->minutes(), + ); + $hour = self::hour( + $this->hour + + $period->hours() + + $minute->hours() + + $second->hours() + + $millisecond->hours() + + $microsecond->hours(), + ); + $day = self::day( + $this->day + + $period->days() + + $hour->days() + + $minute->days() + + $second->days() + + $millisecond->days() + + $microsecond->days(), + ); + $month = self::month( + $this->month + + $period->months() + + $day->months() + + $hour->months() + + $minute->months() + + $second->months() + + $millisecond->months() + + $microsecond->months(), + ); + $year = self::year( + $this->year + + $period->years() + + $month->years() + + $day->years() + + $hour->years() + + $minute->years() + + $second->years() + + $millisecond->years() + + $microsecond->years(), + ); + + return new self( + $year->years(), + $month->months(), + $day->days(), + $hour->hours(), + $minute->minutes(), + $second->seconds(), + $millisecond->milliseconds(), + $microsecond->microseconds(), + ); + } + + /** + * @throws \LogicException When using a period containing months or years + */ + #[\NoDiscard] + public function asElapsedPeriod(): ElapsedPeriod + { + if ($this->months() !== 0 || $this->years() !== 0) { + // a month or a year is not constant + throw new \LogicException('Months and years can not be converted to microseconds'); + } + + $seconds = Period\Value::day->seconds($this->days()) + + Period\Value::hour->seconds($this->hours()) + + Period\Value::minute->seconds($this->minutes()) + + $this->second; + + return ElapsedPeriod::of( + $seconds, + $this->millisecond, + $this->microsecond, + ); + } +} diff --git a/src/Period/Value.php b/src/Period/Value.php new file mode 100644 index 0000000..09ec7ae --- /dev/null +++ b/src/Period/Value.php @@ -0,0 +1,32 @@ + $number + * + * @return int<0, max> + */ + #[\NoDiscard] + public function seconds(int $number): int + { + return match ($this) { + self::minute => $number * 60, + self::hour => $number * self::minute->seconds(60), + self::day => $number * self::hour->seconds(24), + }; + } +} diff --git a/src/Point.php b/src/Point.php new file mode 100644 index 0000000..db85bc3 --- /dev/null +++ b/src/Point.php @@ -0,0 +1,323 @@ +date->format('Y')); + } + + #[\NoDiscard] + public function month(): Month + { + return Month::of($this->date); + } + + #[\NoDiscard] + public function day(): Day + { + return Day::of($this->date); + } + + #[\NoDiscard] + public function hour(): Hour + { + /** @var int<0, 23> */ + $hour = (int) $this->date->format('G'); + + return Hour::of($hour); + } + + #[\NoDiscard] + public function minute(): Minute + { + /** @var int<0, 59> */ + $minute = (int) $this->date->format('i'); + + return Minute::of($minute); + } + + #[\NoDiscard] + public function second(): Second + { + /** @var int<0, 59> */ + $second = (int) $this->date->format('s'); + + return Second::of($second); + } + + #[\NoDiscard] + public function millisecond(): Millisecond + { + /** @var int<0, 999> */ + $millisecond = (int) $this->date->format('v'); + + return Millisecond::of($millisecond); + } + + #[\NoDiscard] + public function microsecond(): Microsecond + { + /** @var int<0, 999> */ + $microsecond = ((int) $this->date->format('u')) % 1000; + + return Microsecond::of($microsecond); + } + + #[\NoDiscard] + public function format(Format|Format\Custom $format): string + { + if ($format instanceof Format\Custom) { + $format = $format->normalize(); + } + + return $this->date->format($format->toString()); + } + + #[\NoDiscard] + public function changeOffset(Offset $offset): self + { + return new self( + $this->date->setTimezone( + new \DateTimeZone($offset->toString()), + ), + $this->highResolution, + ); + } + + #[\NoDiscard] + public function offset(): Offset + { + return Offset::from($this->date->format('P')); + } + + #[\NoDiscard] + public function elapsedSince(self $point): ElapsedPeriod + { + if (!\is_null($this->highResolution) && !\is_null($point->highResolution)) { + return $this->highResolution->elapsedSince($point->highResolution); + } + + $seconds = ((int) $this->date->format('U')) - ((int) $point->date->format('U')); + $milliseconds = $this->millisecond()->toInt() - $point->millisecond()->toInt(); + $microseconds = $this->microsecond()->toInt() - $point->microsecond()->toInt(); + + if ($milliseconds < 0) { + $seconds -= 1; + $milliseconds += 1_000; + } + + if ($microseconds < 0) { + $milliseconds -= 1; + $microseconds += 1_000; + } + + if ($milliseconds < 0) { + // This handles the case where any second diff is positive, but zero + // milliseconds and any microsecond diff. + // Duplication could be avoided by switching the 2 previous if but + // it would require to compute the number of seconds to subtract. + // The duplication seems more obvious to understand (at least for + // now). + $seconds -= 1; + $milliseconds += 1_000; + } + + if ($seconds < 0) { + throw new \RuntimeException(\sprintf( + 'Negative period : %ss, %smillis, %smicros', + $seconds, + $milliseconds, + $microseconds, + )); + } + + return ElapsedPeriod::of( + $seconds, + $milliseconds, + $microseconds, + ); + } + + #[\NoDiscard] + public function goBack(Period $period): self + { + $interval = self::dateInterval($period); + + if (\is_null($interval)) { + return $this; + } + + return new self( + $this->date->sub($interval), + null, + ); + } + + #[\NoDiscard] + public function goForward(Period $period): self + { + $interval = self::dateInterval($period); + + if (\is_null($interval)) { + return $this; + } + + return new self( + $this->date->add($interval), + null, + ); + } + + #[\NoDiscard] + public function equals(self $point): bool + { + $format = Format::of('Y-m-dTH:i:s.u'); + $self = $this->changeOffset(Offset::utc())->format($format); + $other = $point->changeOffset(Offset::utc())->format($format); + + return $self === $other; + } + + #[\NoDiscard] + public function aheadOf(self $point): bool + { + if (!\is_null($this->highResolution) && !\is_null($point->highResolution)) { + return $this->highResolution->aheadOf($point->highResolution); + } + + return $this->date > $point->date; + } + + #[\NoDiscard] + public function toString(): string + { + return $this->date->format('Y-m-d\TH:i:s.uP'); + } + + /** + * @psalm-pure + */ + #[\NoDiscard] + private static function dateInterval(Period $period): ?\DateInterval + { + /** @var list */ + $parts = []; + + if ($period->years() > 0) { + $parts[] = \sprintf( + '%s years', + $period->years(), + ); + } + + if ($period->months() > 0) { + $parts[] = \sprintf( + '%s months', + $period->months(), + ); + } + + if ($period->days() > 0) { + $parts[] = \sprintf( + '%s days', + $period->days(), + ); + } + + if ($period->hours() > 0) { + $parts[] = \sprintf( + '%s hours', + $period->hours(), + ); + } + + if ($period->minutes() > 0) { + $parts[] = \sprintf( + '%s minutes', + $period->minutes(), + ); + } + + if ($period->seconds() > 0) { + $parts[] = \sprintf( + '%s seconds', + $period->seconds(), + ); + } + + if ($period->milliseconds() > 0) { + $parts[] = \sprintf( + '%s milliseconds', + $period->milliseconds(), + ); + } + + if ($period->microseconds() > 0) { + $parts[] = \sprintf( + '%s microseconds', + $period->microseconds(), + ); + } + + if (\count($parts) === 0) { + return null; + } + + /** @psalm-suppress ImpureMethodCall */ + return \DateInterval::createFromDateString(\implode(' + ', $parts)) ?: null; + } +} diff --git a/src/Point/Day.php b/src/Point/Day.php new file mode 100644 index 0000000..9c6d94e --- /dev/null +++ b/src/Point/Day.php @@ -0,0 +1,72 @@ + */ + private int $day; + private Calendar\Day $week; + /** @var int<0, 365> */ + private int $ofYear; + + private function __construct(\DateTimeImmutable $date) + { + /** @var int<1, 31> */ + $day = (int) $date->format('j'); + + $this->day = $day; + $this->week = Calendar\Day::of((int) $date->format('w')); + /** @var int<0, 365> */ + $this->ofYear = (int) $date->format('z'); + } + + /** + * @psalm-pure + * @internal + */ + #[\NoDiscard] + public static function of(\DateTimeImmutable $date): self + { + return new self($date); + } + + #[\NoDiscard] + public function ofWeek(): Calendar\Day + { + return $this->week; + } + + /** + * @return int<0, 365> + */ + #[\NoDiscard] + public function ofYear(): int + { + return $this->ofYear; + } + + /** + * @return 24 + */ + #[\NoDiscard] + public function numberOfHours(): int + { + return 24; + } + + /** + * @return int<1, 31> + */ + #[\NoDiscard] + public function ofMonth(): int + { + return $this->day; + } +} diff --git a/src/Point/HighResolution.php b/src/Point/HighResolution.php new file mode 100644 index 0000000..f7de157 --- /dev/null +++ b/src/Point/HighResolution.php @@ -0,0 +1,96 @@ + $seconds + * @param int<0, 999_999_999> $nanoseconds + */ + private function __construct( + private int $seconds, + private int $nanoseconds, + ) { + } + + /** + * @internal + */ + #[\NoDiscard] + public static function now(): self + { + /** + * @var int<0, max> $seconds + * @var int<0, 999_999_999> $nanoseconds + */ + [$seconds, $nanoseconds] = \hrtime(); + + return new self($seconds, $nanoseconds); + } + + /** + * @internal + * + * @param int<0, max> $seconds + * @param int<0, 999_999_999> $nanoseconds + */ + #[\NoDiscard] + public static function of(int $seconds, int $nanoseconds): self + { + return new self($seconds, $nanoseconds); + } + + #[\NoDiscard] + public function aheadOf(self $other): bool + { + if ($this->seconds > $other->seconds) { + return true; + } + + if ($this->seconds < $other->seconds) { + return false; + } + + return $this->nanoseconds > $other->nanoseconds; + } + + #[\NoDiscard] + public function elapsedSince(self $other): ElapsedPeriod + { + $seconds = $this->seconds - $other->seconds; + $nanoseconds = $this->nanoseconds - $other->nanoseconds; + + if ($nanoseconds < 0) { + $seconds -= 1; + $nanoseconds += 1_000_000_000; + } + + /** @var int<0, 999> */ + $microseconds = ((int) ($nanoseconds / 1_000)) % 1_000; + /** @var int<0, 999> */ + $milliseconds = ((int) ($nanoseconds / 1_000_000)) % 1_000; + + if ($seconds < 0) { + throw new \RuntimeException(\sprintf( + 'Negative period : %ss, %smillis, %smicros', + $seconds, + $milliseconds, + $microseconds, + )); + } + + return ElapsedPeriod::of( + $seconds, + $milliseconds, + $microseconds, + ); + } +} diff --git a/src/Point/Hour.php b/src/Point/Hour.php new file mode 100644 index 0000000..4146438 --- /dev/null +++ b/src/Point/Hour.php @@ -0,0 +1,48 @@ + $hour + */ + private function __construct( + private int $hour, + ) { + } + + /** + * @psalm-pure + * @internal + * + * @param int<0, 23> $hour + */ + #[\NoDiscard] + public static function of(int $hour): self + { + return new self($hour); + } + + /** + * @return 60 + */ + #[\NoDiscard] + public function numberOfMinutes(): int + { + return 60; + } + + /** + * @return int<0, 23> + */ + #[\NoDiscard] + public function toInt(): int + { + return $this->hour; + } +} diff --git a/src/Point/Microsecond.php b/src/Point/Microsecond.php new file mode 100644 index 0000000..2c870a9 --- /dev/null +++ b/src/Point/Microsecond.php @@ -0,0 +1,39 @@ + $microsecond + */ + private function __construct( + private int $microsecond, + ) { + } + + /** + * @psalm-pure + * @internal + * + * @param int<0, 999> $microsecond + */ + #[\NoDiscard] + public static function of(int $microsecond): self + { + return new self($microsecond); + } + + /** + * @return int<0, 999> + */ + #[\NoDiscard] + public function toInt(): int + { + return $this->microsecond; + } +} diff --git a/src/Point/Millisecond.php b/src/Point/Millisecond.php new file mode 100644 index 0000000..c55d357 --- /dev/null +++ b/src/Point/Millisecond.php @@ -0,0 +1,39 @@ + $millisecond + */ + private function __construct( + private int $millisecond, + ) { + } + + /** + * @psalm-pure + * @internal + * + * @param int<0, 999> $millisecond + */ + #[\NoDiscard] + public static function of(int $millisecond): self + { + return new self($millisecond); + } + + /** + * @return int<0, 999> + */ + #[\NoDiscard] + public function toInt(): int + { + return $this->millisecond; + } +} diff --git a/src/Point/Minute.php b/src/Point/Minute.php new file mode 100644 index 0000000..5e631dd --- /dev/null +++ b/src/Point/Minute.php @@ -0,0 +1,48 @@ + $minute + */ + private function __construct( + private int $minute, + ) { + } + + /** + * @psalm-pure + * @internal + * + * @param int<0, 59> $minute + */ + #[\NoDiscard] + public static function of(int $minute): self + { + return new self($minute); + } + + /** + * @return 60 + */ + #[\NoDiscard] + public function numberOfSeconds(): int + { + return 60; + } + + /** + * @return int<0, 59> + */ + #[\NoDiscard] + public function toInt(): int + { + return $this->minute; + } +} diff --git a/src/Point/Month.php b/src/Point/Month.php new file mode 100644 index 0000000..0a2898d --- /dev/null +++ b/src/Point/Month.php @@ -0,0 +1,48 @@ + */ + private int $days; + + private function __construct(\DateTimeImmutable $date) + { + $this->month = Calendar\Month::of((int) $date->format('n')); + /** @var int<28, 31> */ + $this->days = (int) $date->format('t'); + } + + /** + * @psalm-pure + * @internal + */ + #[\NoDiscard] + public static function of(\DateTimeImmutable $date): self + { + return new self($date); + } + + /** + * @return int<28, 31> + */ + #[\NoDiscard] + public function numberOfDays(): int + { + return $this->days; + } + + #[\NoDiscard] + public function ofYear(): Calendar\Month + { + return $this->month; + } +} diff --git a/src/Point/Second.php b/src/Point/Second.php new file mode 100644 index 0000000..6b2a038 --- /dev/null +++ b/src/Point/Second.php @@ -0,0 +1,39 @@ + $second + */ + private function __construct( + private int $second, + ) { + } + + /** + * @psalm-pure + * @internal + * + * @param int<0, 59> $second + */ + #[\NoDiscard] + public static function of(int $second): self + { + return new self($second); + } + + /** + * @return int<0, 59> + */ + #[\NoDiscard] + public function toInt(): int + { + return $this->second; + } +} diff --git a/src/Point/Year.php b/src/Point/Year.php new file mode 100644 index 0000000..aa863ee --- /dev/null +++ b/src/Point/Year.php @@ -0,0 +1,55 @@ +year = $year; + /** @var 365|366 */ + $this->days = ((int) (new \DateTimeImmutable("{$year}-12-31T00:00:00"))->format('z')) + 1; + } + + /** + * @psalm-pure + * @internal + */ + #[\NoDiscard] + public static function of(int $year): self + { + return new self($year); + } + + /** + * @return 365|366 + */ + #[\NoDiscard] + public function numberOfDays(): int + { + return $this->days; + } + + /** + * @return 12 + */ + #[\NoDiscard] + public function numberOfMonths(): int + { + return 12; + } + + #[\NoDiscard] + public function toInt(): int + { + return $this->year; + } +} diff --git a/src/Timezone.php b/src/Timezone.php new file mode 100644 index 0000000..8be5c1c --- /dev/null +++ b/src/Timezone.php @@ -0,0 +1,34 @@ +offset; + } + + #[\NoDiscard] + public function daylightSavingTimeApplied(): bool + { + return $this->dst; + } +} diff --git a/src/Timezone/Africa.php b/src/Timezone/Africa.php new file mode 100644 index 0000000..b6e2f90 --- /dev/null +++ b/src/Timezone/Africa.php @@ -0,0 +1,352 @@ +of)('Africa/Lome'); + } + + #[\NoDiscard] + public function ceuta(): Timezone + { + return ($this->of)('Africa/Ceuta'); + } + + #[\NoDiscard] + public function elAaiun(): Timezone + { + return ($this->of)('Africa/El_Aaiun'); + } + + #[\NoDiscard] + public function portoNovo(): Timezone + { + return ($this->of)('Africa/Porto-Novo'); + } + + #[\NoDiscard] + public function djibouti(): Timezone + { + return ($this->of)('Africa/Djibouti'); + } + + #[\NoDiscard] + public function windhoek(): Timezone + { + return ($this->of)('Africa/Windhoek'); + } + + #[\NoDiscard] + public function algiers(): Timezone + { + return ($this->of)('Africa/Algiers'); + } + + #[\NoDiscard] + public function ouagadougou(): Timezone + { + return ($this->of)('Africa/Ouagadougou'); + } + + #[\NoDiscard] + public function bamako(): Timezone + { + return ($this->of)('Africa/Bamako'); + } + + #[\NoDiscard] + public function harare(): Timezone + { + return ($this->of)('Africa/Harare'); + } + + #[\NoDiscard] + public function bujumbura(): Timezone + { + return ($this->of)('Africa/Bujumbura'); + } + + #[\NoDiscard] + public function douala(): Timezone + { + return ($this->of)('Africa/Douala'); + } + + #[\NoDiscard] + public function brazzaville(): Timezone + { + return ($this->of)('Africa/Brazzaville'); + } + + #[\NoDiscard] + public function tripoli(): Timezone + { + return ($this->of)('Africa/Tripoli'); + } + + #[\NoDiscard] + public function casablanca(): Timezone + { + return ($this->of)('Africa/Casablanca'); + } + + #[\NoDiscard] + public function niamey(): Timezone + { + return ($this->of)('Africa/Niamey'); + } + + #[\NoDiscard] + public function mbabane(): Timezone + { + return ($this->of)('Africa/Mbabane'); + } + + #[\NoDiscard] + public function blantyre(): Timezone + { + return ($this->of)('Africa/Blantyre'); + } + + #[\NoDiscard] + public function conakry(): Timezone + { + return ($this->of)('Africa/Conakry'); + } + + #[\NoDiscard] + public function khartoum(): Timezone + { + return ($this->of)('Africa/Khartoum'); + } + + #[\NoDiscard] + public function luanda(): Timezone + { + return ($this->of)('Africa/Luanda'); + } + + #[\NoDiscard] + public function libreville(): Timezone + { + return ($this->of)('Africa/Libreville'); + } + + #[\NoDiscard] + public function maseru(): Timezone + { + return ($this->of)('Africa/Maseru'); + } + + #[\NoDiscard] + public function lusaka(): Timezone + { + return ($this->of)('Africa/Lusaka'); + } + + #[\NoDiscard] + public function darEsSalaam(): Timezone + { + return ($this->of)('Africa/Dar_es_Salaam'); + } + + #[\NoDiscard] + public function nairobi(): Timezone + { + return ($this->of)('Africa/Nairobi'); + } + + #[\NoDiscard] + public function banjul(): Timezone + { + return ($this->of)('Africa/Banjul'); + } + + #[\NoDiscard] + public function bissau(): Timezone + { + return ($this->of)('Africa/Bissau'); + } + + #[\NoDiscard] + public function nouakchott(): Timezone + { + return ($this->of)('Africa/Nouakchott'); + } + + #[\NoDiscard] + public function johannesburg(): Timezone + { + return ($this->of)('Africa/Johannesburg'); + } + + #[\NoDiscard] + public function timbuktu(): Timezone + { + return ($this->of)('Africa/Timbuktu'); + } + + #[\NoDiscard] + public function saoTome(): Timezone + { + return ($this->of)('Africa/Sao_Tome'); + } + + #[\NoDiscard] + public function freetown(): Timezone + { + return ($this->of)('Africa/Freetown'); + } + + #[\NoDiscard] + public function kampala(): Timezone + { + return ($this->of)('Africa/Kampala'); + } + + #[\NoDiscard] + public function dakar(): Timezone + { + return ($this->of)('Africa/Dakar'); + } + + #[\NoDiscard] + public function lagos(): Timezone + { + return ($this->of)('Africa/Lagos'); + } + + #[\NoDiscard] + public function cairo(): Timezone + { + return ($this->of)('Africa/Cairo'); + } + + #[\NoDiscard] + public function mogadishu(): Timezone + { + return ($this->of)('Africa/Mogadishu'); + } + + #[\NoDiscard] + public function gaborone(): Timezone + { + return ($this->of)('Africa/Gaborone'); + } + + #[\NoDiscard] + public function tunis(): Timezone + { + return ($this->of)('Africa/Tunis'); + } + + #[\NoDiscard] + public function kigali(): Timezone + { + return ($this->of)('Africa/Kigali'); + } + + #[\NoDiscard] + public function malabo(): Timezone + { + return ($this->of)('Africa/Malabo'); + } + + #[\NoDiscard] + public function abidjan(): Timezone + { + return ($this->of)('Africa/Abidjan'); + } + + #[\NoDiscard] + public function accra(): Timezone + { + return ($this->of)('Africa/Accra'); + } + + #[\NoDiscard] + public function asmera(): Timezone + { + return ($this->of)('Africa/Asmera'); + } + + #[\NoDiscard] + public function ndjamena(): Timezone + { + return ($this->of)('Africa/Ndjamena'); + } + + #[\NoDiscard] + public function lubumbashi(): Timezone + { + return ($this->of)('Africa/Lubumbashi'); + } + + #[\NoDiscard] + public function juba(): Timezone + { + return ($this->of)('Africa/Juba'); + } + + #[\NoDiscard] + public function monrovia(): Timezone + { + return ($this->of)('Africa/Monrovia'); + } + + #[\NoDiscard] + public function maputo(): Timezone + { + return ($this->of)('Africa/Maputo'); + } + + #[\NoDiscard] + public function kinshasa(): Timezone + { + return ($this->of)('Africa/Kinshasa'); + } + + #[\NoDiscard] + public function asmara(): Timezone + { + return ($this->of)('Africa/Asmara'); + } + + #[\NoDiscard] + public function bangui(): Timezone + { + return ($this->of)('Africa/Bangui'); + } + + #[\NoDiscard] + public function addisAbaba(): Timezone + { + return ($this->of)('Africa/Addis_Ababa'); + } +} diff --git a/src/Timezone/America.php b/src/Timezone/America.php new file mode 100644 index 0000000..6b7bf24 --- /dev/null +++ b/src/Timezone/America.php @@ -0,0 +1,849 @@ +of); + } + + #[\NoDiscard] + public function indiana(): Indiana + { + return Indiana::new($this->of); + } + + #[\NoDiscard] + public function northDakota(): NorthDakota + { + return NorthDakota::new($this->of); + } + + #[\NoDiscard] + public function montreal(): Timezone + { + return ($this->of)('America/Montreal'); + } + + #[\NoDiscard] + public function guatemala(): Timezone + { + return ($this->of)('America/Guatemala'); + } + + #[\NoDiscard] + public function boaVista(): Timezone + { + return ($this->of)('America/Boa_Vista'); + } + + #[\NoDiscard] + public function portoAcre(): Timezone + { + return ($this->of)('America/Porto_Acre'); + } + + #[\NoDiscard] + public function winnipeg(): Timezone + { + return ($this->of)('America/Winnipeg'); + } + + #[\NoDiscard] + public function santiago(): Timezone + { + return ($this->of)('America/Santiago'); + } + + #[\NoDiscard] + public function virgin(): Timezone + { + return ($this->of)('America/Virgin'); + } + + #[\NoDiscard] + public function moncton(): Timezone + { + return ($this->of)('America/Moncton'); + } + + #[\NoDiscard] + public function noronha(): Timezone + { + return ($this->of)('America/Noronha'); + } + + #[\NoDiscard] + public function recife(): Timezone + { + return ($this->of)('America/Recife'); + } + + #[\NoDiscard] + public function saintKitts(): Timezone + { + return ($this->of)('America/St_Kitts'); + } + + #[\NoDiscard] + public function rankinInlet(): Timezone + { + return ($this->of)('America/Rankin_Inlet'); + } + + #[\NoDiscard] + public function jamaica(): Timezone + { + return ($this->of)('America/Jamaica'); + } + + #[\NoDiscard] + public function lima(): Timezone + { + return ($this->of)('America/Lima'); + } + + #[\NoDiscard] + public function rosario(): Timezone + { + return ($this->of)('America/Rosario'); + } + + #[\NoDiscard] + public function cambridgeBay(): Timezone + { + return ($this->of)('America/Cambridge_Bay'); + } + + #[\NoDiscard] + public function coralHarbour(): Timezone + { + return ($this->of)('America/Coral_Harbour'); + } + + #[\NoDiscard] + public function fortWayne(): Timezone + { + return ($this->of)('America/Fort_Wayne'); + } + + #[\NoDiscard] + public function nassau(): Timezone + { + return ($this->of)('America/Nassau'); + } + + #[\NoDiscard] + public function mazatlan(): Timezone + { + return ($this->of)('America/Mazatlan'); + } + + #[\NoDiscard] + public function grandTurk(): Timezone + { + return ($this->of)('America/Grand_Turk'); + } + + #[\NoDiscard] + public function merida(): Timezone + { + return ($this->of)('America/Merida'); + } + + #[\NoDiscard] + public function ensenada(): Timezone + { + return ($this->of)('America/Ensenada'); + } + + #[\NoDiscard] + public function rainyRiver(): Timezone + { + return ($this->of)('America/Rainy_River'); + } + + #[\NoDiscard] + public function bahiaBanderas(): Timezone + { + return ($this->of)('America/Bahia_Banderas'); + } + + #[\NoDiscard] + public function guadeloupe(): Timezone + { + return ($this->of)('America/Guadeloupe'); + } + + #[\NoDiscard] + public function cuiaba(): Timezone + { + return ($this->of)('America/Cuiaba'); + } + + #[\NoDiscard] + public function scoresbysund(): Timezone + { + return ($this->of)('America/Scoresbysund'); + } + + #[\NoDiscard] + public function maceio(): Timezone + { + return ($this->of)('America/Maceio'); + } + + #[\NoDiscard] + public function curacao(): Timezone + { + return ($this->of)('America/Curacao'); + } + + #[\NoDiscard] + public function aruba(): Timezone + { + return ($this->of)('America/Aruba'); + } + + #[\NoDiscard] + public function monterrey(): Timezone + { + return ($this->of)('America/Monterrey'); + } + + #[\NoDiscard] + public function hermosillo(): Timezone + { + return ($this->of)('America/Hermosillo'); + } + + #[\NoDiscard] + public function guayaquil(): Timezone + { + return ($this->of)('America/Guayaquil'); + } + + #[\NoDiscard] + public function managua(): Timezone + { + return ($this->of)('America/Managua'); + } + + #[\NoDiscard] + public function matamoros(): Timezone + { + return ($this->of)('America/Matamoros'); + } + + #[\NoDiscard] + public function losAngeles(): Timezone + { + return ($this->of)('America/Los_Angeles'); + } + + #[\NoDiscard] + public function tegucigalpa(): Timezone + { + return ($this->of)('America/Tegucigalpa'); + } + + #[\NoDiscard] + public function monticello(): Timezone + { + return ($this->of)('America/Kentucky/Monticello'); + } + + #[\NoDiscard] + public function nome(): Timezone + { + return ($this->of)('America/Nome'); + } + + #[\NoDiscard] + public function montevideo(): Timezone + { + return ($this->of)('America/Montevideo'); + } + + #[\NoDiscard] + public function gooseBay(): Timezone + { + return ($this->of)('America/Goose_Bay'); + } + + #[\NoDiscard] + public function boise(): Timezone + { + return ($this->of)('America/Boise'); + } + + #[\NoDiscard] + public function belem(): Timezone + { + return ($this->of)('America/Belem'); + } + + #[\NoDiscard] + public function atikokan(): Timezone + { + return ($this->of)('America/Atikokan'); + } + + #[\NoDiscard] + public function swiftCurrent(): Timezone + { + return ($this->of)('America/Swift_Current'); + } + + #[\NoDiscard] + public function detroit(): Timezone + { + return ($this->of)('America/Detroit'); + } + + #[\NoDiscard] + public function laPaz(): Timezone + { + return ($this->of)('America/La_Paz'); + } + + #[\NoDiscard] + public function chicago(): Timezone + { + return ($this->of)('America/Chicago'); + } + + #[\NoDiscard] + public function creston(): Timezone + { + return ($this->of)('America/Creston'); + } + + #[\NoDiscard] + public function nipigon(): Timezone + { + return ($this->of)('America/Nipigon'); + } + + #[\NoDiscard] + public function costaRica(): Timezone + { + return ($this->of)('America/Costa_Rica'); + } + + #[\NoDiscard] + public function halifax(): Timezone + { + return ($this->of)('America/Halifax'); + } + + #[\NoDiscard] + public function yellowknife(): Timezone + { + return ($this->of)('America/Yellowknife'); + } + + #[\NoDiscard] + public function puertoRico(): Timezone + { + return ($this->of)('America/Puerto_Rico'); + } + + #[\NoDiscard] + public function edmonton(): Timezone + { + return ($this->of)('America/Edmonton'); + } + + #[\NoDiscard] + public function mexicoCity(): Timezone + { + return ($this->of)('America/Mexico_City'); + } + + #[\NoDiscard] + public function saoPaulo(): Timezone + { + return ($this->of)('America/Sao_Paulo'); + } + + #[\NoDiscard] + public function yakutat(): Timezone + { + return ($this->of)('America/Yakutat'); + } + + #[\NoDiscard] + public function saintThomas(): Timezone + { + return ($this->of)('America/St_Thomas'); + } + + #[\NoDiscard] + public function chihuahua(): Timezone + { + return ($this->of)('America/Chihuahua'); + } + + #[\NoDiscard] + public function grenada(): Timezone + { + return ($this->of)('America/Grenada'); + } + + #[\NoDiscard] + public function elSalvador(): Timezone + { + return ($this->of)('America/El_Salvador'); + } + + #[\NoDiscard] + public function santoDomingo(): Timezone + { + return ($this->of)('America/Santo_Domingo'); + } + + #[\NoDiscard] + public function montserrat(): Timezone + { + return ($this->of)('America/Montserrat'); + } + + #[\NoDiscard] + public function portoVelho(): Timezone + { + return ($this->of)('America/Porto_Velho'); + } + + #[\NoDiscard] + public function panama(): Timezone + { + return ($this->of)('America/Panama'); + } + + #[\NoDiscard] + public function antigua(): Timezone + { + return ($this->of)('America/Antigua'); + } + + #[\NoDiscard] + public function santarem(): Timezone + { + return ($this->of)('America/Santarem'); + } + + #[\NoDiscard] + public function dawson(): Timezone + { + return ($this->of)('America/Dawson'); + } + + #[\NoDiscard] + public function saintBarthelemy(): Timezone + { + return ($this->of)('America/St_Barthelemy'); + } + + #[\NoDiscard] + public function iqaluit(): Timezone + { + return ($this->of)('America/Iqaluit'); + } + + #[\NoDiscard] + public function eirunepe(): Timezone + { + return ($this->of)('America/Eirunepe'); + } + + #[\NoDiscard] + public function inuvik(): Timezone + { + return ($this->of)('America/Inuvik'); + } + + #[\NoDiscard] + public function anguilla(): Timezone + { + return ($this->of)('America/Anguilla'); + } + + #[\NoDiscard] + public function portOfSpain(): Timezone + { + return ($this->of)('America/Port_of_Spain'); + } + + #[\NoDiscard] + public function araguaina(): Timezone + { + return ($this->of)('America/Araguaina'); + } + + #[\NoDiscard] + public function guyana(): Timezone + { + return ($this->of)('America/Guyana'); + } + + #[\NoDiscard] + public function fortaleza(): Timezone + { + return ($this->of)('America/Fortaleza'); + } + + #[\NoDiscard] + public function blancSablon(): Timezone + { + return ($this->of)('America/Blanc-Sablon'); + } + + #[\NoDiscard] + public function juneau(): Timezone + { + return ($this->of)('America/Juneau'); + } + + #[\NoDiscard] + public function cayman(): Timezone + { + return ($this->of)('America/Cayman'); + } + + #[\NoDiscard] + public function menominee(): Timezone + { + return ($this->of)('America/Menominee'); + } + + #[\NoDiscard] + public function cayenne(): Timezone + { + return ($this->of)('America/Cayenne'); + } + + #[\NoDiscard] + public function pangnirtung(): Timezone + { + return ($this->of)('America/Pangnirtung'); + } + + #[\NoDiscard] + public function metlakatla(): Timezone + { + return ($this->of)('America/Metlakatla'); + } + + #[\NoDiscard] + public function asuncion(): Timezone + { + return ($this->of)('America/Asuncion'); + } + + #[\NoDiscard] + public function saintLucia(): Timezone + { + return ($this->of)('America/St_Lucia'); + } + + #[\NoDiscard] + public function saintVincent(): Timezone + { + return ($this->of)('America/St_Vincent'); + } + + #[\NoDiscard] + public function martinique(): Timezone + { + return ($this->of)('America/Martinique'); + } + + #[\NoDiscard] + public function kralendijk(): Timezone + { + return ($this->of)('America/Kralendijk'); + } + + #[\NoDiscard] + public function newYork(): Timezone + { + return ($this->of)('America/New_York'); + } + + #[\NoDiscard] + public function vancouver(): Timezone + { + return ($this->of)('America/Vancouver'); + } + + #[\NoDiscard] + public function bogota(): Timezone + { + return ($this->of)('America/Bogota'); + } + + #[\NoDiscard] + public function dominica(): Timezone + { + return ($this->of)('America/Dominica'); + } + + #[\NoDiscard] + public function danmarkshavn(): Timezone + { + return ($this->of)('America/Danmarkshavn'); + } + + #[\NoDiscard] + public function anchorage(): Timezone + { + return ($this->of)('America/Anchorage'); + } + + #[\NoDiscard] + public function marigot(): Timezone + { + return ($this->of)('America/Marigot'); + } + + #[\NoDiscard] + public function rioBranco(): Timezone + { + return ($this->of)('America/Rio_Branco'); + } + + #[\NoDiscard] + public function paramaribo(): Timezone + { + return ($this->of)('America/Paramaribo'); + } + + #[\NoDiscard] + public function caracas(): Timezone + { + return ($this->of)('America/Caracas'); + } + + #[\NoDiscard] + public function resolute(): Timezone + { + return ($this->of)('America/Resolute'); + } + + #[\NoDiscard] + public function godthab(): Timezone + { + return ($this->of)('America/Godthab'); + } + + #[\NoDiscard] + public function catamarca(): Timezone + { + return ($this->of)('America/Catamarca'); + } + + #[\NoDiscard] + public function glaceBay(): Timezone + { + return ($this->of)('America/Glace_Bay'); + } + + #[\NoDiscard] + public function regina(): Timezone + { + return ($this->of)('America/Regina'); + } + + #[\NoDiscard] + public function toronto(): Timezone + { + return ($this->of)('America/Toronto'); + } + + #[\NoDiscard] + public function barbados(): Timezone + { + return ($this->of)('America/Barbados'); + } + + #[\NoDiscard] + public function santaIsabel(): Timezone + { + return ($this->of)('America/Santa_Isabel'); + } + + #[\NoDiscard] + public function miquelon(): Timezone + { + return ($this->of)('America/Miquelon'); + } + + #[\NoDiscard] + public function havana(): Timezone + { + return ($this->of)('America/Havana'); + } + + #[\NoDiscard] + public function ojinaga(): Timezone + { + return ($this->of)('America/Ojinaga'); + } + + #[\NoDiscard] + public function denver(): Timezone + { + return ($this->of)('America/Denver'); + } + + #[\NoDiscard] + public function cancun(): Timezone + { + return ($this->of)('America/Cancun'); + } + + #[\NoDiscard] + public function thunderBay(): Timezone + { + return ($this->of)('America/Thunder_Bay'); + } + + #[\NoDiscard] + public function adak(): Timezone + { + return ($this->of)('America/Adak'); + } + + #[\NoDiscard] + public function saintJohns(): Timezone + { + return ($this->of)('America/St_Johns'); + } + + #[\NoDiscard] + public function portAuPrince(): Timezone + { + return ($this->of)('America/Port-au-Prince'); + } + + #[\NoDiscard] + public function whitehorse(): Timezone + { + return ($this->of)('America/Whitehorse'); + } + + #[\NoDiscard] + public function louisville(): Timezone + { + return ($this->of)('America/Louisville'); + } + + #[\NoDiscard] + public function manaus(): Timezone + { + return ($this->of)('America/Manaus'); + } + + #[\NoDiscard] + public function lowerPrinces(): Timezone + { + return ($this->of)('America/Lower_Princes'); + } + + #[\NoDiscard] + public function sitka(): Timezone + { + return ($this->of)('America/Sitka'); + } + + #[\NoDiscard] + public function thule(): Timezone + { + return ($this->of)('America/Thule'); + } + + #[\NoDiscard] + public function campoGrande(): Timezone + { + return ($this->of)('America/Campo_Grande'); + } + + #[\NoDiscard] + public function phoenix(): Timezone + { + return ($this->of)('America/Phoenix'); + } + + #[\NoDiscard] + public function shiprock(): Timezone + { + return ($this->of)('America/Shiprock'); + } + + #[\NoDiscard] + public function bahia(): Timezone + { + return ($this->of)('America/Bahia'); + } + + #[\NoDiscard] + public function tortola(): Timezone + { + return ($this->of)('America/Tortola'); + } + + #[\NoDiscard] + public function dawsonCreek(): Timezone + { + return ($this->of)('America/Dawson_Creek'); + } + + #[\NoDiscard] + public function tijuana(): Timezone + { + return ($this->of)('America/Tijuana'); + } + + #[\NoDiscard] + public function belize(): Timezone + { + return ($this->of)('America/Belize'); + } + + #[\NoDiscard] + public function atka(): Timezone + { + return ($this->of)('America/Atka'); + } +} diff --git a/src/Timezone/America/Argentina.php b/src/Timezone/America/Argentina.php new file mode 100644 index 0000000..1164ee3 --- /dev/null +++ b/src/Timezone/America/Argentina.php @@ -0,0 +1,106 @@ +of)('America/Argentina/Rio_Gallegos'); + } + + #[\NoDiscard] + public function mendoza(): Timezone + { + return ($this->of)('America/Argentina/Mendoza'); + } + + #[\NoDiscard] + public function buenosAires(): Timezone + { + return ($this->of)('America/Argentina/Buenos_Aires'); + } + + #[\NoDiscard] + public function ushuaia(): Timezone + { + return ($this->of)('America/Argentina/Ushuaia'); + } + + #[\NoDiscard] + public function sanJuan(): Timezone + { + return ($this->of)('America/Argentina/San_Juan'); + } + + #[\NoDiscard] + public function laRioja(): Timezone + { + return ($this->of)('America/Argentina/La_Rioja'); + } + + #[\NoDiscard] + public function salta(): Timezone + { + return ($this->of)('America/Argentina/Salta'); + } + + #[\NoDiscard] + public function sanLuis(): Timezone + { + return ($this->of)('America/Argentina/San_Luis'); + } + + #[\NoDiscard] + public function jujuy(): Timezone + { + return ($this->of)('America/Argentina/Jujuy'); + } + + #[\NoDiscard] + public function tucuman(): Timezone + { + return ($this->of)('America/Argentina/Tucuman'); + } + + #[\NoDiscard] + public function comodRivadavia(): Timezone + { + return ($this->of)('America/Argentina/ComodRivadavia'); + } + + #[\NoDiscard] + public function catamarca(): Timezone + { + return ($this->of)('America/Argentina/Catamarca'); + } + + #[\NoDiscard] + public function cordoba(): Timezone + { + return ($this->of)('America/Argentina/Cordoba'); + } +} diff --git a/src/Timezone/America/Indiana.php b/src/Timezone/America/Indiana.php new file mode 100644 index 0000000..b01a850 --- /dev/null +++ b/src/Timezone/America/Indiana.php @@ -0,0 +1,76 @@ +of)('America/Indiana/Vincennes'); + } + + #[\NoDiscard] + public function marengo(): Timezone + { + return ($this->of)('America/Indiana/Marengo'); + } + + #[\NoDiscard] + public function tellCity(): Timezone + { + return ($this->of)('America/Indiana/Tell_City'); + } + + #[\NoDiscard] + public function knox(): Timezone + { + return ($this->of)('America/Indiana/Knox'); + } + + #[\NoDiscard] + public function vevay(): Timezone + { + return ($this->of)('America/Indiana/Vevay'); + } + + #[\NoDiscard] + public function indianapolis(): Timezone + { + return ($this->of)('America/Indiana/Indianapolis'); + } + + #[\NoDiscard] + public function petersburg(): Timezone + { + return ($this->of)('America/Indiana/Petersburg'); + } + + #[\NoDiscard] + public function winamac(): Timezone + { + return ($this->of)('America/Indiana/Winamac'); + } +} diff --git a/src/Timezone/America/NorthDakota.php b/src/Timezone/America/NorthDakota.php new file mode 100644 index 0000000..f925e47 --- /dev/null +++ b/src/Timezone/America/NorthDakota.php @@ -0,0 +1,46 @@ +of)('America/North_Dakota/Beulah'); + } + + #[\NoDiscard] + public function newSalem(): Timezone + { + return ($this->of)('America/North_Dakota/New_Salem'); + } + + #[\NoDiscard] + public function center(): Timezone + { + return ($this->of)('America/North_Dakota/Center'); + } +} diff --git a/src/Timezone/Antartica.php b/src/Timezone/Antartica.php new file mode 100644 index 0000000..098ac2d --- /dev/null +++ b/src/Timezone/Antartica.php @@ -0,0 +1,100 @@ +of)('Antarctica/Davis'); + } + + #[\NoDiscard] + public function palmer(): Timezone + { + return ($this->of)('Antarctica/Palmer'); + } + + #[\NoDiscard] + public function syowa(): Timezone + { + return ($this->of)('Antarctica/Syowa'); + } + + #[\NoDiscard] + public function casey(): Timezone + { + return ($this->of)('Antarctica/Casey'); + } + + #[\NoDiscard] + public function troll(): Timezone + { + return ($this->of)('Antarctica/Troll'); + } + + #[\NoDiscard] + public function mcMurdo(): Timezone + { + return ($this->of)('Antarctica/McMurdo'); + } + + #[\NoDiscard] + public function vostok(): Timezone + { + return ($this->of)('Antarctica/Vostok'); + } + + #[\NoDiscard] + public function rothera(): Timezone + { + return ($this->of)('Antarctica/Rothera'); + } + + #[\NoDiscard] + public function mawson(): Timezone + { + return ($this->of)('Antarctica/Mawson'); + } + + #[\NoDiscard] + public function macquarie(): Timezone + { + return ($this->of)('Antarctica/Macquarie'); + } + + #[\NoDiscard] + public function southPole(): Timezone + { + return ($this->of)('Antarctica/South_Pole'); + } + + #[\NoDiscard] + public function dumontDUrville(): Timezone + { + return ($this->of)('Antarctica/DumontDUrville'); + } +} diff --git a/src/Timezone/Arctic.php b/src/Timezone/Arctic.php new file mode 100644 index 0000000..ebd7d0e --- /dev/null +++ b/src/Timezone/Arctic.php @@ -0,0 +1,34 @@ +of)('Arctic/Longyearbyen'); + } +} diff --git a/src/Timezone/Asia.php b/src/Timezone/Asia.php new file mode 100644 index 0000000..5b33653 --- /dev/null +++ b/src/Timezone/Asia.php @@ -0,0 +1,574 @@ +of)('Asia/Manila'); + } + + #[\NoDiscard] + public function baghdad(): Timezone + { + return ($this->of)('Asia/Baghdad'); + } + + #[\NoDiscard] + public function ulaanbaatar(): Timezone + { + return ($this->of)('Asia/Ulaanbaatar'); + } + + #[\NoDiscard] + public function almaty(): Timezone + { + return ($this->of)('Asia/Almaty'); + } + + #[\NoDiscard] + public function samarkand(): Timezone + { + return ($this->of)('Asia/Samarkand'); + } + + #[\NoDiscard] + public function ustNera(): Timezone + { + return ($this->of)('Asia/Ust-Nera'); + } + + #[\NoDiscard] + public function pontianak(): Timezone + { + return ($this->of)('Asia/Pontianak'); + } + + #[\NoDiscard] + public function tehran(): Timezone + { + return ($this->of)('Asia/Tehran'); + } + + #[\NoDiscard] + public function saigon(): Timezone + { + return ($this->of)('Asia/Saigon'); + } + + #[\NoDiscard] + public function krasnoyarsk(): Timezone + { + return ($this->of)('Asia/Krasnoyarsk'); + } + + #[\NoDiscard] + public function hebron(): Timezone + { + return ($this->of)('Asia/Hebron'); + } + + #[\NoDiscard] + public function kuching(): Timezone + { + return ($this->of)('Asia/Kuching'); + } + + #[\NoDiscard] + public function katmandu(): Timezone + { + return ($this->of)('Asia/Katmandu'); + } + + #[\NoDiscard] + public function shanghai(): Timezone + { + return ($this->of)('Asia/Shanghai'); + } + + #[\NoDiscard] + public function calcutta(): Timezone + { + return ($this->of)('Asia/Calcutta'); + } + + #[\NoDiscard] + public function jayapura(): Timezone + { + return ($this->of)('Asia/Jayapura'); + } + + #[\NoDiscard] + public function muscat(): Timezone + { + return ($this->of)('Asia/Muscat'); + } + + #[\NoDiscard] + public function omsk(): Timezone + { + return ($this->of)('Asia/Omsk'); + } + + #[\NoDiscard] + public function aqtau(): Timezone + { + return ($this->of)('Asia/Aqtau'); + } + + #[\NoDiscard] + public function khandyga(): Timezone + { + return ($this->of)('Asia/Khandyga'); + } + + #[\NoDiscard] + public function riyadh(): Timezone + { + return ($this->of)('Asia/Riyadh'); + } + + #[\NoDiscard] + public function bangkok(): Timezone + { + return ($this->of)('Asia/Bangkok'); + } + + #[\NoDiscard] + public function thimphu(): Timezone + { + return ($this->of)('Asia/Thimphu'); + } + + #[\NoDiscard] + public function aden(): Timezone + { + return ($this->of)('Asia/Aden'); + } + + #[\NoDiscard] + public function yekaterinburg(): Timezone + { + return ($this->of)('Asia/Yekaterinburg'); + } + + #[\NoDiscard] + public function oral(): Timezone + { + return ($this->of)('Asia/Oral'); + } + + #[\NoDiscard] + public function novokuznetsk(): Timezone + { + return ($this->of)('Asia/Novokuznetsk'); + } + + #[\NoDiscard] + public function bishkek(): Timezone + { + return ($this->of)('Asia/Bishkek'); + } + + #[\NoDiscard] + public function macau(): Timezone + { + return ($this->of)('Asia/Macau'); + } + + #[\NoDiscard] + public function qyzylorda(): Timezone + { + return ($this->of)('Asia/Qyzylorda'); + } + + #[\NoDiscard] + public function seoul(): Timezone + { + return ($this->of)('Asia/Seoul'); + } + + #[\NoDiscard] + public function irkutsk(): Timezone + { + return ($this->of)('Asia/Irkutsk'); + } + + #[\NoDiscard] + public function aqtobe(): Timezone + { + return ($this->of)('Asia/Aqtobe'); + } + + #[\NoDiscard] + public function chongqing(): Timezone + { + return ($this->of)('Asia/Chongqing'); + } + + #[\NoDiscard] + public function kabul(): Timezone + { + return ($this->of)('Asia/Kabul'); + } + + #[\NoDiscard] + public function thimbu(): Timezone + { + return ($this->of)('Asia/Thimbu'); + } + + #[\NoDiscard] + public function karachi(): Timezone + { + return ($this->of)('Asia/Karachi'); + } + + #[\NoDiscard] + public function jakarta(): Timezone + { + return ($this->of)('Asia/Jakarta'); + } + + #[\NoDiscard] + public function harbin(): Timezone + { + return ($this->of)('Asia/Harbin'); + } + + #[\NoDiscard] + public function novosibirsk(): Timezone + { + return ($this->of)('Asia/Novosibirsk'); + } + + #[\NoDiscard] + public function dili(): Timezone + { + return ($this->of)('Asia/Dili'); + } + + #[\NoDiscard] + public function colombo(): Timezone + { + return ($this->of)('Asia/Colombo'); + } + + #[\NoDiscard] + public function ashkhabad(): Timezone + { + return ($this->of)('Asia/Ashkhabad'); + } + + #[\NoDiscard] + public function dacca(): Timezone + { + return ($this->of)('Asia/Dacca'); + } + + #[\NoDiscard] + public function ashgabat(): Timezone + { + return ($this->of)('Asia/Ashgabat'); + } + + #[\NoDiscard] + public function ujungPandang(): Timezone + { + return ($this->of)('Asia/Ujung_Pandang'); + } + + #[\NoDiscard] + public function qatar(): Timezone + { + return ($this->of)('Asia/Qatar'); + } + + #[\NoDiscard] + public function tokyo(): Timezone + { + return ($this->of)('Asia/Tokyo'); + } + + #[\NoDiscard] + public function macao(): Timezone + { + return ($this->of)('Asia/Macao'); + } + + #[\NoDiscard] + public function tashkent(): Timezone + { + return ($this->of)('Asia/Tashkent'); + } + + #[\NoDiscard] + public function baku(): Timezone + { + return ($this->of)('Asia/Baku'); + } + + #[\NoDiscard] + public function pyongyang(): Timezone + { + return ($this->of)('Asia/Pyongyang'); + } + + #[\NoDiscard] + public function tbilisi(): Timezone + { + return ($this->of)('Asia/Tbilisi'); + } + + #[\NoDiscard] + public function amman(): Timezone + { + return ($this->of)('Asia/Amman'); + } + + #[\NoDiscard] + public function vladivostok(): Timezone + { + return ($this->of)('Asia/Vladivostok'); + } + + #[\NoDiscard] + public function damascus(): Timezone + { + return ($this->of)('Asia/Damascus'); + } + + #[\NoDiscard] + public function bahrain(): Timezone + { + return ($this->of)('Asia/Bahrain'); + } + + #[\NoDiscard] + public function vientiane(): Timezone + { + return ($this->of)('Asia/Vientiane'); + } + + #[\NoDiscard] + public function hovd(): Timezone + { + return ($this->of)('Asia/Hovd'); + } + + #[\NoDiscard] + public function kuwait(): Timezone + { + return ($this->of)('Asia/Kuwait'); + } + + #[\NoDiscard] + public function magadan(): Timezone + { + return ($this->of)('Asia/Magadan'); + } + + #[\NoDiscard] + public function ulanBator(): Timezone + { + return ($this->of)('Asia/Ulan_Bator'); + } + + #[\NoDiscard] + public function nicosia(): Timezone + { + return ($this->of)('Asia/Nicosia'); + } + + #[\NoDiscard] + public function telAviv(): Timezone + { + return ($this->of)('Asia/Tel_Aviv'); + } + + #[\NoDiscard] + public function choibalsan(): Timezone + { + return ($this->of)('Asia/Choibalsan'); + } + + #[\NoDiscard] + public function brunei(): Timezone + { + return ($this->of)('Asia/Brunei'); + } + + #[\NoDiscard] + public function kualaLumpur(): Timezone + { + return ($this->of)('Asia/Kuala_Lumpur'); + } + + #[\NoDiscard] + public function kathmandu(): Timezone + { + return ($this->of)('Asia/Kathmandu'); + } + + #[\NoDiscard] + public function srednekolymsk(): Timezone + { + return ($this->of)('Asia/Srednekolymsk'); + } + + #[\NoDiscard] + public function dubai(): Timezone + { + return ($this->of)('Asia/Dubai'); + } + + #[\NoDiscard] + public function yakutsk(): Timezone + { + return ($this->of)('Asia/Yakutsk'); + } + + #[\NoDiscard] + public function beirut(): Timezone + { + return ($this->of)('Asia/Beirut'); + } + + #[\NoDiscard] + public function gaza(): Timezone + { + return ($this->of)('Asia/Gaza'); + } + + #[\NoDiscard] + public function singapore(): Timezone + { + return ($this->of)('Asia/Singapore'); + } + + #[\NoDiscard] + public function rangoon(): Timezone + { + return ($this->of)('Asia/Rangoon'); + } + + #[\NoDiscard] + public function sakhalin(): Timezone + { + return ($this->of)('Asia/Sakhalin'); + } + + #[\NoDiscard] + public function phnomPenh(): Timezone + { + return ($this->of)('Asia/Phnom_Penh'); + } + + #[\NoDiscard] + public function kamchatka(): Timezone + { + return ($this->of)('Asia/Kamchatka'); + } + + #[\NoDiscard] + public function yerevan(): Timezone + { + return ($this->of)('Asia/Yerevan'); + } + + #[\NoDiscard] + public function chungking(): Timezone + { + return ($this->of)('Asia/Chungking'); + } + + #[\NoDiscard] + public function hoChiMinh(): Timezone + { + return ($this->of)('Asia/Ho_Chi_Minh'); + } + + #[\NoDiscard] + public function chita(): Timezone + { + return ($this->of)('Asia/Chita'); + } + + #[\NoDiscard] + public function istanbul(): Timezone + { + return ($this->of)('Asia/Istanbul'); + } + + #[\NoDiscard] + public function hongKong(): Timezone + { + return ($this->of)('Asia/Hong_Kong'); + } + + #[\NoDiscard] + public function dhaka(): Timezone + { + return ($this->of)('Asia/Dhaka'); + } + + #[\NoDiscard] + public function jerusalem(): Timezone + { + return ($this->of)('Asia/Jerusalem'); + } + + #[\NoDiscard] + public function makassar(): Timezone + { + return ($this->of)('Asia/Makassar'); + } + + #[\NoDiscard] + public function kolkata(): Timezone + { + return ($this->of)('Asia/Kolkata'); + } + + #[\NoDiscard] + public function taipei(): Timezone + { + return ($this->of)('Asia/Taipei'); + } + + #[\NoDiscard] + public function dushanbe(): Timezone + { + return ($this->of)('Asia/Dushanbe'); + } + + #[\NoDiscard] + public function anadyr(): Timezone + { + return ($this->of)('Asia/Anadyr'); + } +} diff --git a/src/Timezone/Atlantic.php b/src/Timezone/Atlantic.php new file mode 100644 index 0000000..87a9ddf --- /dev/null +++ b/src/Timezone/Atlantic.php @@ -0,0 +1,100 @@ +of)('Atlantic/Faroe'); + } + + #[\NoDiscard] + public function southGeorgia(): Timezone + { + return ($this->of)('Atlantic/South_Georgia'); + } + + #[\NoDiscard] + public function capeVerde(): Timezone + { + return ($this->of)('Atlantic/Cape_Verde'); + } + + #[\NoDiscard] + public function faeroe(): Timezone + { + return ($this->of)('Atlantic/Faeroe'); + } + + #[\NoDiscard] + public function bermuda(): Timezone + { + return ($this->of)('Atlantic/Bermuda'); + } + + #[\NoDiscard] + public function janMayen(): Timezone + { + return ($this->of)('Atlantic/Jan_Mayen'); + } + + #[\NoDiscard] + public function reykjavik(): Timezone + { + return ($this->of)('Atlantic/Reykjavik'); + } + + #[\NoDiscard] + public function saintHelena(): Timezone + { + return ($this->of)('Atlantic/St_Helena'); + } + + #[\NoDiscard] + public function canary(): Timezone + { + return ($this->of)('Atlantic/Canary'); + } + + #[\NoDiscard] + public function madeira(): Timezone + { + return ($this->of)('Atlantic/Madeira'); + } + + #[\NoDiscard] + public function azores(): Timezone + { + return ($this->of)('Atlantic/Azores'); + } + + #[\NoDiscard] + public function stanley(): Timezone + { + return ($this->of)('Atlantic/Stanley'); + } +} diff --git a/src/Timezone/Australia.php b/src/Timezone/Australia.php new file mode 100644 index 0000000..d441765 --- /dev/null +++ b/src/Timezone/Australia.php @@ -0,0 +1,160 @@ +of)('Australia/Lindeman'); + } + + #[\NoDiscard] + public function currie(): Timezone + { + return ($this->of)('Australia/Currie'); + } + + #[\NoDiscard] + public function victoria(): Timezone + { + return ($this->of)('Australia/Victoria'); + } + + #[\NoDiscard] + public function adelaide(): Timezone + { + return ($this->of)('Australia/Adelaide'); + } + + #[\NoDiscard] + public function perth(): Timezone + { + return ($this->of)('Australia/Perth'); + } + + #[\NoDiscard] + public function brisbane(): Timezone + { + return ($this->of)('Australia/Brisbane'); + } + + #[\NoDiscard] + public function west(): Timezone + { + return ($this->of)('Australia/West'); + } + + #[\NoDiscard] + public function australianCapitalTerritory(): Timezone + { + return ($this->of)('Australia/ACT'); + } + + #[\NoDiscard] + public function north(): Timezone + { + return ($this->of)('Australia/North'); + } + + #[\NoDiscard] + public function eucla(): Timezone + { + return ($this->of)('Australia/Eucla'); + } + + #[\NoDiscard] + public function lordeHoweIsland(): Timezone + { + return ($this->of)('Australia/LHI'); + } + + #[\NoDiscard] + public function newSouthWales(): Timezone + { + return ($this->of)('Australia/NSW'); + } + + #[\NoDiscard] + public function queensland(): Timezone + { + return ($this->of)('Australia/Queensland'); + } + + #[\NoDiscard] + public function south(): Timezone + { + return ($this->of)('Australia/South'); + } + + #[\NoDiscard] + public function melbourne(): Timezone + { + return ($this->of)('Australia/Melbourne'); + } + + #[\NoDiscard] + public function yancowinna(): Timezone + { + return ($this->of)('Australia/Yancowinna'); + } + + #[\NoDiscard] + public function canberra(): Timezone + { + return ($this->of)('Australia/Canberra'); + } + + #[\NoDiscard] + public function sydney(): Timezone + { + return ($this->of)('Australia/Sydney'); + } + + #[\NoDiscard] + public function darwin(): Timezone + { + return ($this->of)('Australia/Darwin'); + } + + #[\NoDiscard] + public function hobart(): Timezone + { + return ($this->of)('Australia/Hobart'); + } + + #[\NoDiscard] + public function brokenHill(): Timezone + { + return ($this->of)('Australia/Broken_Hill'); + } + + #[\NoDiscard] + public function tasmania(): Timezone + { + return ($this->of)('Australia/Tasmania'); + } +} diff --git a/src/Timezone/Europe.php b/src/Timezone/Europe.php new file mode 100644 index 0000000..85f8f4d --- /dev/null +++ b/src/Timezone/Europe.php @@ -0,0 +1,382 @@ +of)('Europe/Uzhgorod'); + } + + #[\NoDiscard] + public function riga(): Timezone + { + return ($this->of)('Europe/Riga'); + } + + #[\NoDiscard] + public function paris(): Timezone + { + return ($this->of)('Europe/Paris'); + } + + #[\NoDiscard] + public function guernsey(): Timezone + { + return ($this->of)('Europe/Guernsey'); + } + + #[\NoDiscard] + public function samara(): Timezone + { + return ($this->of)('Europe/Samara'); + } + + #[\NoDiscard] + public function athens(): Timezone + { + return ($this->of)('Europe/Athens'); + } + + #[\NoDiscard] + public function tirane(): Timezone + { + return ($this->of)('Europe/Tirane'); + } + + #[\NoDiscard] + public function london(): Timezone + { + return ($this->of)('Europe/London'); + } + + #[\NoDiscard] + public function helsinki(): Timezone + { + return ($this->of)('Europe/Helsinki'); + } + + #[\NoDiscard] + public function oslo(): Timezone + { + return ($this->of)('Europe/Oslo'); + } + + #[\NoDiscard] + public function podgorica(): Timezone + { + return ($this->of)('Europe/Podgorica'); + } + + #[\NoDiscard] + public function minsk(): Timezone + { + return ($this->of)('Europe/Minsk'); + } + + #[\NoDiscard] + public function monaco(): Timezone + { + return ($this->of)('Europe/Monaco'); + } + + #[\NoDiscard] + public function lisbon(): Timezone + { + return ($this->of)('Europe/Lisbon'); + } + + #[\NoDiscard] + public function tallinn(): Timezone + { + return ($this->of)('Europe/Tallinn'); + } + + #[\NoDiscard] + public function berlin(): Timezone + { + return ($this->of)('Europe/Berlin'); + } + + #[\NoDiscard] + public function gibraltar(): Timezone + { + return ($this->of)('Europe/Gibraltar'); + } + + #[\NoDiscard] + public function prague(): Timezone + { + return ($this->of)('Europe/Prague'); + } + + #[\NoDiscard] + public function stockholm(): Timezone + { + return ($this->of)('Europe/Stockholm'); + } + + #[\NoDiscard] + public function moscow(): Timezone + { + return ($this->of)('Europe/Moscow'); + } + + #[\NoDiscard] + public function bucharest(): Timezone + { + return ($this->of)('Europe/Bucharest'); + } + + #[\NoDiscard] + public function andorra(): Timezone + { + return ($this->of)('Europe/Andorra'); + } + + #[\NoDiscard] + public function vilnius(): Timezone + { + return ($this->of)('Europe/Vilnius'); + } + + #[\NoDiscard] + public function rome(): Timezone + { + return ($this->of)('Europe/Rome'); + } + + #[\NoDiscard] + public function kiev(): Timezone + { + return ($this->of)('Europe/Kiev'); + } + + #[\NoDiscard] + public function copenhagen(): Timezone + { + return ($this->of)('Europe/Copenhagen'); + } + + #[\NoDiscard] + public function belgrade(): Timezone + { + return ($this->of)('Europe/Belgrade'); + } + + #[\NoDiscard] + public function isleOfMan(): Timezone + { + return ($this->of)('Europe/Isle_of_Man'); + } + + #[\NoDiscard] + public function budapest(): Timezone + { + return ($this->of)('Europe/Budapest'); + } + + #[\NoDiscard] + public function tiraspol(): Timezone + { + return ($this->of)('Europe/Tiraspol'); + } + + #[\NoDiscard] + public function vaduz(): Timezone + { + return ($this->of)('Europe/Vaduz'); + } + + #[\NoDiscard] + public function sarajevo(): Timezone + { + return ($this->of)('Europe/Sarajevo'); + } + + #[\NoDiscard] + public function amsterdam(): Timezone + { + return ($this->of)('Europe/Amsterdam'); + } + + #[\NoDiscard] + public function mariehamn(): Timezone + { + return ($this->of)('Europe/Mariehamn'); + } + + #[\NoDiscard] + public function skopje(): Timezone + { + return ($this->of)('Europe/Skopje'); + } + + #[\NoDiscard] + public function kaliningrad(): Timezone + { + return ($this->of)('Europe/Kaliningrad'); + } + + #[\NoDiscard] + public function bratislava(): Timezone + { + return ($this->of)('Europe/Bratislava'); + } + + #[\NoDiscard] + public function sanMarino(): Timezone + { + return ($this->of)('Europe/San_Marino'); + } + + #[\NoDiscard] + public function busingen(): Timezone + { + return ($this->of)('Europe/Busingen'); + } + + #[\NoDiscard] + public function zaporozhye(): Timezone + { + return ($this->of)('Europe/Zaporozhye'); + } + + #[\NoDiscard] + public function chisinau(): Timezone + { + return ($this->of)('Europe/Chisinau'); + } + + #[\NoDiscard] + public function brussels(): Timezone + { + return ($this->of)('Europe/Brussels'); + } + + #[\NoDiscard] + public function luxembourg(): Timezone + { + return ($this->of)('Europe/Luxembourg'); + } + + #[\NoDiscard] + public function belfast(): Timezone + { + return ($this->of)('Europe/Belfast'); + } + + #[\NoDiscard] + public function vienna(): Timezone + { + return ($this->of)('Europe/Vienna'); + } + + #[\NoDiscard] + public function ljubljana(): Timezone + { + return ($this->of)('Europe/Ljubljana'); + } + + #[\NoDiscard] + public function simferopol(): Timezone + { + return ($this->of)('Europe/Simferopol'); + } + + #[\NoDiscard] + public function dublin(): Timezone + { + return ($this->of)('Europe/Dublin'); + } + + #[\NoDiscard] + public function nicosia(): Timezone + { + return ($this->of)('Europe/Nicosia'); + } + + #[\NoDiscard] + public function zagreb(): Timezone + { + return ($this->of)('Europe/Zagreb'); + } + + #[\NoDiscard] + public function jersey(): Timezone + { + return ($this->of)('Europe/Jersey'); + } + + #[\NoDiscard] + public function madrid(): Timezone + { + return ($this->of)('Europe/Madrid'); + } + + #[\NoDiscard] + public function vatican(): Timezone + { + return ($this->of)('Europe/Vatican'); + } + + #[\NoDiscard] + public function istanbul(): Timezone + { + return ($this->of)('Europe/Istanbul'); + } + + #[\NoDiscard] + public function zurich(): Timezone + { + return ($this->of)('Europe/Zurich'); + } + + #[\NoDiscard] + public function sofia(): Timezone + { + return ($this->of)('Europe/Sofia'); + } + + #[\NoDiscard] + public function volgograd(): Timezone + { + return ($this->of)('Europe/Volgograd'); + } + + #[\NoDiscard] + public function malta(): Timezone + { + return ($this->of)('Europe/Malta'); + } + + #[\NoDiscard] + public function warsaw(): Timezone + { + return ($this->of)('Europe/Warsaw'); + } +} diff --git a/src/Timezone/Indian.php b/src/Timezone/Indian.php new file mode 100644 index 0000000..e4fffd3 --- /dev/null +++ b/src/Timezone/Indian.php @@ -0,0 +1,94 @@ +of)('Indian/Cocos'); + } + + #[\NoDiscard] + public function antananarivo(): Timezone + { + return ($this->of)('Indian/Antananarivo'); + } + + #[\NoDiscard] + public function reunion(): Timezone + { + return ($this->of)('Indian/Reunion'); + } + + #[\NoDiscard] + public function chagos(): Timezone + { + return ($this->of)('Indian/Chagos'); + } + + #[\NoDiscard] + public function comoro(): Timezone + { + return ($this->of)('Indian/Comoro'); + } + + #[\NoDiscard] + public function mayotte(): Timezone + { + return ($this->of)('Indian/Mayotte'); + } + + #[\NoDiscard] + public function maldives(): Timezone + { + return ($this->of)('Indian/Maldives'); + } + + #[\NoDiscard] + public function mauritius(): Timezone + { + return ($this->of)('Indian/Mauritius'); + } + + #[\NoDiscard] + public function mahe(): Timezone + { + return ($this->of)('Indian/Mahe'); + } + + #[\NoDiscard] + public function kerguelen(): Timezone + { + return ($this->of)('Indian/Kerguelen'); + } + + #[\NoDiscard] + public function christmas(): Timezone + { + return ($this->of)('Indian/Christmas'); + } +} diff --git a/src/Timezone/Pacific.php b/src/Timezone/Pacific.php new file mode 100644 index 0000000..35dab60 --- /dev/null +++ b/src/Timezone/Pacific.php @@ -0,0 +1,280 @@ +of)('Pacific/Kosrae'); + } + + #[\NoDiscard] + public function enderbury(): Timezone + { + return ($this->of)('Pacific/Enderbury'); + } + + #[\NoDiscard] + public function apia(): Timezone + { + return ($this->of)('Pacific/Apia'); + } + + #[\NoDiscard] + public function noumea(): Timezone + { + return ($this->of)('Pacific/Noumea'); + } + + #[\NoDiscard] + public function chatham(): Timezone + { + return ($this->of)('Pacific/Chatham'); + } + + #[\NoDiscard] + public function wake(): Timezone + { + return ($this->of)('Pacific/Wake'); + } + + #[\NoDiscard] + public function wallis(): Timezone + { + return ($this->of)('Pacific/Wallis'); + } + + #[\NoDiscard] + public function johnston(): Timezone + { + return ($this->of)('Pacific/Johnston'); + } + + #[\NoDiscard] + public function saipan(): Timezone + { + return ($this->of)('Pacific/Saipan'); + } + + #[\NoDiscard] + public function tarawa(): Timezone + { + return ($this->of)('Pacific/Tarawa'); + } + + #[\NoDiscard] + public function pitcairn(): Timezone + { + return ($this->of)('Pacific/Pitcairn'); + } + + #[\NoDiscard] + public function niue(): Timezone + { + return ($this->of)('Pacific/Niue'); + } + + #[\NoDiscard] + public function ponape(): Timezone + { + return ($this->of)('Pacific/Ponape'); + } + + #[\NoDiscard] + public function guam(): Timezone + { + return ($this->of)('Pacific/Guam'); + } + + #[\NoDiscard] + public function auckland(): Timezone + { + return ($this->of)('Pacific/Auckland'); + } + + #[\NoDiscard] + public function pagoPago(): Timezone + { + return ($this->of)('Pacific/Pago_Pago'); + } + + #[\NoDiscard] + public function chuuk(): Timezone + { + return ($this->of)('Pacific/Chuuk'); + } + + #[\NoDiscard] + public function kwajalein(): Timezone + { + return ($this->of)('Pacific/Kwajalein'); + } + + #[\NoDiscard] + public function fakaofo(): Timezone + { + return ($this->of)('Pacific/Fakaofo'); + } + + #[\NoDiscard] + public function majuro(): Timezone + { + return ($this->of)('Pacific/Majuro'); + } + + #[\NoDiscard] + public function guadalcanal(): Timezone + { + return ($this->of)('Pacific/Guadalcanal'); + } + + #[\NoDiscard] + public function efate(): Timezone + { + return ($this->of)('Pacific/Efate'); + } + + #[\NoDiscard] + public function tongatapu(): Timezone + { + return ($this->of)('Pacific/Tongatapu'); + } + + #[\NoDiscard] + public function pohnpei(): Timezone + { + return ($this->of)('Pacific/Pohnpei'); + } + + #[\NoDiscard] + public function honolulu(): Timezone + { + return ($this->of)('Pacific/Honolulu'); + } + + #[\NoDiscard] + public function bougainville(): Timezone + { + return ($this->of)('Pacific/Bougainville'); + } + + #[\NoDiscard] + public function galapagos(): Timezone + { + return ($this->of)('Pacific/Galapagos'); + } + + #[\NoDiscard] + public function gambier(): Timezone + { + return ($this->of)('Pacific/Gambier'); + } + + #[\NoDiscard] + public function palau(): Timezone + { + return ($this->of)('Pacific/Palau'); + } + + #[\NoDiscard] + public function midway(): Timezone + { + return ($this->of)('Pacific/Midway'); + } + + #[\NoDiscard] + public function marquesas(): Timezone + { + return ($this->of)('Pacific/Marquesas'); + } + + #[\NoDiscard] + public function funafuti(): Timezone + { + return ($this->of)('Pacific/Funafuti'); + } + + #[\NoDiscard] + public function norfolk(): Timezone + { + return ($this->of)('Pacific/Norfolk'); + } + + #[\NoDiscard] + public function portMoresby(): Timezone + { + return ($this->of)('Pacific/Port_Moresby'); + } + + #[\NoDiscard] + public function tahiti(): Timezone + { + return ($this->of)('Pacific/Tahiti'); + } + + #[\NoDiscard] + public function fiji(): Timezone + { + return ($this->of)('Pacific/Fiji'); + } + + #[\NoDiscard] + public function kiritimati(): Timezone + { + return ($this->of)('Pacific/Kiritimati'); + } + + #[\NoDiscard] + public function truk(): Timezone + { + return ($this->of)('Pacific/Truk'); + } + + #[\NoDiscard] + public function easter(): Timezone + { + return ($this->of)('Pacific/Easter'); + } + + #[\NoDiscard] + public function rarotonga(): Timezone + { + return ($this->of)('Pacific/Rarotonga'); + } + + #[\NoDiscard] + public function yap(): Timezone + { + return ($this->of)('Pacific/Yap'); + } + + #[\NoDiscard] + public function nauru(): Timezone + { + return ($this->of)('Pacific/Nauru'); + } +} diff --git a/src/Timezones.php b/src/Timezones.php new file mode 100644 index 0000000..2fb8878 --- /dev/null +++ b/src/Timezones.php @@ -0,0 +1,105 @@ +of)('UTC'); + } + + #[\NoDiscard] + public function africa(): Africa + { + return Africa::new($this->of); + } + + #[\NoDiscard] + public function america(): America + { + return America::new($this->of); + } + + #[\NoDiscard] + public function antartica(): Antartica + { + return Antartica::new($this->of); + } + + #[\NoDiscard] + public function arctic(): Arctic + { + return Arctic::new($this->of); + } + + #[\NoDiscard] + public function asia(): Asia + { + return Asia::new($this->of); + } + + #[\NoDiscard] + public function atlantic(): Atlantic + { + return Atlantic::new($this->of); + } + + #[\NoDiscard] + public function australia(): Australia + { + return Australia::new($this->of); + } + + #[\NoDiscard] + public function europe(): Europe + { + return Europe::new($this->of); + } + + #[\NoDiscard] + public function indian(): Indian + { + return Indian::new($this->of); + } + + #[\NoDiscard] + public function pacific(): Pacific + { + return Pacific::new($this->of); + } +} diff --git a/tests/Clock/FrozenTest.php b/tests/Clock/FrozenTest.php new file mode 100644 index 0000000..efc0d1b --- /dev/null +++ b/tests/Clock/FrozenTest.php @@ -0,0 +1,52 @@ +forAll(Point::any()) + ->prove(function($now) { + $this->assertSame( + $now->toString(), + Clock::frozen($now)->now()->toString(), + ); + }); + } + + public function testAtReturnWithTheSameTimezoneAsNow(): BlackBox\Proof + { + return $this + ->forAll( + Point::any(), + Point::any(), + ) + ->prove(function($now, $at) { + $clock = Clock::frozen($now); + + $point = $clock->at($at->format(Format::iso8601()), Format::iso8601())->match( + static fn($point) => $point, + static fn() => null, + ); + + $this->assertInstanceOf(PointInTimeInterface::class, $point); + $this->assertSame($now->offset()->toString(), $point->offset()->toString()); + }); + } +} diff --git a/tests/Clock/LiveTest.php b/tests/Clock/LiveTest.php new file mode 100644 index 0000000..d767bc5 --- /dev/null +++ b/tests/Clock/LiveTest.php @@ -0,0 +1,114 @@ +assertInstanceOf( + Point::class, + $now = Clock::live()->now(), + ); + $offset = \date('P'); + $offset = $offset === '+00:00' ? 'Z' : $offset; + $this->assertSame( + $offset, + $now->offset()->toString(), + ); + } + + public function testAt() + { + $this->assertInstanceOf( + Point::class, + $point = Clock::live()->at('2016-10-08T16:08:30+02:00', Format::iso8601())->match( + static fn($point) => $point, + static fn() => null, + ), + ); + $date = new \DateTimeImmutable('2016-10-08T16:08:30+02:00'); + $date = $date->setTimezone(new \DateTimeZone(\date('P'))); //system timezone + $this->assertSame( + $date->format('Y-m-d\TH:i:s.uP'), + $point->toString(), + ); + } + + public function testAtWithSpecificFormat() + { + $this->assertInstanceOf( + Point::class, + $point = Clock::live()->at('+02:00 2016-10-08 16:08:30', Format::of('P Y-m-d H:i:s'))->match( + static fn($point) => $point, + static fn() => null, + ), + ); + $date = new \DateTimeImmutable('2016-10-08T16:08:30+02:00'); + $date = $date->setTimezone(new \DateTimeZone(\date('P'))); //system timezone + $this->assertSame( + $date->format('Y-m-d\TH:i:s.uP'), + $point->toString(), + ); + } + + public function testAtWithDateNotOfExpectedFormat(): BlackBox\Proof + { + return $this + ->forAll(Set::strings()) + ->prove(function($date) { + $clock = Clock::live(); + + $this->assertNull($clock->at($date, Format::iso8601())->match( + static fn($point) => $point, + static fn() => null, + )); + }); + } + + public function testAtWithNullDate() + { + $clock = Clock::live(); + $this->assertNull($clock->at("\x00", Format::iso8601())->match( + static fn($point) => $point, + static fn() => null, + )); + } + + public function testDateCorrectlyRespectTheFormatGiven(): BlackBox\Proof + { + return $this + ->forAll( + Set::integers()->between(10, 99), + Set::integers()->between(1, 9), + Set::integers()->between(10, 28), + ) + ->prove(function($year, $month, $day) { + $date = "$year-0$month-$day"; + + $this->assertNull( + Clock::live() + ->at($date, Format::of('Y-m-d')) + ->match( + static fn($point) => $point, + static fn() => null, + ), + ); + }); + } +} diff --git a/tests/Clock/LoggerTest.php b/tests/Clock/LoggerTest.php new file mode 100644 index 0000000..ddc4d18 --- /dev/null +++ b/tests/Clock/LoggerTest.php @@ -0,0 +1,159 @@ +forAll(Point::any()) + ->prove(function($now) { + $concrete = Clock::frozen($now); + $logger = new class implements LoggerInterface { + use LoggerTrait; + + public array $logs = []; + + public function log($level, string|\Stringable $message, array $context = []): void + { + $this->logs[] = [$level, $message, $context]; + } + }; + + $clock = Clock::logger($concrete, $logger); + + $this->assertSame($now->toString(), $clock->now()->toString()); + $this->assertCount(1, $logger->logs); + $this->assertSame( + [ + 'debug', + 'Current time is {point}', + ['point' => $now->format(Format::iso8601())], + ], + $logger->logs[0], + ); + }); + } + + public function testAskedDateIsLogged(): BlackBox\Proof + { + return $this + ->forAll( + Point::any(), + ) + ->prove(function($point) { + $concrete = Clock::frozen($point); + $logger = new class implements LoggerInterface { + use LoggerTrait; + + public array $logs = []; + + public function log($level, string|\Stringable $message, array $context = []): void + { + $this->logs[] = [$level, $message, $context]; + } + }; + + $clock = Clock::logger($concrete, $logger); + + $this->assertSame( + $point->format(Format::iso8601()), + $clock->at($point->format(Format::iso8601()), Format::iso8601())->match( + static fn($found) => $found->format(Format::iso8601()), + static fn() => null, + ), + ); + $this->assertCount(1, $logger->logs); + $this->assertSame( + [ + 'debug', + 'Asked time {date} ({format}) resolved to {point}', + [ + 'date' => $point->format(Format::iso8601()), + 'format' => Format::iso8601()->toString(), + 'point' => $point->format(Format::iso8601()), + ], + ], + $logger->logs[0], + ); + }); + } + + public function testAskedDateWithSpecificFormatIsLogged(): BlackBox\Proof + { + return $this + ->forAll( + Point::any(), + Set::of( + Format::iso8601(), + Format::rfc1123(), + Format::rfc2822(), + Format::rss(), + Format::w3c(), + // problems with utc timezone as it sometimes return Z or + // GMT+0000 + // Format::cookie(), + // the year is not precise enough to allow to correctly + // parse any date with these formats + // Format::rfc1036(), + // Format::rfc822(), + // Format::rfc850(), + ), + ) + ->prove(function($point, $format) { + $concrete = Clock::frozen($point); + $logger = new class implements LoggerInterface { + use LoggerTrait; + + public array $logs = []; + + public function log($level, string|\Stringable $message, array $context = []): void + { + $this->logs[] = [$level, $message, $context]; + } + }; + + $clock = Clock::logger($concrete, $logger); + + $this->assertSame( + $point->format($format), + $clock->at($point->format($format), $format)->match( + static fn($point) => $point->format($format), + static fn() => null, + ), + ); + $this->assertCount(1, $logger->logs); + $this->assertSame( + [ + 'debug', + 'Asked time {date} ({format}) resolved to {point}', + [ + 'date' => $point->format($format), + 'format' => $format->toString(), + 'point' => $point->format(Format::iso8601()), + ], + ], + $logger->logs[0], + ); + }); + } +} diff --git a/tests/ClockTest.php b/tests/ClockTest.php new file mode 100644 index 0000000..9d89da2 --- /dev/null +++ b/tests/ClockTest.php @@ -0,0 +1,691 @@ +switch(static fn($timezones) => $timezones->utc()); + + $this->assertSame( + $live->now()->format(Format::of('Y-m-d H:i.P')), + $utc->now()->format(Format::of('Y-m-d H:i.P')), + ); + } + + #[DataProvider('zones')] + public function testSwitchTimezone($switch) + { + $live = Clock::live(); + $clock = $live->switch($switch); + + $this->assertIsString($clock->now()->format(Format::iso8601())); + } + + #[DataProvider('zones')] + public function testTimezoneAreAlwaysInBounds($switch) + { + $offset = Clock::live() + ->switch($switch) + ->now() + ->offset(); + + $this->assertGreaterThanOrEqual(-12, $offset->hours()); + $this->assertLessThanOrEqual(14, $offset->hours()); + $this->assertGreaterThanOrEqual(0, $offset->minutes()); + $this->assertLessThanOrEqual(59, $offset->minutes()); + } + + public function testTimezoneDifferences(): BlackBox\Proof + { + return $this + ->forAll( + self::genToSet(self::america())->exclude( + // timezone that collides with other continents + static fn($pair) => $pair[0] === 'danmarkshavn', + ), + Set::either( + self::genToSet(self::africa()), + self::genToSet(self::europe()), + self::genToSet(self::indian()), + self::genToSet(self::asia()), + self::genToSet(self::australia()), + ), + ) + ->prove(function($america, $other) { + $america = $america[1]; + $other = $other[1]; + + $america = Clock::live()->switch($america); + $other = Clock::live()->switch($other); + + $this->assertNotSame( + $america->now()->toString(), + $other->now()->toString(), + ); + }); + } + + public function testLoggerUseNewTimezone(): BlackBox\Proof + { + return $this + ->forAll( + self::genToSet(self::america())->filter( + // timezone that collides with other continents + static fn($pair) => $pair[0] !== 'danmarkshavn', + ), + Set::either( + self::genToSet(self::africa()), + self::genToSet(self::europe()), + self::genToSet(self::indian()), + self::genToSet(self::asia()), + self::genToSet(self::australia()), + ), + ) + ->prove(function($america, $other) { + $america = $america[1]; + $other = $other[1]; + $gather = new class implements LoggerInterface { + use LoggerTrait; + + public array $logs = []; + + public function log($level, string|\Stringable $message, array $context = []): void + { + $this->logs[] = $context; + } + }; + $america = Clock::logger(Clock::live(), $gather)->switch($america); + $new = $america->switch($other); + + $now1 = $america->now(); + $now2 = $new->now(); + + $this->assertSame( + $now1->format(Format::iso8601()), + $gather->logs[0]['point'], + ); + $this->assertSame( + $now2->format(Format::iso8601()), + $gather->logs[1]['point'], + ); + $this->assertNotSame( + $gather->logs[0], + $gather->logs[1], + $gather->logs[0]['point'], + ); + }); + } + + public function testAllDefaultFormatsAreValid() + { + $clock = Clock::live(); + $now = $clock->now(); + + $formats = [ + Format::cookie(), + Format::iso8601(), + Format::rfc1036(), + Format::rfc1123(), + Format::rfc2822(), + Format::rfc822(), + Format::rfc850(), + Format::rss(), + Format::w3c(), + ]; + + foreach ($formats as $format) { + $this->assertSame( + $now->format($format), + $clock->at($now->format($format), $format)->match( + static fn($point) => $point->format($format), + static fn() => null, + ), + ); + } + } + + public static function genToSet($zones): Set + { + $zones = \iterator_to_array($zones); + + return Set::of( + ...\array_map( + static fn($name, $switch) => [$name, $switch[0]], + \array_keys($zones), + \array_values($zones), + ), + ); + } + + public static function zones() + { + yield from self::africa(); + yield from self::america(); + yield from self::antartica(); + yield 'longyearbyen' => [static fn($timezones) => $timezones->arctic()->longyearbyen()]; + yield from self::asia(); + yield from self::atlantic(); + yield from self::australia(); + yield from self::europe(); + yield from self::indian(); + yield from self::pacific(); + } + + public static function africa() + { + yield 'lome' => [static fn($timezones) => $timezones->africa()->lome()]; + yield 'ceuta' => [static fn($timezones) => $timezones->africa()->ceuta()]; + yield 'elAaiun' => [static fn($timezones) => $timezones->africa()->elAaiun()]; + yield 'portoNovo' => [static fn($timezones) => $timezones->africa()->portoNovo()]; + yield 'djibouti' => [static fn($timezones) => $timezones->africa()->djibouti()]; + yield 'windhoek' => [static fn($timezones) => $timezones->africa()->windhoek()]; + yield 'algiers' => [static fn($timezones) => $timezones->africa()->algiers()]; + yield 'ouagadougou' => [static fn($timezones) => $timezones->africa()->ouagadougou()]; + yield 'bamako' => [static fn($timezones) => $timezones->africa()->bamako()]; + yield 'harare' => [static fn($timezones) => $timezones->africa()->harare()]; + yield 'bujumbura' => [static fn($timezones) => $timezones->africa()->bujumbura()]; + yield 'douala' => [static fn($timezones) => $timezones->africa()->douala()]; + yield 'brazzaville' => [static fn($timezones) => $timezones->africa()->brazzaville()]; + yield 'tripoli' => [static fn($timezones) => $timezones->africa()->tripoli()]; + yield 'casablanca' => [static fn($timezones) => $timezones->africa()->casablanca()]; + yield 'niamey' => [static fn($timezones) => $timezones->africa()->niamey()]; + yield 'mbabane' => [static fn($timezones) => $timezones->africa()->mbabane()]; + yield 'blantyre' => [static fn($timezones) => $timezones->africa()->blantyre()]; + yield 'conakry' => [static fn($timezones) => $timezones->africa()->conakry()]; + yield 'khartoum' => [static fn($timezones) => $timezones->africa()->khartoum()]; + yield 'luanda' => [static fn($timezones) => $timezones->africa()->luanda()]; + yield 'libreville' => [static fn($timezones) => $timezones->africa()->libreville()]; + yield 'maseru' => [static fn($timezones) => $timezones->africa()->maseru()]; + yield 'lusaka' => [static fn($timezones) => $timezones->africa()->lusaka()]; + yield 'darEsSalaam' => [static fn($timezones) => $timezones->africa()->darEsSalaam()]; + yield 'nairobi' => [static fn($timezones) => $timezones->africa()->nairobi()]; + yield 'banjul' => [static fn($timezones) => $timezones->africa()->banjul()]; + yield 'bissau' => [static fn($timezones) => $timezones->africa()->bissau()]; + yield 'nouakchott' => [static fn($timezones) => $timezones->africa()->nouakchott()]; + yield 'johannesburg' => [static fn($timezones) => $timezones->africa()->johannesburg()]; + yield 'timbuktu' => [static fn($timezones) => $timezones->africa()->timbuktu()]; + yield 'saoTome' => [static fn($timezones) => $timezones->africa()->saoTome()]; + yield 'freetown' => [static fn($timezones) => $timezones->africa()->freetown()]; + yield 'kampala' => [static fn($timezones) => $timezones->africa()->kampala()]; + yield 'dakar' => [static fn($timezones) => $timezones->africa()->dakar()]; + yield 'lagos' => [static fn($timezones) => $timezones->africa()->lagos()]; + yield 'cairo' => [static fn($timezones) => $timezones->africa()->cairo()]; + yield 'mogadishu' => [static fn($timezones) => $timezones->africa()->mogadishu()]; + yield 'gaborone' => [static fn($timezones) => $timezones->africa()->gaborone()]; + yield 'tunis' => [static fn($timezones) => $timezones->africa()->tunis()]; + yield 'kigali' => [static fn($timezones) => $timezones->africa()->kigali()]; + yield 'malabo' => [static fn($timezones) => $timezones->africa()->malabo()]; + yield 'abidjan' => [static fn($timezones) => $timezones->africa()->abidjan()]; + yield 'accra' => [static fn($timezones) => $timezones->africa()->accra()]; + yield 'asmera' => [static fn($timezones) => $timezones->africa()->asmera()]; + yield 'ndjamena' => [static fn($timezones) => $timezones->africa()->ndjamena()]; + yield 'lubumbashi' => [static fn($timezones) => $timezones->africa()->lubumbashi()]; + yield 'juba' => [static fn($timezones) => $timezones->africa()->juba()]; + yield 'monrovia' => [static fn($timezones) => $timezones->africa()->monrovia()]; + yield 'maputo' => [static fn($timezones) => $timezones->africa()->maputo()]; + yield 'kinshasa' => [static fn($timezones) => $timezones->africa()->kinshasa()]; + yield 'asmara' => [static fn($timezones) => $timezones->africa()->asmara()]; + yield 'bangui' => [static fn($timezones) => $timezones->africa()->bangui()]; + yield 'addisAbaba' => [static fn($timezones) => $timezones->africa()->addisAbaba()]; + } + + public static function america() + { + yield 'rioGallegos' => [static fn($timezones) => $timezones->america()->argentina()->rioGallegos()]; + yield 'mendoza' => [static fn($timezones) => $timezones->america()->argentina()->mendoza()]; + yield 'buenosAires' => [static fn($timezones) => $timezones->america()->argentina()->buenosAires()]; + yield 'ushuaia' => [static fn($timezones) => $timezones->america()->argentina()->ushuaia()]; + yield 'sanJuan' => [static fn($timezones) => $timezones->america()->argentina()->sanJuan()]; + yield 'laRioja' => [static fn($timezones) => $timezones->america()->argentina()->laRioja()]; + yield 'salta' => [static fn($timezones) => $timezones->america()->argentina()->salta()]; + yield 'sanLuis' => [static fn($timezones) => $timezones->america()->argentina()->sanLuis()]; + yield 'jujuy' => [static fn($timezones) => $timezones->america()->argentina()->jujuy()]; + yield 'tucuman' => [static fn($timezones) => $timezones->america()->argentina()->tucuman()]; + yield 'comodRivadavia' => [static fn($timezones) => $timezones->america()->argentina()->comodRivadavia()]; + yield 'argantina catamarca' => [static fn($timezones) => $timezones->america()->argentina()->catamarca()]; + yield 'cordoba' => [static fn($timezones) => $timezones->america()->argentina()->cordoba()]; + yield 'vincennes' => [static fn($timezones) => $timezones->america()->indiana()->vincennes()]; + yield 'marengo' => [static fn($timezones) => $timezones->america()->indiana()->marengo()]; + yield 'tellCity' => [static fn($timezones) => $timezones->america()->indiana()->tellCity()]; + yield 'knox' => [static fn($timezones) => $timezones->america()->indiana()->knox()]; + yield 'vevay' => [static fn($timezones) => $timezones->america()->indiana()->vevay()]; + yield 'indianapolis' => [static fn($timezones) => $timezones->america()->indiana()->indianapolis()]; + yield 'petersburg' => [static fn($timezones) => $timezones->america()->indiana()->petersburg()]; + yield 'winamac' => [static fn($timezones) => $timezones->america()->indiana()->winamac()]; + yield 'beulah' => [static fn($timezones) => $timezones->america()->northDakota()->beulah()]; + yield 'newSalem' => [static fn($timezones) => $timezones->america()->northDakota()->newSalem()]; + yield 'center' => [static fn($timezones) => $timezones->america()->northDakota()->center()]; + yield 'montreal' => [static fn($timezones) => $timezones->america()->montreal()]; + yield 'guatemala' => [static fn($timezones) => $timezones->america()->guatemala()]; + yield 'boaVista' => [static fn($timezones) => $timezones->america()->boaVista()]; + yield 'portoAcre' => [static fn($timezones) => $timezones->america()->portoAcre()]; + yield 'winnipeg' => [static fn($timezones) => $timezones->america()->winnipeg()]; + yield 'santiago' => [static fn($timezones) => $timezones->america()->santiago()]; + yield 'virgin' => [static fn($timezones) => $timezones->america()->virgin()]; + yield 'moncton' => [static fn($timezones) => $timezones->america()->moncton()]; + yield 'noronha' => [static fn($timezones) => $timezones->america()->noronha()]; + yield 'recife' => [static fn($timezones) => $timezones->america()->recife()]; + yield 'saintKitts' => [static fn($timezones) => $timezones->america()->saintKitts()]; + yield 'rankinInlet' => [static fn($timezones) => $timezones->america()->rankinInlet()]; + yield 'jamaica' => [static fn($timezones) => $timezones->america()->jamaica()]; + yield 'lima' => [static fn($timezones) => $timezones->america()->lima()]; + yield 'rosario' => [static fn($timezones) => $timezones->america()->rosario()]; + yield 'cambridgeBay' => [static fn($timezones) => $timezones->america()->cambridgeBay()]; + yield 'coralHarbour' => [static fn($timezones) => $timezones->america()->coralHarbour()]; + yield 'fortWayne' => [static fn($timezones) => $timezones->america()->fortWayne()]; + yield 'nassau' => [static fn($timezones) => $timezones->america()->nassau()]; + yield 'mazatlan' => [static fn($timezones) => $timezones->america()->mazatlan()]; + yield 'grandTurk' => [static fn($timezones) => $timezones->america()->grandTurk()]; + yield 'merida' => [static fn($timezones) => $timezones->america()->merida()]; + yield 'ensenada' => [static fn($timezones) => $timezones->america()->ensenada()]; + yield 'rainyRiver' => [static fn($timezones) => $timezones->america()->rainyRiver()]; + yield 'bahiaBanderas' => [static fn($timezones) => $timezones->america()->bahiaBanderas()]; + yield 'guadeloupe' => [static fn($timezones) => $timezones->america()->guadeloupe()]; + yield 'cuiaba' => [static fn($timezones) => $timezones->america()->cuiaba()]; + yield 'scoresbysund' => [static fn($timezones) => $timezones->america()->scoresbysund()]; + yield 'maceio' => [static fn($timezones) => $timezones->america()->maceio()]; + yield 'curacao' => [static fn($timezones) => $timezones->america()->curacao()]; + yield 'aruba' => [static fn($timezones) => $timezones->america()->aruba()]; + yield 'monterrey' => [static fn($timezones) => $timezones->america()->monterrey()]; + yield 'hermosillo' => [static fn($timezones) => $timezones->america()->hermosillo()]; + yield 'guayaquil' => [static fn($timezones) => $timezones->america()->guayaquil()]; + yield 'managua' => [static fn($timezones) => $timezones->america()->managua()]; + yield 'matamoros' => [static fn($timezones) => $timezones->america()->matamoros()]; + yield 'losAngeles' => [static fn($timezones) => $timezones->america()->losAngeles()]; + yield 'tegucigalpa' => [static fn($timezones) => $timezones->america()->tegucigalpa()]; + yield 'monticello' => [static fn($timezones) => $timezones->america()->monticello()]; + yield 'nome' => [static fn($timezones) => $timezones->america()->nome()]; + yield 'montevideo' => [static fn($timezones) => $timezones->america()->montevideo()]; + yield 'gooseBay' => [static fn($timezones) => $timezones->america()->gooseBay()]; + yield 'boise' => [static fn($timezones) => $timezones->america()->boise()]; + yield 'belem' => [static fn($timezones) => $timezones->america()->belem()]; + yield 'atikokan' => [static fn($timezones) => $timezones->america()->atikokan()]; + yield 'swiftCurrent' => [static fn($timezones) => $timezones->america()->swiftCurrent()]; + yield 'detroit' => [static fn($timezones) => $timezones->america()->detroit()]; + yield 'laPaz' => [static fn($timezones) => $timezones->america()->laPaz()]; + yield 'chicago' => [static fn($timezones) => $timezones->america()->chicago()]; + yield 'creston' => [static fn($timezones) => $timezones->america()->creston()]; + yield 'nipigon' => [static fn($timezones) => $timezones->america()->nipigon()]; + yield 'costaRica' => [static fn($timezones) => $timezones->america()->costaRica()]; + yield 'halifax' => [static fn($timezones) => $timezones->america()->halifax()]; + yield 'yellowknife' => [static fn($timezones) => $timezones->america()->yellowknife()]; + yield 'puertoRico' => [static fn($timezones) => $timezones->america()->puertoRico()]; + yield 'edmonton' => [static fn($timezones) => $timezones->america()->edmonton()]; + yield 'mexicoCity' => [static fn($timezones) => $timezones->america()->mexicoCity()]; + yield 'saoPaulo' => [static fn($timezones) => $timezones->america()->saoPaulo()]; + yield 'yakutat' => [static fn($timezones) => $timezones->america()->yakutat()]; + yield 'saintThomas' => [static fn($timezones) => $timezones->america()->saintThomas()]; + yield 'chihuahua' => [static fn($timezones) => $timezones->america()->chihuahua()]; + yield 'grenada' => [static fn($timezones) => $timezones->america()->grenada()]; + yield 'elSalvador' => [static fn($timezones) => $timezones->america()->elSalvador()]; + yield 'santoDomingo' => [static fn($timezones) => $timezones->america()->santoDomingo()]; + yield 'montserrat' => [static fn($timezones) => $timezones->america()->montserrat()]; + yield 'portoVelho' => [static fn($timezones) => $timezones->america()->portoVelho()]; + yield 'panama' => [static fn($timezones) => $timezones->america()->panama()]; + yield 'antigua' => [static fn($timezones) => $timezones->america()->antigua()]; + yield 'santarem' => [static fn($timezones) => $timezones->america()->santarem()]; + yield 'dawson' => [static fn($timezones) => $timezones->america()->dawson()]; + yield 'saintBarthelemy' => [static fn($timezones) => $timezones->america()->saintBarthelemy()]; + yield 'iqaluit' => [static fn($timezones) => $timezones->america()->iqaluit()]; + yield 'eirunepe' => [static fn($timezones) => $timezones->america()->eirunepe()]; + yield 'inuvik' => [static fn($timezones) => $timezones->america()->inuvik()]; + yield 'anguilla' => [static fn($timezones) => $timezones->america()->anguilla()]; + yield 'portOfSpain' => [static fn($timezones) => $timezones->america()->portOfSpain()]; + yield 'araguaina' => [static fn($timezones) => $timezones->america()->araguaina()]; + yield 'guyana' => [static fn($timezones) => $timezones->america()->guyana()]; + yield 'fortaleza' => [static fn($timezones) => $timezones->america()->fortaleza()]; + yield 'blancSablon' => [static fn($timezones) => $timezones->america()->blancSablon()]; + yield 'juneau' => [static fn($timezones) => $timezones->america()->juneau()]; + yield 'cayman' => [static fn($timezones) => $timezones->america()->cayman()]; + yield 'menominee' => [static fn($timezones) => $timezones->america()->menominee()]; + yield 'cayenne' => [static fn($timezones) => $timezones->america()->cayenne()]; + yield 'pangnirtung' => [static fn($timezones) => $timezones->america()->pangnirtung()]; + yield 'metlakatla' => [static fn($timezones) => $timezones->america()->metlakatla()]; + yield 'asuncion' => [static fn($timezones) => $timezones->america()->asuncion()]; + yield 'saintLucia' => [static fn($timezones) => $timezones->america()->saintLucia()]; + yield 'saintVincent' => [static fn($timezones) => $timezones->america()->saintVincent()]; + yield 'martinique' => [static fn($timezones) => $timezones->america()->martinique()]; + yield 'kralendijk' => [static fn($timezones) => $timezones->america()->kralendijk()]; + yield 'newYork' => [static fn($timezones) => $timezones->america()->newYork()]; + yield 'vancouver' => [static fn($timezones) => $timezones->america()->vancouver()]; + yield 'bogota' => [static fn($timezones) => $timezones->america()->bogota()]; + yield 'dominica' => [static fn($timezones) => $timezones->america()->dominica()]; + yield 'danmarkshavn' => [static fn($timezones) => $timezones->america()->danmarkshavn()]; + yield 'anchorage' => [static fn($timezones) => $timezones->america()->anchorage()]; + yield 'marigot' => [static fn($timezones) => $timezones->america()->marigot()]; + yield 'rioBranco' => [static fn($timezones) => $timezones->america()->rioBranco()]; + yield 'paramaribo' => [static fn($timezones) => $timezones->america()->paramaribo()]; + yield 'caracas' => [static fn($timezones) => $timezones->america()->caracas()]; + yield 'resolute' => [static fn($timezones) => $timezones->america()->resolute()]; + yield 'godthab' => [static fn($timezones) => $timezones->america()->godthab()]; + yield 'catamarca' => [static fn($timezones) => $timezones->america()->catamarca()]; + yield 'glaceBay' => [static fn($timezones) => $timezones->america()->glaceBay()]; + yield 'regina' => [static fn($timezones) => $timezones->america()->regina()]; + yield 'toronto' => [static fn($timezones) => $timezones->america()->toronto()]; + yield 'barbados' => [static fn($timezones) => $timezones->america()->barbados()]; + yield 'santaIsabel' => [static fn($timezones) => $timezones->america()->santaIsabel()]; + yield 'miquelon' => [static fn($timezones) => $timezones->america()->miquelon()]; + yield 'havana' => [static fn($timezones) => $timezones->america()->havana()]; + yield 'ojinaga' => [static fn($timezones) => $timezones->america()->ojinaga()]; + yield 'denver' => [static fn($timezones) => $timezones->america()->denver()]; + yield 'cancun' => [static fn($timezones) => $timezones->america()->cancun()]; + yield 'thunderBay' => [static fn($timezones) => $timezones->america()->thunderBay()]; + yield 'adak' => [static fn($timezones) => $timezones->america()->adak()]; + yield 'saintJohns' => [static fn($timezones) => $timezones->america()->saintJohns()]; + yield 'portAuPrince' => [static fn($timezones) => $timezones->america()->portAuPrince()]; + yield 'whitehorse' => [static fn($timezones) => $timezones->america()->whitehorse()]; + yield 'louisville' => [static fn($timezones) => $timezones->america()->louisville()]; + yield 'manaus' => [static fn($timezones) => $timezones->america()->manaus()]; + yield 'lowerPrinces' => [static fn($timezones) => $timezones->america()->lowerPrinces()]; + yield 'sitka' => [static fn($timezones) => $timezones->america()->sitka()]; + yield 'thule' => [static fn($timezones) => $timezones->america()->thule()]; + yield 'campoGrande' => [static fn($timezones) => $timezones->america()->campoGrande()]; + yield 'phoenix' => [static fn($timezones) => $timezones->america()->phoenix()]; + yield 'shiprock' => [static fn($timezones) => $timezones->america()->shiprock()]; + yield 'bahia' => [static fn($timezones) => $timezones->america()->bahia()]; + yield 'tortola' => [static fn($timezones) => $timezones->america()->tortola()]; + yield 'dawsonCreek' => [static fn($timezones) => $timezones->america()->dawsonCreek()]; + yield 'tijuana' => [static fn($timezones) => $timezones->america()->tijuana()]; + yield 'belize' => [static fn($timezones) => $timezones->america()->belize()]; + yield 'atka' => [static fn($timezones) => $timezones->america()->atka()]; + } + + public static function antartica() + { + yield 'davis' => [static fn($timezones) => $timezones->antartica()->davis()]; + yield 'palmer' => [static fn($timezones) => $timezones->antartica()->palmer()]; + yield 'syowa' => [static fn($timezones) => $timezones->antartica()->syowa()]; + yield 'casey' => [static fn($timezones) => $timezones->antartica()->casey()]; + yield 'troll' => [static fn($timezones) => $timezones->antartica()->troll()]; + yield 'mcMurdo' => [static fn($timezones) => $timezones->antartica()->mcMurdo()]; + yield 'vostok' => [static fn($timezones) => $timezones->antartica()->vostok()]; + yield 'rothera' => [static fn($timezones) => $timezones->antartica()->rothera()]; + yield 'mawson' => [static fn($timezones) => $timezones->antartica()->mawson()]; + yield 'macquarie' => [static fn($timezones) => $timezones->antartica()->macquarie()]; + yield 'southPole' => [static fn($timezones) => $timezones->antartica()->southPole()]; + yield 'dumontDUrville' => [static fn($timezones) => $timezones->antartica()->dumontDUrville()]; + } + + public static function asia() + { + yield 'manila' => [static fn($timezones) => $timezones->asia()->manila()]; + yield 'baghdad' => [static fn($timezones) => $timezones->asia()->baghdad()]; + yield 'ulaanbaatar' => [static fn($timezones) => $timezones->asia()->ulaanbaatar()]; + yield 'almaty' => [static fn($timezones) => $timezones->asia()->almaty()]; + yield 'samarkand' => [static fn($timezones) => $timezones->asia()->samarkand()]; + yield 'ustNera' => [static fn($timezones) => $timezones->asia()->ustNera()]; + yield 'pontianak' => [static fn($timezones) => $timezones->asia()->pontianak()]; + yield 'tehran' => [static fn($timezones) => $timezones->asia()->tehran()]; + yield 'saigon' => [static fn($timezones) => $timezones->asia()->saigon()]; + yield 'krasnoyarsk' => [static fn($timezones) => $timezones->asia()->krasnoyarsk()]; + yield 'hebron' => [static fn($timezones) => $timezones->asia()->hebron()]; + yield 'kuching' => [static fn($timezones) => $timezones->asia()->kuching()]; + yield 'katmandu' => [static fn($timezones) => $timezones->asia()->katmandu()]; + yield 'shanghai' => [static fn($timezones) => $timezones->asia()->shanghai()]; + yield 'calcutta' => [static fn($timezones) => $timezones->asia()->calcutta()]; + yield 'jayapura' => [static fn($timezones) => $timezones->asia()->jayapura()]; + yield 'muscat' => [static fn($timezones) => $timezones->asia()->muscat()]; + yield 'omsk' => [static fn($timezones) => $timezones->asia()->omsk()]; + yield 'aqtau' => [static fn($timezones) => $timezones->asia()->aqtau()]; + yield 'khandyga' => [static fn($timezones) => $timezones->asia()->khandyga()]; + yield 'riyadh' => [static fn($timezones) => $timezones->asia()->riyadh()]; + yield 'bangkok' => [static fn($timezones) => $timezones->asia()->bangkok()]; + yield 'thimphu' => [static fn($timezones) => $timezones->asia()->thimphu()]; + yield 'aden' => [static fn($timezones) => $timezones->asia()->aden()]; + yield 'yekaterinburg' => [static fn($timezones) => $timezones->asia()->yekaterinburg()]; + yield 'oral' => [static fn($timezones) => $timezones->asia()->oral()]; + yield 'novokuznetsk' => [static fn($timezones) => $timezones->asia()->novokuznetsk()]; + yield 'bishkek' => [static fn($timezones) => $timezones->asia()->bishkek()]; + yield 'macau' => [static fn($timezones) => $timezones->asia()->macau()]; + yield 'qyzylorda' => [static fn($timezones) => $timezones->asia()->qyzylorda()]; + yield 'seoul' => [static fn($timezones) => $timezones->asia()->seoul()]; + yield 'irkutsk' => [static fn($timezones) => $timezones->asia()->irkutsk()]; + yield 'aqtobe' => [static fn($timezones) => $timezones->asia()->aqtobe()]; + yield 'chongqing' => [static fn($timezones) => $timezones->asia()->chongqing()]; + yield 'kabul' => [static fn($timezones) => $timezones->asia()->kabul()]; + yield 'thimbu' => [static fn($timezones) => $timezones->asia()->thimbu()]; + yield 'karachi' => [static fn($timezones) => $timezones->asia()->karachi()]; + yield 'jakarta' => [static fn($timezones) => $timezones->asia()->jakarta()]; + yield 'harbin' => [static fn($timezones) => $timezones->asia()->harbin()]; + yield 'novosibirsk' => [static fn($timezones) => $timezones->asia()->novosibirsk()]; + yield 'dili' => [static fn($timezones) => $timezones->asia()->dili()]; + yield 'colombo' => [static fn($timezones) => $timezones->asia()->colombo()]; + yield 'ashkhabad' => [static fn($timezones) => $timezones->asia()->ashkhabad()]; + yield 'dacca' => [static fn($timezones) => $timezones->asia()->dacca()]; + yield 'ashgabat' => [static fn($timezones) => $timezones->asia()->ashgabat()]; + yield 'ujungPandang' => [static fn($timezones) => $timezones->asia()->ujungPandang()]; + yield 'qatar' => [static fn($timezones) => $timezones->asia()->qatar()]; + yield 'tokyo' => [static fn($timezones) => $timezones->asia()->tokyo()]; + yield 'macao' => [static fn($timezones) => $timezones->asia()->macao()]; + yield 'tashkent' => [static fn($timezones) => $timezones->asia()->tashkent()]; + yield 'baku' => [static fn($timezones) => $timezones->asia()->baku()]; + yield 'pyongyang' => [static fn($timezones) => $timezones->asia()->pyongyang()]; + yield 'tbilisi' => [static fn($timezones) => $timezones->asia()->tbilisi()]; + yield 'amman' => [static fn($timezones) => $timezones->asia()->amman()]; + yield 'vladivostok' => [static fn($timezones) => $timezones->asia()->vladivostok()]; + yield 'damascus' => [static fn($timezones) => $timezones->asia()->damascus()]; + yield 'bahrain' => [static fn($timezones) => $timezones->asia()->bahrain()]; + yield 'vientiane' => [static fn($timezones) => $timezones->asia()->vientiane()]; + yield 'hovd' => [static fn($timezones) => $timezones->asia()->hovd()]; + yield 'kuwait' => [static fn($timezones) => $timezones->asia()->kuwait()]; + yield 'magadan' => [static fn($timezones) => $timezones->asia()->magadan()]; + yield 'ulanBator' => [static fn($timezones) => $timezones->asia()->ulanBator()]; + yield 'asia nicosia' => [static fn($timezones) => $timezones->asia()->nicosia()]; + yield 'telAviv' => [static fn($timezones) => $timezones->asia()->telAviv()]; + yield 'choibalsan' => [static fn($timezones) => $timezones->asia()->choibalsan()]; + yield 'brunei' => [static fn($timezones) => $timezones->asia()->brunei()]; + yield 'kualaLumpur' => [static fn($timezones) => $timezones->asia()->kualaLumpur()]; + yield 'kathmandu' => [static fn($timezones) => $timezones->asia()->kathmandu()]; + yield 'srednekolymsk' => [static fn($timezones) => $timezones->asia()->srednekolymsk()]; + yield 'dubai' => [static fn($timezones) => $timezones->asia()->dubai()]; + yield 'yakutsk' => [static fn($timezones) => $timezones->asia()->yakutsk()]; + yield 'beirut' => [static fn($timezones) => $timezones->asia()->beirut()]; + yield 'gaza' => [static fn($timezones) => $timezones->asia()->gaza()]; + yield 'singapore' => [static fn($timezones) => $timezones->asia()->singapore()]; + yield 'rangoon' => [static fn($timezones) => $timezones->asia()->rangoon()]; + yield 'sakhalin' => [static fn($timezones) => $timezones->asia()->sakhalin()]; + yield 'phnomPenh' => [static fn($timezones) => $timezones->asia()->phnomPenh()]; + yield 'kamchatka' => [static fn($timezones) => $timezones->asia()->kamchatka()]; + yield 'yerevan' => [static fn($timezones) => $timezones->asia()->yerevan()]; + yield 'chungking' => [static fn($timezones) => $timezones->asia()->chungking()]; + yield 'hoChiMinh' => [static fn($timezones) => $timezones->asia()->hoChiMinh()]; + yield 'chita' => [static fn($timezones) => $timezones->asia()->chita()]; + yield 'asia istanbul' => [static fn($timezones) => $timezones->asia()->istanbul()]; + yield 'hongKong' => [static fn($timezones) => $timezones->asia()->hongKong()]; + yield 'dhaka' => [static fn($timezones) => $timezones->asia()->dhaka()]; + yield 'jerusalem' => [static fn($timezones) => $timezones->asia()->jerusalem()]; + yield 'makassar' => [static fn($timezones) => $timezones->asia()->makassar()]; + yield 'kolkata' => [static fn($timezones) => $timezones->asia()->kolkata()]; + yield 'taipei' => [static fn($timezones) => $timezones->asia()->taipei()]; + yield 'dushanbe' => [static fn($timezones) => $timezones->asia()->dushanbe()]; + yield 'anadyr' => [static fn($timezones) => $timezones->asia()->anadyr()]; + } + + public static function atlantic() + { + yield 'faroe' => [static fn($timezones) => $timezones->atlantic()->faroe()]; + yield 'southGeorgia' => [static fn($timezones) => $timezones->atlantic()->southGeorgia()]; + yield 'capeVerde' => [static fn($timezones) => $timezones->atlantic()->capeVerde()]; + yield 'faeroe' => [static fn($timezones) => $timezones->atlantic()->faeroe()]; + yield 'bermuda' => [static fn($timezones) => $timezones->atlantic()->bermuda()]; + yield 'janMayen' => [static fn($timezones) => $timezones->atlantic()->janMayen()]; + yield 'reykjavik' => [static fn($timezones) => $timezones->atlantic()->reykjavik()]; + yield 'saintHelena' => [static fn($timezones) => $timezones->atlantic()->saintHelena()]; + yield 'canary' => [static fn($timezones) => $timezones->atlantic()->canary()]; + yield 'madeira' => [static fn($timezones) => $timezones->atlantic()->madeira()]; + yield 'azores' => [static fn($timezones) => $timezones->atlantic()->azores()]; + yield 'stanley' => [static fn($timezones) => $timezones->atlantic()->stanley()]; + } + + public static function australia() + { + yield 'lindeman' => [static fn($timezones) => $timezones->australia()->lindeman()]; + yield 'currie' => [static fn($timezones) => $timezones->australia()->currie()]; + yield 'victoria' => [static fn($timezones) => $timezones->australia()->victoria()]; + yield 'adelaide' => [static fn($timezones) => $timezones->australia()->adelaide()]; + yield 'perth' => [static fn($timezones) => $timezones->australia()->perth()]; + yield 'brisbane' => [static fn($timezones) => $timezones->australia()->brisbane()]; + yield 'west' => [static fn($timezones) => $timezones->australia()->west()]; + yield 'australianCapitalTerritory' => [static fn($timezones) => $timezones->australia()->australianCapitalTerritory()]; + yield 'north' => [static fn($timezones) => $timezones->australia()->north()]; + yield 'eucla' => [static fn($timezones) => $timezones->australia()->eucla()]; + yield 'lordeHoweIsland' => [static fn($timezones) => $timezones->australia()->lordeHoweIsland()]; + yield 'newSouthWales' => [static fn($timezones) => $timezones->australia()->newSouthWales()]; + yield 'queensland' => [static fn($timezones) => $timezones->australia()->queensland()]; + yield 'south' => [static fn($timezones) => $timezones->australia()->south()]; + yield 'melbourne' => [static fn($timezones) => $timezones->australia()->melbourne()]; + yield 'yancowinna' => [static fn($timezones) => $timezones->australia()->yancowinna()]; + yield 'canberra' => [static fn($timezones) => $timezones->australia()->canberra()]; + yield 'sydney' => [static fn($timezones) => $timezones->australia()->sydney()]; + yield 'darwin' => [static fn($timezones) => $timezones->australia()->darwin()]; + yield 'hobart' => [static fn($timezones) => $timezones->australia()->hobart()]; + yield 'brokenHill' => [static fn($timezones) => $timezones->australia()->brokenHill()]; + yield 'tasmania' => [static fn($timezones) => $timezones->australia()->tasmania()]; + } + + public static function europe() + { + yield 'uzhgorod' => [static fn($timezones) => $timezones->europe()->uzhgorod()]; + yield 'riga' => [static fn($timezones) => $timezones->europe()->riga()]; + yield 'paris' => [static fn($timezones) => $timezones->europe()->paris()]; + yield 'guernsey' => [static fn($timezones) => $timezones->europe()->guernsey()]; + yield 'samara' => [static fn($timezones) => $timezones->europe()->samara()]; + yield 'athens' => [static fn($timezones) => $timezones->europe()->athens()]; + yield 'tirane' => [static fn($timezones) => $timezones->europe()->tirane()]; + yield 'london' => [static fn($timezones) => $timezones->europe()->london()]; + yield 'helsinki' => [static fn($timezones) => $timezones->europe()->helsinki()]; + yield 'oslo' => [static fn($timezones) => $timezones->europe()->oslo()]; + yield 'podgorica' => [static fn($timezones) => $timezones->europe()->podgorica()]; + yield 'minsk' => [static fn($timezones) => $timezones->europe()->minsk()]; + yield 'monaco' => [static fn($timezones) => $timezones->europe()->monaco()]; + yield 'lisbon' => [static fn($timezones) => $timezones->europe()->lisbon()]; + yield 'tallinn' => [static fn($timezones) => $timezones->europe()->tallinn()]; + yield 'berlin' => [static fn($timezones) => $timezones->europe()->berlin()]; + yield 'gibraltar' => [static fn($timezones) => $timezones->europe()->gibraltar()]; + yield 'prague' => [static fn($timezones) => $timezones->europe()->prague()]; + yield 'stockholm' => [static fn($timezones) => $timezones->europe()->stockholm()]; + yield 'moscow' => [static fn($timezones) => $timezones->europe()->moscow()]; + yield 'bucharest' => [static fn($timezones) => $timezones->europe()->bucharest()]; + yield 'andorra' => [static fn($timezones) => $timezones->europe()->andorra()]; + yield 'vilnius' => [static fn($timezones) => $timezones->europe()->vilnius()]; + yield 'rome' => [static fn($timezones) => $timezones->europe()->rome()]; + yield 'kiev' => [static fn($timezones) => $timezones->europe()->kiev()]; + yield 'copenhagen' => [static fn($timezones) => $timezones->europe()->copenhagen()]; + yield 'belgrade' => [static fn($timezones) => $timezones->europe()->belgrade()]; + yield 'isleOfMan' => [static fn($timezones) => $timezones->europe()->isleOfMan()]; + yield 'budapest' => [static fn($timezones) => $timezones->europe()->budapest()]; + yield 'tiraspol' => [static fn($timezones) => $timezones->europe()->tiraspol()]; + yield 'vaduz' => [static fn($timezones) => $timezones->europe()->vaduz()]; + yield 'sarajevo' => [static fn($timezones) => $timezones->europe()->sarajevo()]; + yield 'amsterdam' => [static fn($timezones) => $timezones->europe()->amsterdam()]; + yield 'mariehamn' => [static fn($timezones) => $timezones->europe()->mariehamn()]; + yield 'skopje' => [static fn($timezones) => $timezones->europe()->skopje()]; + yield 'kaliningrad' => [static fn($timezones) => $timezones->europe()->kaliningrad()]; + yield 'bratislava' => [static fn($timezones) => $timezones->europe()->bratislava()]; + yield 'sanMarino' => [static fn($timezones) => $timezones->europe()->sanMarino()]; + yield 'busingen' => [static fn($timezones) => $timezones->europe()->busingen()]; + yield 'zaporozhye' => [static fn($timezones) => $timezones->europe()->zaporozhye()]; + yield 'chisinau' => [static fn($timezones) => $timezones->europe()->chisinau()]; + yield 'brussels' => [static fn($timezones) => $timezones->europe()->brussels()]; + yield 'luxembourg' => [static fn($timezones) => $timezones->europe()->luxembourg()]; + yield 'belfast' => [static fn($timezones) => $timezones->europe()->belfast()]; + yield 'vienna' => [static fn($timezones) => $timezones->europe()->vienna()]; + yield 'ljubljana' => [static fn($timezones) => $timezones->europe()->ljubljana()]; + yield 'simferopol' => [static fn($timezones) => $timezones->europe()->simferopol()]; + yield 'dublin' => [static fn($timezones) => $timezones->europe()->dublin()]; + yield 'europe nicosia' => [static fn($timezones) => $timezones->europe()->nicosia()]; + yield 'zagreb' => [static fn($timezones) => $timezones->europe()->zagreb()]; + yield 'jersey' => [static fn($timezones) => $timezones->europe()->jersey()]; + yield 'madrid' => [static fn($timezones) => $timezones->europe()->madrid()]; + yield 'vatican' => [static fn($timezones) => $timezones->europe()->vatican()]; + yield 'europe istanbul' => [static fn($timezones) => $timezones->europe()->istanbul()]; + yield 'zurich' => [static fn($timezones) => $timezones->europe()->zurich()]; + yield 'sofia' => [static fn($timezones) => $timezones->europe()->sofia()]; + yield 'volgograd' => [static fn($timezones) => $timezones->europe()->volgograd()]; + yield 'malta' => [static fn($timezones) => $timezones->europe()->malta()]; + yield 'warsaw' => [static fn($timezones) => $timezones->europe()->warsaw()]; + } + + public static function indian() + { + yield 'cocos' => [static fn($timezones) => $timezones->indian()->cocos()]; + yield 'antananarivo' => [static fn($timezones) => $timezones->indian()->antananarivo()]; + yield 'reunion' => [static fn($timezones) => $timezones->indian()->reunion()]; + yield 'chagos' => [static fn($timezones) => $timezones->indian()->chagos()]; + yield 'comoro' => [static fn($timezones) => $timezones->indian()->comoro()]; + yield 'mayotte' => [static fn($timezones) => $timezones->indian()->mayotte()]; + yield 'maldives' => [static fn($timezones) => $timezones->indian()->maldives()]; + yield 'mauritius' => [static fn($timezones) => $timezones->indian()->mauritius()]; + yield 'mahe' => [static fn($timezones) => $timezones->indian()->mahe()]; + yield 'kerguelen' => [static fn($timezones) => $timezones->indian()->kerguelen()]; + yield 'christmas' => [static fn($timezones) => $timezones->indian()->christmas()]; + } + + public static function pacific() + { + yield 'kosrae' => [static fn($timezones) => $timezones->pacific()->kosrae()]; + yield 'enderbury' => [static fn($timezones) => $timezones->pacific()->enderbury()]; + yield 'apia' => [static fn($timezones) => $timezones->pacific()->apia()]; + yield 'noumea' => [static fn($timezones) => $timezones->pacific()->noumea()]; + yield 'chatham' => [static fn($timezones) => $timezones->pacific()->chatham()]; + yield 'wake' => [static fn($timezones) => $timezones->pacific()->wake()]; + yield 'wallis' => [static fn($timezones) => $timezones->pacific()->wallis()]; + yield 'johnston' => [static fn($timezones) => $timezones->pacific()->johnston()]; + yield 'saipan' => [static fn($timezones) => $timezones->pacific()->saipan()]; + yield 'tarawa' => [static fn($timezones) => $timezones->pacific()->tarawa()]; + yield 'pitcairn' => [static fn($timezones) => $timezones->pacific()->pitcairn()]; + yield 'niue' => [static fn($timezones) => $timezones->pacific()->niue()]; + yield 'ponape' => [static fn($timezones) => $timezones->pacific()->ponape()]; + yield 'guam' => [static fn($timezones) => $timezones->pacific()->guam()]; + yield 'auckland' => [static fn($timezones) => $timezones->pacific()->auckland()]; + yield 'pagoPago' => [static fn($timezones) => $timezones->pacific()->pagoPago()]; + yield 'chuuk' => [static fn($timezones) => $timezones->pacific()->chuuk()]; + yield 'kwajalein' => [static fn($timezones) => $timezones->pacific()->kwajalein()]; + yield 'fakaofo' => [static fn($timezones) => $timezones->pacific()->fakaofo()]; + yield 'majuro' => [static fn($timezones) => $timezones->pacific()->majuro()]; + yield 'guadalcanal' => [static fn($timezones) => $timezones->pacific()->guadalcanal()]; + yield 'efate' => [static fn($timezones) => $timezones->pacific()->efate()]; + yield 'tongatapu' => [static fn($timezones) => $timezones->pacific()->tongatapu()]; + yield 'pohnpei' => [static fn($timezones) => $timezones->pacific()->pohnpei()]; + yield 'honolulu' => [static fn($timezones) => $timezones->pacific()->honolulu()]; + yield 'bougainville' => [static fn($timezones) => $timezones->pacific()->bougainville()]; + yield 'galapagos' => [static fn($timezones) => $timezones->pacific()->galapagos()]; + yield 'gambier' => [static fn($timezones) => $timezones->pacific()->gambier()]; + yield 'palau' => [static fn($timezones) => $timezones->pacific()->palau()]; + yield 'midway' => [static fn($timezones) => $timezones->pacific()->midway()]; + yield 'marquesas' => [static fn($timezones) => $timezones->pacific()->marquesas()]; + yield 'funafuti' => [static fn($timezones) => $timezones->pacific()->funafuti()]; + yield 'norfolk' => [static fn($timezones) => $timezones->pacific()->norfolk()]; + yield 'portMoresby' => [static fn($timezones) => $timezones->pacific()->portMoresby()]; + yield 'tahiti' => [static fn($timezones) => $timezones->pacific()->tahiti()]; + yield 'fiji' => [static fn($timezones) => $timezones->pacific()->fiji()]; + yield 'kiritimati' => [static fn($timezones) => $timezones->pacific()->kiritimati()]; + yield 'truk' => [static fn($timezones) => $timezones->pacific()->truk()]; + yield 'easter' => [static fn($timezones) => $timezones->pacific()->easter()]; + yield 'rarotonga' => [static fn($timezones) => $timezones->pacific()->rarotonga()]; + yield 'yap' => [static fn($timezones) => $timezones->pacific()->yap()]; + yield 'nauru' => [static fn($timezones) => $timezones->pacific()->nauru()]; + } +} diff --git a/tests/ElapsedPeriodTest.php b/tests/ElapsedPeriodTest.php new file mode 100644 index 0000000..dc21d72 --- /dev/null +++ b/tests/ElapsedPeriodTest.php @@ -0,0 +1,82 @@ +assertTrue( + $period->equals( + Period::microsecond(42)->asElapsedPeriod(), + ), + ); + + $period = ElapsedPeriod::of(0, 42, 0); + + $this->assertTrue( + $period->equals( + Period::millisecond(42)->asElapsedPeriod(), + ), + ); + + $period = ElapsedPeriod::of(42, 0, 0); + + $this->assertTrue( + $period->equals( + Period::second(42)->asElapsedPeriod(), + ), + ); + } + + public function testLongerThan() + { + $this->assertTrue( + ElapsedPeriod::of(0, 0, 42)->longerThan( + ElapsedPeriod::of(0, 0, 0), + ), + ); + $this->assertFalse( + ElapsedPeriod::of(0, 0, 42)->longerThan( + ElapsedPeriod::of(0, 0, 66), + ), + ); + } + + public function testEquals() + { + $this->assertTrue( + ElapsedPeriod::of(0, 0, 42)->equals( + ElapsedPeriod::of(0, 0, 42), + ), + ); + $this->assertFalse( + ElapsedPeriod::of(0, 0, 42)->equals( + ElapsedPeriod::of(0, 0, 66), + ), + ); + } + + public function testThrowWhenTryingToBuildFromYearPeriod() + { + $this->expectException(\LogicException::class); + + Period::year(1)->asElapsedPeriod(); + } + + public function testThrowWhenTryingToBuildFromMonthPeriod() + { + $this->expectException(\LogicException::class); + + Period::month(1)->asElapsedPeriod(); + } +} diff --git a/tests/Fixtures/PeriodTest.php b/tests/Fixtures/PeriodTest.php new file mode 100644 index 0000000..6c763bf --- /dev/null +++ b/tests/Fixtures/PeriodTest.php @@ -0,0 +1,109 @@ +assertInstanceOf(Set::class, $periods); + $this->assertCount(100, \iterator_to_array($periods->values(Random::default))); + + foreach ($periods->values(Random::default) as $period) { + $this->assertInstanceOf(Set\Value::class, $period); + $this->assertInstanceOf(Model::class, $period->unwrap()); + + if (\interface_exists(Set\Implementation::class)) { + $this->assertTrue($period->immutable()); + } else { + $this->assertTrue($period->isImmutable()); + } + } + } + + public function testAnyNumberOfYear() + { + $periods = Period::anyNumberOfYear(); + + $this->assertInstanceOf(Set::class, $periods); + $this->assertCount(100, \iterator_to_array($periods->values(Random::default))); + + $periods = $periods->values(Random::default); + $generated = []; + + foreach ($periods as $period) { + $this->assertInstanceOf(Set\Value::class, $period); + + if (\interface_exists(Set\Implementation::class)) { + $this->assertTrue($period->immutable()); + } else { + $this->assertTrue($period->isImmutable()); + } + + $value = $period->unwrap(); + $this->assertInstanceOf(Model::class, $value); + $this->assertSame(0, $value->months()); + $this->assertSame(0, $value->days()); + $this->assertSame(0, $value->hours()); + $this->assertSame(0, $value->minutes()); + $this->assertSame(0, $value->seconds()); + $this->assertSame(0, $value->milliseconds()); + + $generated[] = $value->years(); + } + + $this->assertGreaterThan(80, \count(\array_unique($generated))); + } + + public function testLessThanAYear() + { + $periods = Period::lessThanAYear(); + + $this->assertInstanceOf(Set::class, $periods); + $this->assertCount(100, \iterator_to_array($periods->values(Random::default))); + + $periods = $periods->values(Random::default); + $unique = []; + + foreach ($periods as $period) { + $this->assertInstanceOf(Set\Value::class, $period); + + if (\interface_exists(Set\Implementation::class)) { + $this->assertTrue($period->immutable()); + } else { + $this->assertTrue($period->isImmutable()); + } + + $value = $period->unwrap(); + $this->assertInstanceOf(Model::class, $value); + $this->assertLessThan(365, $value->days()); + $this->assertSame(0, $value->years()); + $this->assertSame(0, $value->months()); + $generated = [ + $value->days(), + $value->hours(), + $value->minutes(), + $value->seconds(), + $value->milliseconds(), + $value->microseconds(), + ]; + + if (!\in_array($generated, $unique, true)) { + $unique[] = $generated; + } + } + + $this->assertGreaterThan(80, \count($unique)); + } +} diff --git a/tests/Fixtures/PointTest.php b/tests/Fixtures/PointTest.php new file mode 100644 index 0000000..5ed23c5 --- /dev/null +++ b/tests/Fixtures/PointTest.php @@ -0,0 +1,69 @@ +assertInstanceOf(Set::class, $pointsInTime); + $this->assertCount(100, \iterator_to_array($pointsInTime->values(Random::default))); + + foreach ($pointsInTime->values(Random::default) as $pointInTime) { + $this->assertInstanceOf(Set\Value::class, $pointInTime); + $this->assertInstanceOf(Model::class, $pointInTime->unwrap()); + + if (\interface_exists(Set\Implementation::class)) { + $this->assertTrue($pointInTime->immutable()); + } else { + $this->assertTrue($pointInTime->isImmutable()); + } + } + } + + public function testAfter() + { + $start = Model::at(new \DateTimeImmutable('1970-01-01T12:13:14+02:00')); + $points = Point::after('1970-01-01T12:13:14+02:00'); + + $this->assertInstanceOf(Set::class, $points); + $this->assertCount(100, \iterator_to_array($points->values(Random::default))); + + foreach ($points->values(Random::default) as $point) { + $this->assertGreaterThanOrEqual( + (int) $start->format(Format::of('U')), + (int) $point->unwrap()->format(Format::of('U')), + ); + } + } + + public function testBefore() + { + $start = Model::at(new \DateTimeImmutable('1970-01-01T12:13:14+02:00')); + $points = Point::before('1970-01-01T12:13:14+02:00'); + + $this->assertInstanceOf(Set::class, $points); + $this->assertCount(100, \iterator_to_array($points->values(Random::default))); + + foreach ($points->values(Random::default) as $point) { + $this->assertLessThanOrEqual( + (int) $start->format(Format::of('U')), + (int) $point->unwrap()->format(Format::of('U')), + ); + } + } +} diff --git a/tests/FormatTest.php b/tests/FormatTest.php new file mode 100644 index 0000000..7cf08d9 --- /dev/null +++ b/tests/FormatTest.php @@ -0,0 +1,30 @@ +assertSame($expected, $format->toString()); + } + + public static function formats() + { + yield [Format::cookie(), \DateTime::COOKIE]; + yield [Format::iso8601(), \DateTime::ATOM]; + yield [Format::rfc1036(), \DateTime::RFC1036]; + yield [Format::rfc1123(), \DateTime::RFC1123]; + yield [Format::rfc2822(), \DateTime::RFC2822]; + yield [Format::rfc822(), \DateTime::RFC822]; + yield [Format::rfc850(), \DateTime::RFC850]; + yield [Format::rss(), \DateTime::RSS]; + yield [Format::w3c(), \DateTime::W3C]; + } +} diff --git a/tests/Move/EndOfDayTest.php b/tests/Move/EndOfDayTest.php new file mode 100644 index 0000000..3bcb872 --- /dev/null +++ b/tests/Move/EndOfDayTest.php @@ -0,0 +1,35 @@ +assertSame($expected, $point->format(Format::of('Y-m-d H:i:s.u'))); + } + + public static function cases(): array + { + return [ + ['2016-02-29 13:12:11.675', '2016-02-29 23:59:59.999999'], + ['2018-04-28 01:12:11.675', '2018-04-28 23:59:59.999999'], + ['2018-04-01 00:00:00.000', '2018-04-01 23:59:59.999999'], + ['2018-04-01 23:59:59.999', '2018-04-01 23:59:59.999999'], + ]; + } +} diff --git a/tests/Move/EndOfMonthTest.php b/tests/Move/EndOfMonthTest.php new file mode 100644 index 0000000..e1461c1 --- /dev/null +++ b/tests/Move/EndOfMonthTest.php @@ -0,0 +1,37 @@ +assertSame($expected, $point->format(Format::of('Y-m-d H:i:s.u'))); + } + + public static function cases(): array + { + return [ + ['2016-02-01 00:00:00.000', '2016-02-29 23:59:59.999999'], + ['2016-02-05 00:00:00.000', '2016-02-29 23:59:59.999999'], + ['2018-02-01 00:00:00.000', '2018-02-28 23:59:59.999999'], + ['2018-04-01 00:00:00.000', '2018-04-30 23:59:59.999999'], + ['2018-01-01 00:00:00.000', '2018-01-31 23:59:59.999999'], + ['2018-04-30 23:59:59.999', '2018-04-30 23:59:59.999999'], + ]; + } +} diff --git a/tests/Move/EndOfYearTest.php b/tests/Move/EndOfYearTest.php new file mode 100644 index 0000000..bfac68a --- /dev/null +++ b/tests/Move/EndOfYearTest.php @@ -0,0 +1,36 @@ +assertSame($expected, $point->format(Format::of('Y-m-d H:i:s.u'))); + } + + public static function cases(): array + { + return [ + ['2016-02-29 13:12:11.675', '2016-12-31 23:59:59.999999'], + ['2018-04-28 01:12:11.675', '2018-12-31 23:59:59.999999'], + ['2018-04-01 00:00:00.000', '2018-12-31 23:59:59.999999'], + ['2018-01-01 00:00:00.000', '2018-12-31 23:59:59.999999'], + ['2018-12-31 23:59:59.999', '2018-12-31 23:59:59.999999'], + ]; + } +} diff --git a/tests/Move/MonthTest.php b/tests/Move/MonthTest.php new file mode 100644 index 0000000..9f323c3 --- /dev/null +++ b/tests/Move/MonthTest.php @@ -0,0 +1,40 @@ +assertSame($expectedForward, $point->format($format)); + + $point = $backward(Point::at(new \DateTimeImmutable($time))); + + $this->assertSame($expectedBackward, $point->format($format)); + } + + public static function cases(): array + { + return [ + ['2016-02-29 13:12:11.675342', '2016-03-29 13:12:11.675342', '2016-01-29 13:12:11.675342'], + ['2016-01-30 13:12:11.675342', '2016-02-29 13:12:11.675342', '2015-12-30 13:12:11.675342'], + ['2017-12-31 13:12:11.675342', '2018-01-31 13:12:11.675342', '2017-11-30 13:12:11.675342'], + ]; + } +} diff --git a/tests/Move/StartOfDayTest.php b/tests/Move/StartOfDayTest.php new file mode 100644 index 0000000..082a841 --- /dev/null +++ b/tests/Move/StartOfDayTest.php @@ -0,0 +1,35 @@ +assertSame($expected, $point->format(Format::of('Y-m-d H:i:s.u'))); + } + + public static function cases(): array + { + return [ + ['2016-02-29 13:12:11.675', '2016-02-29 00:00:00.000000'], + ['2018-04-28 01:12:11.675', '2018-04-28 00:00:00.000000'], + ['2018-04-01 00:00:00.000', '2018-04-01 00:00:00.000000'], + ['2018-11-30T12:13:14.000+0100', '2018-11-30 00:00:00.000000'], + ]; + } +} diff --git a/tests/Move/StartOfMonthTest.php b/tests/Move/StartOfMonthTest.php new file mode 100644 index 0000000..03a7889 --- /dev/null +++ b/tests/Move/StartOfMonthTest.php @@ -0,0 +1,34 @@ +assertSame($expected, $point->format(Format::of('Y-m-d H:i:s.u'))); + } + + public static function cases(): array + { + return [ + ['2016-02-29 13:12:11.675', '2016-02-01 00:00:00.000000'], + ['2018-04-28 01:12:11.675', '2018-04-01 00:00:00.000000'], + ['2018-04-01 00:00:00.000', '2018-04-01 00:00:00.000000'], + ]; + } +} diff --git a/tests/Move/StartOfYearTest.php b/tests/Move/StartOfYearTest.php new file mode 100644 index 0000000..0aba7b2 --- /dev/null +++ b/tests/Move/StartOfYearTest.php @@ -0,0 +1,35 @@ +assertSame($expected, $point->format(Format::of('Y-m-d H:i:s.u'))); + } + + public static function cases(): array + { + return [ + ['2016-02-29 13:12:11.675', '2016-01-01 00:00:00.000000'], + ['2018-04-28 01:12:11.675', '2018-01-01 00:00:00.000000'], + ['2018-04-01 00:00:00.000', '2018-01-01 00:00:00.000000'], + ['2018-01-01 00:00:00.000', '2018-01-01 00:00:00.000000'], + ]; + } +} diff --git a/tests/NowTest.php b/tests/NowTest.php new file mode 100644 index 0000000..9160a06 --- /dev/null +++ b/tests/NowTest.php @@ -0,0 +1,168 @@ +assertInstanceOf(Year::class, $point->year()); + $this->assertInstanceOf(Month::class, $point->month()); + $this->assertInstanceOf(Day::class, $point->day()); + $this->assertInstanceOf(Hour::class, $point->hour()); + $this->assertInstanceOf(Minute::class, $point->minute()); + $this->assertInstanceOf(Second::class, $point->second()); + $this->assertInstanceOf(Millisecond::class, $point->millisecond()); + $this->assertInstanceOf(Offset::class, $point->offset()); + $this->assertSame((int) \date('Y', $timestamp), $point->year()->toInt()); + $this->assertSame((int) \date('m', $timestamp), $point->month()->ofYear()->toInt()); + $this->assertSame((int) \date('d', $timestamp), $point->day()->ofMonth()); + $this->assertSame((int) \date('H', $timestamp), $point->hour()->toInt()); + $this->assertSame((int) \date('i', $timestamp), $point->minute()->toInt()); + $this->assertSame((int) \date('s', $timestamp), $point->second()->toInt()); + //allow 50 milliseconds delay between our microtime + //and the one in Now::__construct + $this->assertTrue($point->millisecond()->toInt() >= $now - ($timestamp * 1000)); + $this->assertTrue($point->millisecond()->toInt() <= $now - ($timestamp * 1000) + 50); + $timezone = \date('P', $timestamp); + $timezone = $timezone === '+00:00' ? 'Z' : $timezone; + $this->assertSame($timezone, $point->offset()->toString()); + } + + public function testFormat() + { + $point = Point::now(); + + $this->assertSame( + \date('H:i:s d/m/Y'), + $point->format(Format::of('H:i:s d/m/Y')), + ); + } + + public function testChangeOffset() + { + $now = new \DateTimeImmutable; + $now = $now->setTimezone(new \DateTimeZone('-02:30')); + $point = Point::now(); + $point2 = $point->changeOffset(Offset::minus(2, 30)); + + $this->assertNotSame($point, $point2); + $this->assertNotSame($point->year(), $point2->year()); + $this->assertNotSame($point->month(), $point2->month()); + $this->assertNotSame($point->day(), $point2->day()); + $this->assertNotSame($point->hour(), $point2->hour()); + $this->assertNotSame($point->minute(), $point2->minute()); + $this->assertNotSame($point->second(), $point2->second()); + $this->assertNotSame($point->millisecond(), $point2->millisecond()); + $this->assertSame((int) $now->format('Y'), $point2->year()->toInt()); + $this->assertSame((int) $now->format('m'), $point2->month()->ofYear()->toInt()); + $this->assertSame((int) $now->format('d'), $point2->day()->ofMonth()); + $this->assertSame((int) $now->format('H'), $point2->hour()->toInt()); + $this->assertSame((int) $now->format('i'), $point2->minute()->toInt()); + $this->assertSame((int) $now->format('s'), $point2->second()->toInt()); + $this->assertSame('-02:30', $point2->offset()->toString()); + } + + public function testElapsedSince() + { + $point = Point::now(); + \sleep(1); + $point2 = Point::now(); + $elapsed = $point2->elapsedSince($point); + + $this->assertInstanceOf(ElapsedPeriod::class, $elapsed); + //make sure there's at least 1 second elapsed due to the sleep() + $this->assertTrue( + $elapsed->longerThan( + Period::second(1)->asElapsedPeriod(), + ), + ); + } + + public function testAheadOf() + { + $point = Point::now(); + \sleep(1); + $point2 = Point::now(); + + $this->assertTrue( + $point2->aheadOf($point), + $point->format(Format::of('Y-m-d\TH:i:s.uP')).' '.$point2->format(Format::of('Y-m-d\TH:i:s.uP')), + ); + $this->assertFalse( + $point->aheadOf($point2), + $point->format(Format::of('Y-m-d\TH:i:s.uP')).' '.$point2->format(Format::of('Y-m-d\TH:i:s.uP')), + ); + } + + public function testEquals() + { + $point = Point::now(); + \sleep(1); + $point2 = Point::now(); + $point3 = clone $point; + + $this->assertTrue($point->equals($point3)); + $this->assertFalse($point->equals($point2)); + } + + public function testGoForward() + { + $point = Point::now(); + $point2 = $point->goForward(Period::year(1)); + + $this->assertNotSame($point, $point2); + $this->assertSame($point->year()->toInt() + 1, $point2->year()->toInt()); + } + + public function testGoBack() + { + $point = Point::now(); + $point2 = $point->goBack(Period::year(1)); + + $this->assertNotSame($point, $point2); + $this->assertSame($point->year()->toInt() - 1, $point2->year()->toInt()); + + $point3 = $point->goBack(Period::millisecond(500)); + + if ($point->millisecond()->toInt() > 500) { + $this->assertSame( + $point->millisecond()->toInt() - 500, + $point3->millisecond()->toInt(), + ); + } else { + $this->assertSame( + 1000 - \abs($point->millisecond()->toInt() - 500), + $point3->millisecond()->toInt(), + ); + $this->assertSame( + $point->second()->toInt() === 0 ? + 59 : $point->second()->toInt() - 1, + $point3->second()->toInt(), + ); + } + } +} diff --git a/tests/Period.php b/tests/Period.php new file mode 100644 index 0000000..6021a0f --- /dev/null +++ b/tests/Period.php @@ -0,0 +1,84 @@ +assertSame(1, $period->years()); + $this->assertSame(2, $period->months()); + $this->assertSame(3, $period->days()); + $this->assertSame(4, $period->hours()); + $this->assertSame(5, $period->minutes()); + $this->assertSame(6, $period->seconds()); + $this->assertSame(7, $period->milliseconds()); + } + + public function testAdjustEachComponent() + { + $period = Period::of( + 1, + 13, + 1, + 49, + 2880, + 90061, + 90061001, + ); + + $this->assertSame(2, $period->years()); + $this->assertSame(1, $period->months()); + $this->assertSame(7, $period->days()); + $this->assertSame(3, $period->hours()); + $this->assertSame(2, $period->minutes()); + $this->assertSame(2, $period->seconds()); + $this->assertSame(1, $period->milliseconds()); + } + + public function testEquals() + { + $period = Period::of(1, 2, 3, 4, 5, 6, 7); + $period2 = Period::of(1, 2, 3, 4, 5, 6, 8); + $period3 = Period::of(1, 2, 3, 4, 5, 6, 7); + + $this->assertTrue($period->equals($period3)); + $this->assertFalse($period->equals($period2)); + } + + public function testAdd() + { + $period = Period::of(1, 2, 3, 4, 5, 6, 7); + $period2 = $period->add($period); + + $this->assertNotSame($period, $period2); + $this->assertSame(1, $period->years()); + $this->assertSame(2, $period->months()); + $this->assertSame(3, $period->days()); + $this->assertSame(4, $period->hours()); + $this->assertSame(5, $period->minutes()); + $this->assertSame(6, $period->seconds()); + $this->assertSame(7, $period->milliseconds()); + $this->assertSame(2, $period2->years()); + $this->assertSame(4, $period2->months()); + $this->assertSame(6, $period2->days()); + $this->assertSame(8, $period2->hours()); + $this->assertSame(10, $period2->minutes()); + $this->assertSame(12, $period2->seconds()); + $this->assertSame(14, $period2->milliseconds()); + } + + public function testAsElapsedPeriod() + { + $this->assertSame( + 90_061_001, + Period::of(0, 0, 1, 1, 1, 1, 1)->asElapsedPeriod()->milliseconds(), + ); + } +} diff --git a/tests/Period/DayTest.php b/tests/Period/DayTest.php new file mode 100644 index 0000000..18cb137 --- /dev/null +++ b/tests/Period/DayTest.php @@ -0,0 +1,69 @@ +assertInstanceOf(Period::class, $period); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(1000, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + public function testEquals() + { + $this->assertTrue(Period::day(2)->equals(Period::day(2))); + $this->assertFalse(Period::day(2)->equals(Period::day(3))); + } + + public function testAdd() + { + $period = Period::day(1000); + $period2 = $period->add($period); + + $this->assertInstanceOf(Period::class, $period); + $this->assertNotSame($period, $period2); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(1000, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + $this->assertSame(0, $period2->years()); + $this->assertSame(0, $period2->months()); + $this->assertSame(2000, $period2->days()); + $this->assertSame(0, $period2->hours()); + $this->assertSame(0, $period2->minutes()); + $this->assertSame(0, $period2->seconds()); + $this->assertSame(0, $period2->milliseconds()); + } + + public function testAsElapsedPeriod() + { + $this->assertSame( + 1, + Period::day(1)->asElapsedPeriod()->asPeriod()->days(), + ); + $this->assertSame( + 2, + Period::day(2)->asElapsedPeriod()->asPeriod()->days(), + ); + $this->assertSame( + 3, + Period::day(3)->asElapsedPeriod()->asPeriod()->days(), + ); + } +} diff --git a/tests/Period/HourTest.php b/tests/Period/HourTest.php new file mode 100644 index 0000000..6c9ba09 --- /dev/null +++ b/tests/Period/HourTest.php @@ -0,0 +1,95 @@ +assertInstanceOf(Period::class, $period); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(20, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + #[DataProvider('cases')] + public function testConvertToDays(int $hour, int $days, int $expectedHours) + { + $period = Period::hour($hour); + + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame($days, $period->days()); + $this->assertSame($expectedHours, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + public function testEquals() + { + $this->assertTrue(Period::hour(25)->equals(Period::hour(25))); + $this->assertFalse(Period::hour(2)->equals(Period::hour(3))); + } + + public function testAdd() + { + $period = Period::hour(20); + $period2 = $period->add($period); + + $this->assertInstanceOf(Period::class, $period2); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(20, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + $this->assertSame(0, $period2->years()); + $this->assertSame(0, $period2->months()); + $this->assertSame(1, $period2->days()); + $this->assertSame(16, $period2->hours()); + $this->assertSame(0, $period2->minutes()); + $this->assertSame(0, $period2->seconds()); + $this->assertSame(0, $period2->milliseconds()); + } + + public function testAsElapsedPeriod() + { + $this->assertSame( + 1, + Period::hour(1)->asElapsedPeriod()->asPeriod()->hours(), + ); + $this->assertSame( + 2, + Period::hour(2)->asElapsedPeriod()->asPeriod()->hours(), + ); + $this->assertSame( + 3, + Period::hour(3)->asElapsedPeriod()->asPeriod()->hours(), + ); + } + + public static function cases() + { + return [ + [20, 0, 20], + [24, 1, 0], + [23, 0, 23], + [25, 1, 1], + [48, 2, 0], + [49, 2, 1], + ]; + } +} diff --git a/tests/Period/MillisecondTest.php b/tests/Period/MillisecondTest.php new file mode 100644 index 0000000..331f919 --- /dev/null +++ b/tests/Period/MillisecondTest.php @@ -0,0 +1,118 @@ +assertInstanceOf(Period::class, $period); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(20, $period->milliseconds()); + } + + #[DataProvider('cases')] + public function testConvertToSeconds( + int $millisecond, + int $days, + int $hours, + int $minutes, + int $seconds, + int $expectedMilliseconds, + ) { + $period = Period::millisecond($millisecond); + + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame($days, $period->days()); + $this->assertSame($hours, $period->hours()); + $this->assertSame($minutes, $period->minutes()); + $this->assertSame($seconds, $period->seconds()); + $this->assertSame($expectedMilliseconds, $period->milliseconds()); + } + + public function testEquals() + { + $this->assertTrue(Period::millisecond(66)->equals(Period::millisecond(66))); + $this->assertFalse(Period::millisecond(2)->equals(Period::millisecond(3))); + } + + public function testAdd() + { + $period = Period::millisecond(20); + $period2 = $period->add($period); + + $this->assertInstanceOf(Period::class, $period2); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(20, $period->milliseconds()); + $this->assertSame(0, $period2->years()); + $this->assertSame(0, $period2->months()); + $this->assertSame(0, $period2->days()); + $this->assertSame(0, $period2->hours()); + $this->assertSame(0, $period2->minutes()); + $this->assertSame(0, $period2->seconds()); + $this->assertSame(40, $period2->milliseconds()); + } + + public function testAsElapsedPeriod() + { + $this->assertSame( + 1, + Period::millisecond(1)->asElapsedPeriod()->asPeriod()->milliseconds(), + ); + $this->assertSame( + 2, + Period::millisecond(2)->asElapsedPeriod()->asPeriod()->milliseconds(), + ); + $this->assertSame( + 3, + Period::millisecond(3)->asElapsedPeriod()->asPeriod()->milliseconds(), + ); + } + + public static function cases() + { + return [ + [20, 0, 0, 0, 0, 20], + [1000, 0, 0, 0, 1, 0], + [1001, 0, 0, 0, 1, 1], + [2000, 0, 0, 0, 2, 0], + [2001, 0, 0, 0, 2, 1], + [2001, 0, 0, 0, 2, 1], + [60000, 0, 0, 1, 0, 0], + [60001, 0, 0, 1, 0, 1], + [61001, 0, 0, 1, 1, 1], + [120000, 0, 0, 2, 0, 0], + [120001, 0, 0, 2, 0, 1], + [121001, 0, 0, 2, 1, 1], + [3600000, 0, 1, 0, 0, 0], + [3600001, 0, 1, 0, 0, 1], + [3601001, 0, 1, 0, 1, 1], + [3660000, 0, 1, 1, 0, 0], + [3660001, 0, 1, 1, 0, 1], + [3661001, 0, 1, 1, 1, 1], + [86400000, 1, 0, 0, 0, 0], + [86400001, 1, 0, 0, 0, 1], + [86401001, 1, 0, 0, 1, 1], + [86461001, 1, 0, 1, 1, 1], + [90061001, 1, 1, 1, 1, 1], + ]; + } +} diff --git a/tests/Period/MinuteTest.php b/tests/Period/MinuteTest.php new file mode 100644 index 0000000..46e68a2 --- /dev/null +++ b/tests/Period/MinuteTest.php @@ -0,0 +1,103 @@ +assertInstanceOf(Period::class, $period); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(20, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + #[DataProvider('cases')] + public function testConvertToHours( + int $minute, + int $days, + int $hours, + int $expectedMinutes, + ) { + $period = Period::minute($minute); + + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame($days, $period->days()); + $this->assertSame($hours, $period->hours()); + $this->assertSame($expectedMinutes, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + public function testEquals() + { + $this->assertTrue(Period::minute(66)->equals(Period::minute(66))); + $this->assertFalse(Period::minute(2)->equals(Period::minute(3))); + } + + public function testAdd() + { + $period = Period::minute(20); + $period2 = $period->add($period); + + $this->assertInstanceOf(Period::class, $period2); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(20, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + $this->assertSame(0, $period2->years()); + $this->assertSame(0, $period2->months()); + $this->assertSame(0, $period2->days()); + $this->assertSame(0, $period2->hours()); + $this->assertSame(40, $period2->minutes()); + $this->assertSame(0, $period2->seconds()); + $this->assertSame(0, $period2->milliseconds()); + } + + public function testAsElapsedPeriod() + { + $this->assertSame( + 1, + Period::minute(1)->asElapsedPeriod()->asPeriod()->minutes(), + ); + $this->assertSame( + 2, + Period::minute(2)->asElapsedPeriod()->asPeriod()->minutes(), + ); + $this->assertSame( + 3, + Period::minute(3)->asElapsedPeriod()->asPeriod()->minutes(), + ); + } + + public static function cases() + { + return [ + [20, 0, 0, 20], + [59, 0, 0, 59], + [60, 0, 1, 0], + [61, 0, 1, 1], + [120, 0, 2, 0], + [121, 0, 2, 1], + [1440, 1, 0, 0], + [1441, 1, 0, 1], + [1501, 1, 1, 1], + [2880, 2, 0, 0], + ]; + } +} diff --git a/tests/Period/MonthTest.php b/tests/Period/MonthTest.php new file mode 100644 index 0000000..28a9ae4 --- /dev/null +++ b/tests/Period/MonthTest.php @@ -0,0 +1,78 @@ +assertInstanceOf(Period::class, $period); + $this->assertSame(0, $period->years()); + $this->assertSame(10, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + #[DataProvider('cases')] + public function testConvertMonthsIntoYears(int $months, int $year, int $expectedMonths) + { + $period = Period::month($months); + + $this->assertSame($year, $period->years()); + $this->assertSame($expectedMonths, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + public function testEquals() + { + $this->assertTrue(Period::month(20)->equals(Period::month(20))); + $this->assertFalse(Period::month(2)->equals(Period::month(3))); + } + + public function testAdd() + { + $period = Period::month(10); + $period2 = $period->add($period); + + $this->assertInstanceOf(Period::class, $period2); + $this->assertSame(0, $period->years()); + $this->assertSame(10, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + $this->assertSame(1, $period2->years()); + $this->assertSame(8, $period2->months()); + $this->assertSame(0, $period2->days()); + $this->assertSame(0, $period2->hours()); + $this->assertSame(0, $period2->minutes()); + $this->assertSame(0, $period2->seconds()); + $this->assertSame(0, $period2->milliseconds()); + } + + public static function cases() + { + return [ + [12, 1, 0], + [11, 0, 11], + [13, 1, 1], + [24, 2, 0], + [25, 2, 1], + ]; + } +} diff --git a/tests/Period/SecondTest.php b/tests/Period/SecondTest.php new file mode 100644 index 0000000..cc1fb4b --- /dev/null +++ b/tests/Period/SecondTest.php @@ -0,0 +1,110 @@ +assertInstanceOf(Period::class, $period); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(20, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + #[DataProvider('cases')] + public function testConvertToMinutes( + int $second, + int $days, + int $hours, + int $minutes, + int $expectedSeconds, + ) { + $period = Period::second($second); + + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame($days, $period->days()); + $this->assertSame($hours, $period->hours()); + $this->assertSame($minutes, $period->minutes()); + $this->assertSame($expectedSeconds, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + public function testEquals() + { + $this->assertTrue(Period::second(66)->equals(Period::second(66))); + $this->assertFalse(Period::second(2)->equals(Period::second(3))); + } + + public function testAdd() + { + $period = Period::second(20); + $period2 = $period->add($period); + + $this->assertInstanceOf(Period::class, $period2); + $this->assertSame(0, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(20, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + $this->assertSame(0, $period2->years()); + $this->assertSame(0, $period2->months()); + $this->assertSame(0, $period2->days()); + $this->assertSame(0, $period2->hours()); + $this->assertSame(0, $period2->minutes()); + $this->assertSame(40, $period2->seconds()); + $this->assertSame(0, $period2->milliseconds()); + } + + public function testAsElapsedPeriod() + { + $this->assertSame( + 1, + Period::second(1)->asElapsedPeriod()->asPeriod()->seconds(), + ); + $this->assertSame( + 2, + Period::second(2)->asElapsedPeriod()->asPeriod()->seconds(), + ); + $this->assertSame( + 3, + Period::second(3)->asElapsedPeriod()->asPeriod()->seconds(), + ); + } + + public static function cases() + { + return [ + [20, 0, 0, 0, 20], + [60, 0, 0, 1, 0], + [61, 0, 0, 1, 1], + [120, 0, 0, 2, 0], + [121, 0, 0, 2, 1], + [3600, 0, 1, 0, 0], + [3601, 0, 1, 0, 1], + [7200, 0, 2, 0, 0], + [7201, 0, 2, 0, 1], + [7261, 0, 2, 1, 1], + [86400, 1, 0, 0, 0], + [86401, 1, 0, 0, 1], + [86461, 1, 0, 1, 1], + [90000, 1, 1, 0, 0], + [90001, 1, 1, 0, 1], + [90061, 1, 1, 1, 1], + ]; + } +} diff --git a/tests/Period/YearTest.php b/tests/Period/YearTest.php new file mode 100644 index 0000000..bcbfda9 --- /dev/null +++ b/tests/Period/YearTest.php @@ -0,0 +1,52 @@ +assertInstanceOf(Period::class, $period); + $this->assertSame(42, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + } + + public function testEquals() + { + $this->assertTrue(Period::year(2)->equals(Period::year(2))); + $this->assertFalse(Period::year(2)->equals(Period::year(3))); + } + + public function testAdd() + { + $period = Period::year(42); + $period2 = $period->add($period); + + $this->assertInstanceOf(Period::class, $period2); + $this->assertSame(42, $period->years()); + $this->assertSame(0, $period->months()); + $this->assertSame(0, $period->days()); + $this->assertSame(0, $period->hours()); + $this->assertSame(0, $period->minutes()); + $this->assertSame(0, $period->seconds()); + $this->assertSame(0, $period->milliseconds()); + $this->assertSame(84, $period2->years()); + $this->assertSame(0, $period2->months()); + $this->assertSame(0, $period2->days()); + $this->assertSame(0, $period2->hours()); + $this->assertSame(0, $period2->minutes()); + $this->assertSame(0, $period2->seconds()); + $this->assertSame(0, $period2->milliseconds()); + } +} diff --git a/tests/Point/DayTest.php b/tests/Point/DayTest.php new file mode 100644 index 0000000..6ad1812 --- /dev/null +++ b/tests/Point/DayTest.php @@ -0,0 +1,21 @@ +assertSame(3, $day->ofWeek()->toInt()); + $this->assertSame(278, $day->ofYear()); + $this->assertSame(24, $day->numberOfHours()); + $this->assertSame(5, $day->ofMonth()); + $this->assertSame('wednesday', $day->ofWeek()->name); + } +} diff --git a/tests/Point/HighResolutionTest.php b/tests/Point/HighResolutionTest.php new file mode 100644 index 0000000..0ca7eb8 --- /dev/null +++ b/tests/Point/HighResolutionTest.php @@ -0,0 +1,75 @@ +elapsedSince($started)->asPeriod(); + $this->assertSame(0, $period->seconds()); + $this->assertSame(1, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); + } + + public function testDiffLessThanOneSecondButNotInSameSecond() + { + $started = HighResolution::of(1, 2_000_000); + $end = HighResolution::of(2, 1_000_000); + + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(0, $period->seconds()); + $this->assertSame(999, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); + } + + public function testDiffMoreThanOneSecond() + { + $started = HighResolution::of(1, 2_000_000); + $end = HighResolution::of(2, 3_000_000); + + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(1, $period->seconds()); + $this->assertSame(1, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); + } + + public function testDiffMoreThanMultipleSeconds() + { + $started = HighResolution::of(1, 2_000_000); + $end = HighResolution::of(3, 3_000_000); + + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(2, $period->seconds()); + $this->assertSame(1, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); + } + + public function testDiffMoreThanMultipleSeconds2() + { + $started = HighResolution::of(1, 2_000_000); + $end = HighResolution::of(3, 1_000_000); + + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(1, $period->seconds()); + $this->assertSame(999, $period->milliseconds()); + $this->assertSame(0, $period->microseconds()); + } + + public function test1() + { + $started = HighResolution::of(684461, 547614375); + $end = HighResolution::of(684462, 602541); + + $period = $end->elapsedSince($started)->asPeriod(); + $this->assertSame(452, $period->milliseconds()); + $this->assertSame(988, $period->microseconds()); + } +} diff --git a/tests/Point/HourTest.php b/tests/Point/HourTest.php new file mode 100644 index 0000000..fa62f38 --- /dev/null +++ b/tests/Point/HourTest.php @@ -0,0 +1,18 @@ +assertSame(60, $hour->numberOfMinutes()); + $this->assertSame(13, $hour->toInt()); + } +} diff --git a/tests/Point/MillisecondTest.php b/tests/Point/MillisecondTest.php new file mode 100644 index 0000000..80b2dd3 --- /dev/null +++ b/tests/Point/MillisecondTest.php @@ -0,0 +1,17 @@ +assertSame(600, $millisecond->toInt()); + } +} diff --git a/tests/Point/MinuteTest.php b/tests/Point/MinuteTest.php new file mode 100644 index 0000000..4236a58 --- /dev/null +++ b/tests/Point/MinuteTest.php @@ -0,0 +1,18 @@ +assertSame(60, $minute->numberOfSeconds()); + $this->assertSame(13, $minute->toInt()); + } +} diff --git a/tests/Point/MonthTest.php b/tests/Point/MonthTest.php new file mode 100644 index 0000000..680e5d5 --- /dev/null +++ b/tests/Point/MonthTest.php @@ -0,0 +1,19 @@ +assertSame(31, $month->numberOfDays()); + $this->assertSame(10, $month->ofYear()->toInt()); + $this->assertSame('october', $month->ofYear()->name); + } +} diff --git a/tests/Point/SecondTest.php b/tests/Point/SecondTest.php new file mode 100644 index 0000000..144ae24 --- /dev/null +++ b/tests/Point/SecondTest.php @@ -0,0 +1,17 @@ +assertSame(13, $second->toInt()); + } +} diff --git a/tests/Point/YearTest.php b/tests/Point/YearTest.php new file mode 100644 index 0000000..cffaa28 --- /dev/null +++ b/tests/Point/YearTest.php @@ -0,0 +1,21 @@ +assertSame(366, $year->numberOfDays()); + $this->assertSame(12, $year->numberOfMonths()); + $this->assertSame(2016, $year->toInt()); + + $this->assertSame(365, Year::of(2017)->numberOfDays()); + } +} diff --git a/tests/PointTest.php b/tests/PointTest.php new file mode 100644 index 0000000..c4a5e5c --- /dev/null +++ b/tests/PointTest.php @@ -0,0 +1,218 @@ +assertInstanceOf(Year::class, $point->year()); + $this->assertInstanceOf(Month::class, $point->month()); + $this->assertInstanceOf(Day::class, $point->day()); + $this->assertInstanceOf(Hour::class, $point->hour()); + $this->assertInstanceOf(Minute::class, $point->minute()); + $this->assertInstanceOf(Second::class, $point->second()); + $this->assertInstanceOf(Millisecond::class, $point->millisecond()); + $this->assertInstanceOf(Offset::class, $point->offset()); + $this->assertSame(2016, $point->year()->toInt()); + $this->assertSame(10, $point->month()->ofYear()->toInt()); + $this->assertSame(5, $point->day()->ofMonth()); + $this->assertSame(8, $point->hour()->toInt()); + $this->assertSame(1, $point->minute()->toInt()); + $this->assertSame(30, $point->second()->toInt()); + $this->assertSame(123, $point->millisecond()->toInt()); + $this->assertSame('+02:00', $point->offset()->toString()); + $this->assertSame('2016-10-05T08:01:30.123000+02:00', $point->toString()); + } + + public function testPreserveMillisecondsWhenNanosecondsInString() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.999678+02:00')); + + $this->assertSame(999, $point->millisecond()->toInt()); + $this->assertSame(30, $point->second()->toInt()); + } + + public function testFormat() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + + $this->assertSame( + '08:01:30 05/10/2016', + $point->format(Format::of('H:i:s d/m/Y')), + ); + } + + public function testChangeOffset() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + $point2 = $point->changeOffset(Offset::minus(2, 30)); + + $this->assertNotSame($point, $point2); + $this->assertNotSame($point->year(), $point2->year()); + $this->assertNotSame($point->month(), $point2->month()); + $this->assertNotSame($point->day(), $point2->day()); + $this->assertNotSame($point->hour(), $point2->hour()); + $this->assertNotSame($point->minute(), $point2->minute()); + $this->assertNotSame($point->second(), $point2->second()); + $this->assertNotSame($point->millisecond(), $point2->millisecond()); + $this->assertSame(2016, $point2->year()->toInt()); + $this->assertSame(10, $point2->month()->ofYear()->toInt()); + $this->assertSame(5, $point2->day()->ofMonth()); + $this->assertSame(3, $point2->hour()->toInt()); + $this->assertSame(31, $point2->minute()->toInt()); + $this->assertSame(30, $point2->second()->toInt()); + $this->assertSame(123, $point2->millisecond()->toInt()); + $this->assertSame('-02:30', $point2->offset()->toString()); + } + + public function testElapsedSince() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + $point2 = Point::at(new \DateTimeImmutable('2016-10-05T08:03:30.234+02:00')); + $elapsed = $point2->elapsedSince($point)->asPeriod(); + + $this->assertSame(2, $elapsed->minutes()); + $this->assertSame(0, $elapsed->seconds()); + $this->assertSame(111, $elapsed->milliseconds()); + $this->assertSame(0, $elapsed->microseconds()); + } + + public function testAheadOf() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + $point2 = Point::at(new \DateTimeImmutable('2016-10-05T08:03:30.234+02:00')); + + $this->assertTrue($point2->aheadOf($point)); + $this->assertFalse($point->aheadOf($point2)); + } + + public function testEquals() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + $point2 = Point::at(new \DateTimeImmutable('2016-10-05T08:03:30.234+02:00')); + $point3 = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + + $this->assertTrue($point->equals($point3)); + $this->assertFalse($point->equals($point2)); + } + + public function testGoForward() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + $point2 = $point->goForward( + Period::of(1, 1, 1, 1, 1, 30, 878, 0), + ); + + $this->assertSame(2016, $point->year()->toInt()); + $this->assertSame(10, $point->month()->ofYear()->toInt()); + $this->assertSame(5, $point->day()->ofMonth()); + $this->assertSame(8, $point->hour()->toInt()); + $this->assertSame(1, $point->minute()->toInt()); + $this->assertSame(30, $point->second()->toInt()); + $this->assertSame(123, $point->millisecond()->toInt()); + $this->assertSame(2017, $point2->year()->toInt()); + $this->assertSame(11, $point2->month()->ofYear()->toInt()); + $this->assertSame(6, $point2->day()->ofMonth()); + $this->assertSame(9, $point2->hour()->toInt()); + $this->assertSame(3, $point2->minute()->toInt()); + $this->assertSame(1, $point2->second()->toInt()); + $this->assertSame(1, $point2->millisecond()->toInt()); + } + + public function testGoBack() + { + $point = Point::at(new \DateTimeImmutable('2016-10-05T08:01:30.123+02:00')); + $point2 = $point->goBack( + Period::of(1, 1, 1, 1, 1, 30, 125, 0), + ); + + $this->assertSame(2016, $point->year()->toInt()); + $this->assertSame(10, $point->month()->ofYear()->toInt()); + $this->assertSame(5, $point->day()->ofMonth()); + $this->assertSame(8, $point->hour()->toInt()); + $this->assertSame(1, $point->minute()->toInt()); + $this->assertSame(30, $point->second()->toInt()); + $this->assertSame(123, $point->millisecond()->toInt()); + $this->assertSame(2015, $point2->year()->toInt()); + $this->assertSame(9, $point2->month()->ofYear()->toInt()); + $this->assertSame(4, $point2->day()->ofMonth()); + $this->assertSame(6, $point2->hour()->toInt()); + $this->assertSame(59, $point2->minute()->toInt()); + $this->assertSame(59, $point2->second()->toInt()); + $this->assertSame(998, $point2->millisecond()->toInt()); + + $this->assertSame( + '2016-10-05T08:01:29.500000+02:00', + $point + ->goBack(Period::millisecond(623)) + ->format(Format::of('Y-m-d\TH:i:s.uP')), + ); + } + + public function testGoBackOneDay() + { + $point = Point::at(new \DateTimeImmutable('2018-03-04')); + $point2 = $point->goBack(Period::day(1)); + $format = Format::of('Y-m-d\TH:i:s.u'); + + $this->assertSame( + '2018-03-04T00:00:00.000000', + $point->format($format), + ); + $this->assertSame( + '2018-03-03T00:00:00.000000', + $point2->format($format), + ); + } + + public function testGoBackOneMillisecondWhenCurrentPointIsAtPreciselyZeroMillisecond() + { + $point = Point::at(new \DateTimeImmutable('1402-07-21 02:42:53.000000')); + $point2 = $point->goBack(Period::millisecond(1)); + $format = Format::of('Y-m-d\TH:i:s.u'); + + $this->assertSame( + '1402-07-21T02:42:53.000000', + $point->format($format), + ); + $this->assertSame( + '1402-07-21T02:42:52.999000', + $point2->format($format), + ); + } + + public function testGoBackOneMicrosecondWhenCurrentPointIsAtPreciselyZeroMillisecond() + { + $point = Point::at(new \DateTimeImmutable('1402-07-21 02:42:53.000000')); + $point2 = $point->goBack(Period::microsecond(1)); + $format = Format::of('Y-m-d\TH:i:s.u'); + + $this->assertSame( + '1402-07-21T02:42:53.000000', + $point->format($format), + ); + $this->assertSame( + '1402-07-21T02:42:52.999999', + $point2->format($format), + ); + } +}