diff --git a/Makefile b/Makefile
index ba7b8ffe..7cad2c6f 100644
--- a/Makefile
+++ b/Makefile
@@ -22,8 +22,8 @@ compare-benchmark-to-reference:
./vendor/bin/phpbench run --config config/phpbench.json --ref=benchmark_reference
static-analysis: ## run static analysis checks
- ./vendor/bin/psalm -c config/psalm.xml --show-info=true --no-cache --threads=1
- ./vendor/bin/psalm -c config/psalm.xml tests/static-analysis --no-cache --threads=1
+ ./vendor/bin/psalm -c config/psalm.xml --show-info=true --no-cache --threads=2
+ ./vendor/bin/psalm -c config/psalm.xml tests/static-analysis --no-cache --threads=2
type-coverage: ## send static analysis type coverage metrics to https://shepherd.dev/
./vendor/bin/psalm -c config/psalm.xml --shepherd --stats --threads=1
diff --git a/composer.lock b/composer.lock
index d795a738..8598a993 100644
--- a/composer.lock
+++ b/composer.lock
@@ -171,16 +171,16 @@
},
{
"name": "amphp/byte-stream",
- "version": "v1.8.1",
+ "version": "v1.8.2",
"source": {
"type": "git",
"url": "https://github.com/amphp/byte-stream.git",
- "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd"
+ "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/amphp/byte-stream/zipball/acbd8002b3536485c997c4e019206b3f10ca15bd",
- "reference": "acbd8002b3536485c997c4e019206b3f10ca15bd",
+ "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc",
+ "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc",
"shasum": ""
},
"require": {
@@ -196,11 +196,6 @@
"psalm/phar": "^3.11.4"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.x-dev"
- }
- },
"autoload": {
"files": [
"lib/functions.php"
@@ -224,7 +219,7 @@
}
],
"description": "A stream abstraction to make working with non-blocking I/O simple.",
- "homepage": "http://amphp.org/byte-stream",
+ "homepage": "https://amphp.org/byte-stream",
"keywords": [
"amp",
"amphp",
@@ -234,9 +229,8 @@
"stream"
],
"support": {
- "irc": "irc://irc.freenode.org/amphp",
"issues": "https://github.com/amphp/byte-stream/issues",
- "source": "https://github.com/amphp/byte-stream/tree/v1.8.1"
+ "source": "https://github.com/amphp/byte-stream/tree/v1.8.2"
},
"funding": [
{
@@ -244,7 +238,71 @@
"type": "github"
}
],
- "time": "2021-03-30T17:13:30+00:00"
+ "time": "2024-04-13T18:00:56+00:00"
+ },
+ {
+ "name": "clue/ndjson-react",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/clue/reactphp-ndjson.git",
+ "reference": "392dc165fce93b5bb5c637b67e59619223c931b0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0",
+ "reference": "392dc165fce93b5bb5c637b67e59619223c931b0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3",
+ "react/stream": "^1.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35",
+ "react/event-loop": "^1.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Clue\\React\\NDJson\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering"
+ }
+ ],
+ "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.",
+ "homepage": "https://github.com/clue/reactphp-ndjson",
+ "keywords": [
+ "NDJSON",
+ "json",
+ "jsonlines",
+ "newline",
+ "reactphp",
+ "streaming"
+ ],
+ "support": {
+ "issues": "https://github.com/clue/reactphp-ndjson/issues",
+ "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://clue.engineering/support",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/clue",
+ "type": "github"
+ }
+ ],
+ "time": "2022-12-23T10:58:28+00:00"
},
{
"name": "colinodell/json5",
@@ -339,16 +397,16 @@
},
{
"name": "composer/pcre",
- "version": "3.1.3",
+ "version": "3.1.4",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
- "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8"
+ "reference": "04229f163664973f68f38f6f73d917799168ef24"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
- "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24",
+ "reference": "04229f163664973f68f38f6f73d917799168ef24",
"shasum": ""
},
"require": {
@@ -390,7 +448,7 @@
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
- "source": "https://github.com/composer/pcre/tree/3.1.3"
+ "source": "https://github.com/composer/pcre/tree/3.1.4"
},
"funding": [
{
@@ -406,7 +464,7 @@
"type": "tidelift"
}
],
- "time": "2024-03-19T10:26:25+00:00"
+ "time": "2024-05-27T13:40:54+00:00"
},
{
"name": "composer/semver",
@@ -491,16 +549,16 @@
},
{
"name": "composer/xdebug-handler",
- "version": "3.0.4",
+ "version": "3.0.5",
"source": {
"type": "git",
"url": "https://github.com/composer/xdebug-handler.git",
- "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255"
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255",
- "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
"shasum": ""
},
"require": {
@@ -537,7 +595,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/xdebug-handler/issues",
- "source": "https://github.com/composer/xdebug-handler/tree/3.0.4"
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
},
"funding": [
{
@@ -553,7 +611,7 @@
"type": "tidelift"
}
],
- "time": "2024-03-26T18:29:49+00:00"
+ "time": "2024-05-06T16:37:16+00:00"
},
{
"name": "dnoegel/php-xdg-base-dir",
@@ -862,6 +920,53 @@
],
"time": "2024-02-05T11:56:58+00:00"
},
+ {
+ "name": "evenement/evenement",
+ "version": "v3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/igorw/evenement.git",
+ "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
+ "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9 || ^6"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Evenement\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ }
+ ],
+ "description": "Événement is a very simple event dispatching library for PHP",
+ "keywords": [
+ "event-dispatcher",
+ "event-emitter"
+ ],
+ "support": {
+ "issues": "https://github.com/igorw/evenement/issues",
+ "source": "https://github.com/igorw/evenement/tree/v3.0.2"
+ },
+ "time": "2023-08-08T05:53:35+00:00"
+ },
{
"name": "felixfbecker/advanced-json-rpc",
"version": "v3.2.1",
@@ -1026,25 +1131,32 @@
},
{
"name": "friendsofphp/php-cs-fixer",
- "version": "v3.52.1",
+ "version": "v3.58.0",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
- "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc"
+ "reference": "55d3483c80c09f91d876aa4e2971ce349d07310c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc",
- "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/55d3483c80c09f91d876aa4e2971ce349d07310c",
+ "reference": "55d3483c80c09f91d876aa4e2971ce349d07310c",
"shasum": ""
},
"require": {
+ "clue/ndjson-react": "^1.0",
"composer/semver": "^3.4",
"composer/xdebug-handler": "^3.0.3",
"ext-filter": "*",
"ext-json": "*",
"ext-tokenizer": "*",
+ "fidry/cpu-core-counter": "^1.0",
"php": "^7.4 || ^8.0",
+ "react/child-process": "^0.6.5",
+ "react/event-loop": "^1.0",
+ "react/promise": "^2.0 || ^3.0",
+ "react/socket": "^1.0",
+ "react/stream": "^1.0",
"sebastian/diff": "^4.0 || ^5.0 || ^6.0",
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0",
@@ -1059,6 +1171,7 @@
},
"require-dev": {
"facile-it/paraunit": "^1.3 || ^2.0",
+ "infection/infection": "^0.27.11",
"justinrainbow/json-schema": "^5.2",
"keradus/cli-executor": "^2.1",
"mikey179/vfsstream": "^1.6.11",
@@ -1106,7 +1219,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
- "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.1"
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.58.0"
},
"funding": [
{
@@ -1114,7 +1227,7 @@
"type": "github"
}
],
- "time": "2024-03-19T21:02:43+00:00"
+ "time": "2024-05-28T16:55:30+00:00"
},
{
"name": "guzzlehttp/guzzle",
@@ -1755,12 +1868,12 @@
"version": "v5.2.13",
"source": {
"type": "git",
- "url": "https://github.com/justinrainbow/json-schema.git",
+ "url": "https://github.com/jsonrainbow/json-schema.git",
"reference": "fbbe7e5d79f618997bc3332a6f49246036c45793"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793",
+ "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793",
"reference": "fbbe7e5d79f618997bc3332a6f49246036c45793",
"shasum": ""
},
@@ -1815,8 +1928,8 @@
"schema"
],
"support": {
- "issues": "https://github.com/justinrainbow/json-schema/issues",
- "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13"
+ "issues": "https://github.com/jsonrainbow/json-schema/issues",
+ "source": "https://github.com/jsonrainbow/json-schema/tree/v5.2.13"
},
"time": "2023-09-26T02:20:38+00:00"
},
@@ -2574,28 +2687,35 @@
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "5.3.0",
+ "version": "5.4.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
+ "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
- "reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c",
+ "reference": "9d07b3f7fdcf5efec5d1609cba3c19c5ea2bdc9c",
"shasum": ""
},
"require": {
+ "doctrine/deprecations": "^1.1",
"ext-filter": "*",
- "php": "^7.2 || ^8.0",
+ "php": "^7.4 || ^8.0",
"phpdocumentor/reflection-common": "^2.2",
- "phpdocumentor/type-resolver": "^1.3",
+ "phpdocumentor/type-resolver": "^1.7",
+ "phpstan/phpdoc-parser": "^1.7",
"webmozart/assert": "^1.9.1"
},
"require-dev": {
- "mockery/mockery": "~1.3.2",
- "psalm/phar": "^4.8"
+ "mockery/mockery": "~1.3.5",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpstan/phpstan-webmozart-assert": "^1.2",
+ "phpunit/phpunit": "^9.5",
+ "vimeo/psalm": "^5.13"
},
"type": "library",
"extra": {
@@ -2619,15 +2739,15 @@
},
{
"name": "Jaap van Otterdijk",
- "email": "account@ijaap.nl"
+ "email": "opensource@ijaap.nl"
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
- "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.1"
},
- "time": "2021-10-19T17:43:47+00:00"
+ "time": "2024-05-21T05:55:05+00:00"
},
{
"name": "phpdocumentor/type-resolver",
@@ -2689,16 +2809,16 @@
},
{
"name": "phpstan/phpdoc-parser",
- "version": "1.27.0",
+ "version": "1.29.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757"
+ "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/86e4d5a4b036f8f0be1464522f4c6b584c452757",
- "reference": "86e4d5a4b036f8f0be1464522f4c6b584c452757",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc",
+ "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc",
"shasum": ""
},
"require": {
@@ -2730,9 +2850,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
- "source": "https://github.com/phpstan/phpdoc-parser/tree/1.27.0"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0"
},
- "time": "2024-03-21T13:14:53+00:00"
+ "time": "2024-05-06T12:04:23+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -3055,16 +3175,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "9.6.18",
+ "version": "9.6.19",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04"
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04",
- "reference": "32c2c2d6580b1d8ab3c10b1e9e4dc263cc69bb04",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8",
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8",
"shasum": ""
},
"require": {
@@ -3138,7 +3258,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.18"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19"
},
"funding": [
{
@@ -3154,7 +3274,7 @@
"type": "tidelift"
}
],
- "time": "2024-03-21T12:07:32+00:00"
+ "time": "2024-04-05T04:35:58+00:00"
},
{
"name": "psr/cache",
@@ -3362,20 +3482,20 @@
},
{
"name": "psr/http-factory",
- "version": "1.0.2",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
- "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
- "reference": "e616d01114759c4c489f93b099585439f795fe35",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": ""
},
"require": {
- "php": ">=7.0.0",
+ "php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
@@ -3399,7 +3519,7 @@
"homepage": "https://www.php-fig.org/"
}
],
- "description": "Common interfaces for PSR-7 HTTP message factories",
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
@@ -3411,9 +3531,9 @@
"response"
],
"support": {
- "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ "source": "https://github.com/php-fig/http-factory"
},
- "time": "2023-04-10T20:10:41+00:00"
+ "time": "2024-04-15T12:06:14+00:00"
},
{
"name": "psr/http-message",
@@ -3562,6 +3682,536 @@
},
"time": "2019-03-08T08:55:37+00:00"
},
+ {
+ "name": "react/cache",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/cache.git",
+ "reference": "d47c472b64aa5608225f47965a484b75c7817d5b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b",
+ "reference": "d47c472b64aa5608225f47965a484b75c7817d5b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "react/promise": "^3.0 || ^2.0 || ^1.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async, Promise-based cache interface for ReactPHP",
+ "keywords": [
+ "cache",
+ "caching",
+ "promise",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/cache/issues",
+ "source": "https://github.com/reactphp/cache/tree/v1.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2022-11-30T15:59:55+00:00"
+ },
+ {
+ "name": "react/child-process",
+ "version": "v0.6.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/child-process.git",
+ "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43",
+ "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.0",
+ "react/event-loop": "^1.2",
+ "react/stream": "^1.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
+ "react/socket": "^1.8",
+ "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\ChildProcess\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Event-driven library for executing child processes with ReactPHP.",
+ "keywords": [
+ "event-driven",
+ "process",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/child-process/issues",
+ "source": "https://github.com/reactphp/child-process/tree/v0.6.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/WyriHaximus",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/clue",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-16T13:41:56+00:00"
+ },
+ {
+ "name": "react/dns",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/dns.git",
+ "reference": "c134600642fa615b46b41237ef243daa65bb64ec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/dns/zipball/c134600642fa615b46b41237ef243daa65bb64ec",
+ "reference": "c134600642fa615b46b41237ef243daa65bb64ec",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "react/cache": "^1.0 || ^0.6 || ^0.5",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3.0 || ^2.7 || ^1.2.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/async": "^4 || ^3 || ^2",
+ "react/promise-timer": "^1.9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Dns\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async DNS resolver for ReactPHP",
+ "keywords": [
+ "async",
+ "dns",
+ "dns-resolver",
+ "reactphp"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/dns/issues",
+ "source": "https://github.com/reactphp/dns/tree/v1.12.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2023-11-29T12:41:06+00:00"
+ },
+ {
+ "name": "react/event-loop",
+ "version": "v1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/event-loop.git",
+ "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
+ "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
+ },
+ "suggest": {
+ "ext-pcntl": "For signal handling support when using the StreamSelectLoop"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\EventLoop\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
+ "keywords": [
+ "asynchronous",
+ "event-loop"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/event-loop/issues",
+ "source": "https://github.com/reactphp/event-loop/tree/v1.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2023-11-13T13:48:05+00:00"
+ },
+ {
+ "name": "react/promise",
+ "version": "v3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/promise.git",
+ "reference": "8a164643313c71354582dc850b42b33fa12a4b63"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63",
+ "reference": "8a164643313c71354582dc850b42b33fa12a4b63",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "1.10.39 || 1.4.10",
+ "phpunit/phpunit": "^9.6 || ^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "React\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "A lightweight implementation of CommonJS Promises/A for PHP",
+ "keywords": [
+ "promise",
+ "promises"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/promise/issues",
+ "source": "https://github.com/reactphp/promise/tree/v3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2024-05-24T10:39:05+00:00"
+ },
+ {
+ "name": "react/socket",
+ "version": "v1.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/socket.git",
+ "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/socket/zipball/216d3aec0b87f04a40ca04f481e6af01bdd1d038",
+ "reference": "216d3aec0b87f04a40ca04f481e6af01bdd1d038",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.0",
+ "react/dns": "^1.11",
+ "react/event-loop": "^1.2",
+ "react/promise": "^3 || ^2.6 || ^1.2.1",
+ "react/stream": "^1.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
+ "react/async": "^4 || ^3 || ^2",
+ "react/promise-stream": "^1.4",
+ "react/promise-timer": "^1.10"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Socket\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
+ "keywords": [
+ "Connection",
+ "Socket",
+ "async",
+ "reactphp",
+ "stream"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/socket/issues",
+ "source": "https://github.com/reactphp/socket/tree/v1.15.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2023-12-15T11:02:10+00:00"
+ },
+ {
+ "name": "react/stream",
+ "version": "v1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/reactphp/stream.git",
+ "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/reactphp/stream/zipball/6fbc9672905c7d5a885f2da2fc696f65840f4a66",
+ "reference": "6fbc9672905c7d5a885f2da2fc696f65840f4a66",
+ "shasum": ""
+ },
+ "require": {
+ "evenement/evenement": "^3.0 || ^2.0 || ^1.0",
+ "php": ">=5.3.8",
+ "react/event-loop": "^1.2"
+ },
+ "require-dev": {
+ "clue/stream-filter": "~1.2",
+ "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "React\\Stream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering",
+ "homepage": "https://clue.engineering/"
+ },
+ {
+ "name": "Cees-Jan Kiewiet",
+ "email": "reactphp@ceesjankiewiet.nl",
+ "homepage": "https://wyrihaximus.net/"
+ },
+ {
+ "name": "Jan Sorgalla",
+ "email": "jsorgalla@gmail.com",
+ "homepage": "https://sorgalla.com/"
+ },
+ {
+ "name": "Chris Boden",
+ "email": "cboden@gmail.com",
+ "homepage": "https://cboden.dev/"
+ }
+ ],
+ "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
+ "keywords": [
+ "event-driven",
+ "io",
+ "non-blocking",
+ "pipe",
+ "reactphp",
+ "readable",
+ "stream",
+ "writable"
+ ],
+ "support": {
+ "issues": "https://github.com/reactphp/stream/issues",
+ "source": "https://github.com/reactphp/stream/tree/v1.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/reactphp",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2023-06-16T10:52:11+00:00"
+ },
{
"name": "roave/infection-static-analysis-plugin",
"version": "1.35.0",
@@ -4771,16 +5421,16 @@
},
{
"name": "spatie/array-to-xml",
- "version": "3.2.3",
+ "version": "3.3.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/array-to-xml.git",
- "reference": "c95fd4db94ec199f798d4b5b4a81757bd20d88ab"
+ "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/c95fd4db94ec199f798d4b5b4a81757bd20d88ab",
- "reference": "c95fd4db94ec199f798d4b5b4a81757bd20d88ab",
+ "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f56b220fe2db1ade4c88098d83413ebdfc3bf876",
+ "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876",
"shasum": ""
},
"require": {
@@ -4793,6 +5443,11 @@
"spatie/pest-plugin-snapshots": "^1.1"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
"Spatie\\ArrayToXml\\": "src"
@@ -4818,7 +5473,7 @@
"xml"
],
"support": {
- "source": "https://github.com/spatie/array-to-xml/tree/3.2.3"
+ "source": "https://github.com/spatie/array-to-xml/tree/3.3.0"
},
"funding": [
{
@@ -4830,20 +5485,20 @@
"type": "github"
}
],
- "time": "2024-02-07T10:39:02+00:00"
+ "time": "2024-05-01T10:20:27+00:00"
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.9.0",
+ "version": "3.10.1",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
- "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b"
+ "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b",
- "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877",
+ "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877",
"shasum": ""
},
"require": {
@@ -4910,20 +5565,20 @@
"type": "open_collective"
}
],
- "time": "2024-02-16T15:06:51+00:00"
+ "time": "2024-05-22T21:24:41+00:00"
},
{
"name": "symfony/config",
- "version": "v7.0.4",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "44deeba7233f08f383185ffa37dace3b3bc87364"
+ "reference": "f66f908a975500aa4594258bf454dc66e3939eac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/44deeba7233f08f383185ffa37dace3b3bc87364",
- "reference": "44deeba7233f08f383185ffa37dace3b3bc87364",
+ "url": "https://api.github.com/repos/symfony/config/zipball/f66f908a975500aa4594258bf454dc66e3939eac",
+ "reference": "f66f908a975500aa4594258bf454dc66e3939eac",
"shasum": ""
},
"require": {
@@ -4969,7 +5624,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/config/tree/v7.0.4"
+ "source": "https://github.com/symfony/config/tree/v7.0.7"
},
"funding": [
{
@@ -4985,20 +5640,20 @@
"type": "tidelift"
}
],
- "time": "2024-02-26T07:52:39+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/console",
- "version": "v7.0.4",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f"
+ "reference": "c981e0e9380ce9f146416bde3150c79197ce9986"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/6b099f3306f7c9c2d2786ed736d0026b2903205f",
- "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f",
+ "url": "https://api.github.com/repos/symfony/console/zipball/c981e0e9380ce9f146416bde3150c79197ce9986",
+ "reference": "c981e0e9380ce9f146416bde3150c79197ce9986",
"shasum": ""
},
"require": {
@@ -5062,7 +5717,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.0.4"
+ "source": "https://github.com/symfony/console/tree/v7.0.7"
},
"funding": [
{
@@ -5078,20 +5733,20 @@
"type": "tidelift"
}
],
- "time": "2024-02-22T20:27:20+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.4.0",
+ "version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
+ "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
- "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
+ "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
@@ -5100,7 +5755,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "3.4-dev"
+ "dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@@ -5129,7 +5784,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
@@ -5145,20 +5800,20 @@
"type": "tidelift"
}
],
- "time": "2023-05-23T14:45:45+00:00"
+ "time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/event-dispatcher",
- "version": "v7.0.3",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e"
+ "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e",
- "reference": "834c28d533dd0636f910909d01b9ff45cc094b5e",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db2a7fab994d67d92356bb39c367db115d9d30f9",
+ "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9",
"shasum": ""
},
"require": {
@@ -5209,7 +5864,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.3"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.7"
},
"funding": [
{
@@ -5225,20 +5880,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T15:02:46+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
- "version": "v3.4.0",
+ "version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
- "reference": "a76aed96a42d2b521153fb382d418e30d18b59df"
+ "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df",
- "reference": "a76aed96a42d2b521153fb382d418e30d18b59df",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50",
+ "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50",
"shasum": ""
},
"require": {
@@ -5248,7 +5903,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "3.4-dev"
+ "dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@@ -5285,7 +5940,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.0"
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0"
},
"funding": [
{
@@ -5301,26 +5956,27 @@
"type": "tidelift"
}
],
- "time": "2023-05-23T14:45:45+00:00"
+ "time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v7.0.3",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12"
+ "reference": "cc168be6fbdcdf3401f50ae863ee3818ed4338f5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/2890e3a825bc0c0558526c04499c13f83e1b6b12",
- "reference": "2890e3a825bc0c0558526c04499c13f83e1b6b12",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/cc168be6fbdcdf3401f50ae863ee3818ed4338f5",
+ "reference": "cc168be6fbdcdf3401f50ae863ee3818ed4338f5",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
- "symfony/polyfill-mbstring": "~1.8"
+ "symfony/polyfill-mbstring": "~1.8",
+ "symfony/process": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@@ -5348,7 +6004,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.0.3"
+ "source": "https://github.com/symfony/filesystem/tree/v7.0.7"
},
"funding": [
{
@@ -5364,20 +6020,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T15:02:46+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.0.0",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56"
+ "reference": "4d58f0f4fe95a30d7b538d71197135483560b97c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/6e5688d69f7cfc4ed4a511e96007e06c2d34ce56",
- "reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/4d58f0f4fe95a30d7b538d71197135483560b97c",
+ "reference": "4d58f0f4fe95a30d7b538d71197135483560b97c",
"shasum": ""
},
"require": {
@@ -5412,7 +6068,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.0.0"
+ "source": "https://github.com/symfony/finder/tree/v7.0.7"
},
"funding": [
{
@@ -5428,20 +6084,20 @@
"type": "tidelift"
}
],
- "time": "2023-10-31T17:59:56+00:00"
+ "time": "2024-04-28T11:44:19+00:00"
},
{
"name": "symfony/options-resolver",
- "version": "v7.0.0",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
- "reference": "700ff4096e346f54cb628ea650767c8130f1001f"
+ "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f",
- "reference": "700ff4096e346f54cb628ea650767c8130f1001f",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/23cc173858776ad451e31f053b1c9f47840b2cfa",
+ "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa",
"shasum": ""
},
"require": {
@@ -5479,7 +6135,7 @@
"options"
],
"support": {
- "source": "https://github.com/symfony/options-resolver/tree/v7.0.0"
+ "source": "https://github.com/symfony/options-resolver/tree/v7.0.7"
},
"funding": [
{
@@ -5495,7 +6151,7 @@
"type": "tidelift"
}
],
- "time": "2023-08-08T10:20:21+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -5973,16 +6629,16 @@
},
{
"name": "symfony/process",
- "version": "v7.0.4",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9"
+ "reference": "3839e56b94dd1dbd13235d27504e66baf23faba0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9",
- "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9",
+ "url": "https://api.github.com/repos/symfony/process/zipball/3839e56b94dd1dbd13235d27504e66baf23faba0",
+ "reference": "3839e56b94dd1dbd13235d27504e66baf23faba0",
"shasum": ""
},
"require": {
@@ -6014,7 +6670,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.0.4"
+ "source": "https://github.com/symfony/process/tree/v7.0.7"
},
"funding": [
{
@@ -6030,25 +6686,26 @@
"type": "tidelift"
}
],
- "time": "2024-02-22T20:27:20+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/service-contracts",
- "version": "v3.4.1",
+ "version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0"
+ "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0",
- "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
+ "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
"shasum": ""
},
"require": {
"php": ">=8.1",
- "psr/container": "^1.1|^2.0"
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
},
"conflict": {
"ext-psr": "<1.1|>=2"
@@ -6056,7 +6713,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "3.4-dev"
+ "dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
@@ -6096,7 +6753,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.4.1"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.5.0"
},
"funding": [
{
@@ -6112,20 +6769,20 @@
"type": "tidelift"
}
],
- "time": "2023-12-26T14:02:43+00:00"
+ "time": "2024-04-18T09:32:20+00:00"
},
{
"name": "symfony/stopwatch",
- "version": "v7.0.3",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
- "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112"
+ "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/stopwatch/zipball/983900d6fddf2b0cbaacacbbad07610854bd8112",
- "reference": "983900d6fddf2b0cbaacacbbad07610854bd8112",
+ "url": "https://api.github.com/repos/symfony/stopwatch/zipball/41a7a24aa1dc82adf46a06bc292d1923acfe6b84",
+ "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84",
"shasum": ""
},
"require": {
@@ -6158,7 +6815,7 @@
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/stopwatch/tree/v7.0.3"
+ "source": "https://github.com/symfony/stopwatch/tree/v7.0.7"
},
"funding": [
{
@@ -6174,20 +6831,20 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T15:02:46+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/string",
- "version": "v7.0.4",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b"
+ "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b",
- "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b",
+ "url": "https://api.github.com/repos/symfony/string/zipball/e405b5424dc2528e02e31ba26b83a79fd4eb8f63",
+ "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63",
"shasum": ""
},
"require": {
@@ -6244,7 +6901,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.0.4"
+ "source": "https://github.com/symfony/string/tree/v7.0.7"
},
"funding": [
{
@@ -6260,20 +6917,20 @@
"type": "tidelift"
}
],
- "time": "2024-02-01T13:17:36+00:00"
+ "time": "2024-04-18T09:29:19+00:00"
},
{
"name": "symfony/yaml",
- "version": "v7.0.3",
+ "version": "v7.0.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "2d4fca631c00700597e9442a0b2451ce234513d3"
+ "reference": "0d3916ae69ea28b59d94b60c4f2b50f4e25adb5c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/2d4fca631c00700597e9442a0b2451ce234513d3",
- "reference": "2d4fca631c00700597e9442a0b2451ce234513d3",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/0d3916ae69ea28b59d94b60c4f2b50f4e25adb5c",
+ "reference": "0d3916ae69ea28b59d94b60c4f2b50f4e25adb5c",
"shasum": ""
},
"require": {
@@ -6315,7 +6972,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v7.0.3"
+ "source": "https://github.com/symfony/yaml/tree/v7.0.7"
},
"funding": [
{
@@ -6331,7 +6988,7 @@
"type": "tidelift"
}
],
- "time": "2024-01-23T15:02:46+00:00"
+ "time": "2024-04-28T11:44:19+00:00"
},
{
"name": "thecodingmachine/safe",
@@ -6524,16 +7181,16 @@
},
{
"name": "vimeo/psalm",
- "version": "5.23.1",
+ "version": "5.24.0",
"source": {
"type": "git",
"url": "https://github.com/vimeo/psalm.git",
- "reference": "8471a896ccea3526b26d082f4461eeea467f10a4"
+ "reference": "462c80e31c34e58cc4f750c656be3927e80e550e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vimeo/psalm/zipball/8471a896ccea3526b26d082f4461eeea467f10a4",
- "reference": "8471a896ccea3526b26d082f4461eeea467f10a4",
+ "url": "https://api.github.com/repos/vimeo/psalm/zipball/462c80e31c34e58cc4f750c656be3927e80e550e",
+ "reference": "462c80e31c34e58cc4f750c656be3927e80e550e",
"shasum": ""
},
"require": {
@@ -6630,7 +7287,7 @@
"issues": "https://github.com/vimeo/psalm/issues",
"source": "https://github.com/vimeo/psalm"
},
- "time": "2024-03-11T20:33:46+00:00"
+ "time": "2024-05-01T19:32:08+00:00"
},
{
"name": "webmozart/assert",
@@ -6754,5 +7411,5 @@
"ext-intl": "*"
},
"platform-dev": [],
- "plugin-api-version": "2.2.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/config/.phpcs.xml b/config/.phpcs.xml
index a1c70fb1..4c03b766 100644
--- a/config/.phpcs.xml
+++ b/config/.phpcs.xml
@@ -30,6 +30,7 @@
error
+
diff --git a/docs/README.md b/docs/README.md
index 2dc967f7..3c88ee36 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -20,6 +20,7 @@
- [Psl\Collection](./component/collection.md)
- [Psl\Comparison](./component/comparison.md)
- [Psl\DataStructure](./component/data-structure.md)
+- [Psl\DateTime](./component/date-time.md)
- [Psl\Dict](./component/dict.md)
- [Psl\Encoding\Base64](./component/encoding-base64.md)
- [Psl\Encoding\Hex](./component/encoding-hex.md)
diff --git a/docs/component/date-time.md b/docs/component/date-time.md
new file mode 100644
index 00000000..13c3fb3a
--- /dev/null
+++ b/docs/component/date-time.md
@@ -0,0 +1,65 @@
+
+
+[*index](./../README.md)
+
+---
+
+### `Psl\DateTime` Component
+
+#### `Constants`
+
+- [DAYS_PER_WEEK](./../../src/Psl/DateTime/constants.php#L0)
+- [HOURS_PER_DAY](./../../src/Psl/DateTime/constants.php#L0)
+- [HOURS_PER_WEEK](./../../src/Psl/DateTime/constants.php#L0)
+- [MICROSECONDS_PER_MILLISECOND](./../../src/Psl/DateTime/constants.php#L0)
+- [MICROSECONDS_PER_SECOND](./../../src/Psl/DateTime/constants.php#L0)
+- [MILLISECONDS_PER_SECOND](./../../src/Psl/DateTime/constants.php#L0)
+- [MINUTES_PER_DAY](./../../src/Psl/DateTime/constants.php#L0)
+- [MINUTES_PER_HOUR](./../../src/Psl/DateTime/constants.php#L0)
+- [MINUTES_PER_WEEK](./../../src/Psl/DateTime/constants.php#L0)
+- [MONTHS_PER_YEAR](./../../src/Psl/DateTime/constants.php#L0)
+- [NANOSECONDS_PER_MICROSECOND](./../../src/Psl/DateTime/constants.php#L0)
+- [NANOSECONDS_PER_MILLISECOND](./../../src/Psl/DateTime/constants.php#L0)
+- [NANOSECONDS_PER_SECOND](./../../src/Psl/DateTime/constants.php#L0)
+- [SECONDS_PER_DAY](./../../src/Psl/DateTime/constants.php#L0)
+- [SECONDS_PER_HOUR](./../../src/Psl/DateTime/constants.php#L0)
+- [SECONDS_PER_MINUTE](./../../src/Psl/DateTime/constants.php#L0)
+- [SECONDS_PER_WEEK](./../../src/Psl/DateTime/constants.php#L0)
+
+#### `Functions`
+
+- [is_leap_year](./../../src/Psl/DateTime/is_leap_year.php#L17)
+
+#### `Interfaces`
+
+- [DateTimeInterface](./../../src/Psl/DateTime/DateTimeInterface.php#L9)
+- [TemporalInterface](./../../src/Psl/DateTime/TemporalInterface.php#L20)
+
+#### `Classes`
+
+- [DateTime](./../../src/Psl/DateTime/DateTime.php#L13)
+- [Duration](./../../src/Psl/DateTime/Duration.php#L30)
+- [Timestamp](./../../src/Psl/DateTime/Timestamp.php#L16)
+
+#### `Traits`
+
+- [DateTimeConvenienceMethodsTrait](./../../src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php#L15)
+- [TemporalConvenienceMethodsTrait](./../../src/Psl/DateTime/TemporalConvenienceMethodsTrait.php#L16)
+
+#### `Enums`
+
+- [DateStyle](./../../src/Psl/DateTime/DateStyle.php#L23)
+- [Era](./../../src/Psl/DateTime/Era.php#L14)
+- [FormatPattern](./../../src/Psl/DateTime/FormatPattern.php#L15)
+- [Meridiem](./../../src/Psl/DateTime/Meridiem.php#L14)
+- [Month](./../../src/Psl/DateTime/Month.php#L15)
+- [SecondsStyle](./../../src/Psl/DateTime/SecondsStyle.php#L13)
+- [TimeStyle](./../../src/Psl/DateTime/TimeStyle.php#L23)
+- [Timezone](./../../src/Psl/DateTime/Timezone.php#L21)
+- [Weekday](./../../src/Psl/DateTime/Weekday.php#L15)
+
+
diff --git a/docs/component/file.md b/docs/component/file.md
index d280981d..28374bb6 100644
--- a/docs/component/file.md
+++ b/docs/component/file.md
@@ -28,9 +28,9 @@
#### `Classes`
- [Lock](./../../src/Psl/File/Lock.php#L9)
-- [ReadHandle](./../../src/Psl/File/ReadHandle.php#L10)
-- [ReadWriteHandle](./../../src/Psl/File/ReadWriteHandle.php#L11)
-- [WriteHandle](./../../src/Psl/File/WriteHandle.php#L11)
+- [ReadHandle](./../../src/Psl/File/ReadHandle.php#L11)
+- [ReadWriteHandle](./../../src/Psl/File/ReadWriteHandle.php#L12)
+- [WriteHandle](./../../src/Psl/File/WriteHandle.php#L12)
#### `Enums`
diff --git a/docs/component/io.md b/docs/component/io.md
index 66083467..b5f18496 100644
--- a/docs/component/io.md
+++ b/docs/component/io.md
@@ -16,7 +16,7 @@
- [input_handle](./../../src/Psl/IO/input_handle.php#L20)
- [output_handle](./../../src/Psl/IO/output_handle.php#L20)
- [pipe](./../../src/Psl/IO/pipe.php#L24)
-- [streaming](./../../src/Psl/IO/streaming.php#L38)
+- [streaming](./../../src/Psl/IO/streaming.php#L41)
- [write](./../../src/Psl/IO/write.php#L21)
- [write_error](./../../src/Psl/IO/write_error.php#L23)
- [write_error_line](./../../src/Psl/IO/write_error_line.php#L23)
@@ -41,7 +41,7 @@
- [CloseWriteHandleInterface](./../../src/Psl/IO/CloseWriteHandleInterface.php#L7)
- [CloseWriteStreamHandleInterface](./../../src/Psl/IO/CloseWriteStreamHandleInterface.php#L9)
- [HandleInterface](./../../src/Psl/IO/HandleInterface.php#L21)
-- [ReadHandleInterface](./../../src/Psl/IO/ReadHandleInterface.php#L10)
+- [ReadHandleInterface](./../../src/Psl/IO/ReadHandleInterface.php#L12)
- [ReadStreamHandleInterface](./../../src/Psl/IO/ReadStreamHandleInterface.php#L9)
- [ReadWriteHandleInterface](./../../src/Psl/IO/ReadWriteHandleInterface.php#L7)
- [ReadWriteStreamHandleInterface](./../../src/Psl/IO/ReadWriteStreamHandleInterface.php#L9)
@@ -54,32 +54,32 @@
- [SeekWriteHandleInterface](./../../src/Psl/IO/SeekWriteHandleInterface.php#L7)
- [SeekWriteStreamHandleInterface](./../../src/Psl/IO/SeekWriteStreamHandleInterface.php#L9)
- [StreamHandleInterface](./../../src/Psl/IO/StreamHandleInterface.php#L9)
-- [WriteHandleInterface](./../../src/Psl/IO/WriteHandleInterface.php#L10)
+- [WriteHandleInterface](./../../src/Psl/IO/WriteHandleInterface.php#L12)
- [WriteStreamHandleInterface](./../../src/Psl/IO/WriteStreamHandleInterface.php#L9)
#### `Classes`
-- [CloseReadStreamHandle](./../../src/Psl/IO/CloseReadStreamHandle.php#L12)
-- [CloseReadWriteStreamHandle](./../../src/Psl/IO/CloseReadWriteStreamHandle.php#L12)
-- [CloseSeekReadStreamHandle](./../../src/Psl/IO/CloseSeekReadStreamHandle.php#L12)
-- [CloseSeekReadWriteStreamHandle](./../../src/Psl/IO/CloseSeekReadWriteStreamHandle.php#L12)
+- [CloseReadStreamHandle](./../../src/Psl/IO/CloseReadStreamHandle.php#L13)
+- [CloseReadWriteStreamHandle](./../../src/Psl/IO/CloseReadWriteStreamHandle.php#L13)
+- [CloseSeekReadStreamHandle](./../../src/Psl/IO/CloseSeekReadStreamHandle.php#L13)
+- [CloseSeekReadWriteStreamHandle](./../../src/Psl/IO/CloseSeekReadWriteStreamHandle.php#L13)
- [CloseSeekStreamHandle](./../../src/Psl/IO/CloseSeekStreamHandle.php#L10)
-- [CloseSeekWriteStreamHandle](./../../src/Psl/IO/CloseSeekWriteStreamHandle.php#L12)
+- [CloseSeekWriteStreamHandle](./../../src/Psl/IO/CloseSeekWriteStreamHandle.php#L13)
- [CloseStreamHandle](./../../src/Psl/IO/CloseStreamHandle.php#L10)
-- [CloseWriteStreamHandle](./../../src/Psl/IO/CloseWriteStreamHandle.php#L12)
-- [MemoryHandle](./../../src/Psl/IO/MemoryHandle.php#L13)
-- [ReadStreamHandle](./../../src/Psl/IO/ReadStreamHandle.php#L12)
-- [ReadWriteStreamHandle](./../../src/Psl/IO/ReadWriteStreamHandle.php#L12)
-- [Reader](./../../src/Psl/IO/Reader.php#L16)
-- [SeekReadStreamHandle](./../../src/Psl/IO/SeekReadStreamHandle.php#L12)
-- [SeekReadWriteStreamHandle](./../../src/Psl/IO/SeekReadWriteStreamHandle.php#L12)
+- [CloseWriteStreamHandle](./../../src/Psl/IO/CloseWriteStreamHandle.php#L13)
+- [MemoryHandle](./../../src/Psl/IO/MemoryHandle.php#L14)
+- [ReadStreamHandle](./../../src/Psl/IO/ReadStreamHandle.php#L13)
+- [ReadWriteStreamHandle](./../../src/Psl/IO/ReadWriteStreamHandle.php#L13)
+- [Reader](./../../src/Psl/IO/Reader.php#L17)
+- [SeekReadStreamHandle](./../../src/Psl/IO/SeekReadStreamHandle.php#L13)
+- [SeekReadWriteStreamHandle](./../../src/Psl/IO/SeekReadWriteStreamHandle.php#L13)
- [SeekStreamHandle](./../../src/Psl/IO/SeekStreamHandle.php#L10)
-- [SeekWriteStreamHandle](./../../src/Psl/IO/SeekWriteStreamHandle.php#L12)
-- [WriteStreamHandle](./../../src/Psl/IO/WriteStreamHandle.php#L12)
+- [SeekWriteStreamHandle](./../../src/Psl/IO/SeekWriteStreamHandle.php#L13)
+- [WriteStreamHandle](./../../src/Psl/IO/WriteStreamHandle.php#L13)
#### `Traits`
-- [ReadHandleConvenienceMethodsTrait](./../../src/Psl/IO/ReadHandleConvenienceMethodsTrait.php#L15)
-- [WriteHandleConvenienceMethodsTrait](./../../src/Psl/IO/WriteHandleConvenienceMethodsTrait.php#L16)
+- [ReadHandleConvenienceMethodsTrait](./../../src/Psl/IO/ReadHandleConvenienceMethodsTrait.php#L16)
+- [WriteHandleConvenienceMethodsTrait](./../../src/Psl/IO/WriteHandleConvenienceMethodsTrait.php#L17)
diff --git a/docs/component/shell.md b/docs/component/shell.md
index 364fdd49..90a27c32 100644
--- a/docs/component/shell.md
+++ b/docs/component/shell.md
@@ -12,7 +12,7 @@
#### `Functions`
-- [execute](./../../src/Psl/Shell/execute.php#L41)
+- [execute](./../../src/Psl/Shell/execute.php#L42)
- [stream_unpack](./../../src/Psl/Shell/stream_unpack.php#L30)
- [unpack](./../../src/Psl/Shell/unpack.php#L16)
diff --git a/docs/component/tcp.md b/docs/component/tcp.md
index 8c208bab..11694b10 100644
--- a/docs/component/tcp.md
+++ b/docs/component/tcp.md
@@ -12,7 +12,7 @@
#### `Functions`
-- [connect](./../../src/Psl/TCP/connect.php#L18)
+- [connect](./../../src/Psl/TCP/connect.php#L19)
#### `Classes`
diff --git a/docs/component/unix.md b/docs/component/unix.md
index cec4b206..0b22b2bb 100644
--- a/docs/component/unix.md
+++ b/docs/component/unix.md
@@ -12,7 +12,7 @@
#### `Functions`
-- [connect](./../../src/Psl/Unix/connect.php#L18)
+- [connect](./../../src/Psl/Unix/connect.php#L19)
#### `Classes`
diff --git a/docs/documenter.php b/docs/documenter.php
index 1fd92dd4..2e020ef1 100644
--- a/docs/documenter.php
+++ b/docs/documenter.php
@@ -191,6 +191,7 @@ function get_all_components(): array
'Psl\\Collection',
'Psl\\Comparison',
'Psl\\DataStructure',
+ 'Psl\\DateTime',
'Psl\\Dict',
'Psl\\Encoding\\Base64',
'Psl\\Encoding\\Hex',
diff --git a/examples/async/usleep.php b/examples/async/usleep.php
index 44b0dcad..dafcbebb 100644
--- a/examples/async/usleep.php
+++ b/examples/async/usleep.php
@@ -5,22 +5,27 @@
namespace Psl\Example\IO;
use Psl\Async;
+use Psl\DateTime;
use Psl\IO;
require __DIR__ . '/../../vendor/autoload.php';
Async\main(static function (): int {
- $start = time();
+ $start = DateTime\Timestamp::monotonic();
Async\concurrently([
- static fn() => Async\sleep(2.0),
- static fn() => Async\sleep(2.0),
- static fn() => Async\sleep(2.0),
+ static fn() => Async\sleep(DateTime\Duration::hours(0)),
+ static fn() => Async\sleep(DateTime\Duration::minutes(0)),
+ static fn() => Async\sleep(DateTime\Duration::zero()),
+ static fn() => Async\sleep(DateTime\Duration::seconds(2)),
+ static fn() => Async\sleep(DateTime\Duration::nanoseconds(20000000)),
+ static fn() => Async\sleep(DateTime\Duration::microseconds(200000)),
+ static fn() => Async\sleep(DateTime\Duration::milliseconds(2000)),
]);
- $duration = time() - $start;
+ $duration = DateTime\Timestamp::monotonic()->since($start);
- IO\write_error_line("duration: %d.", $duration);
+ IO\write_error_line("duration : %s.", $duration->toString(max_decimals: 5));
return 0;
});
diff --git a/examples/channel/main.php b/examples/channel/main.php
index 1d899555..2f5faa5c 100644
--- a/examples/channel/main.php
+++ b/examples/channel/main.php
@@ -6,6 +6,7 @@
use Psl\Async;
use Psl\Channel;
+use Psl\DateTime\Duration;
use Psl\IO;
require __DIR__ . '/../../vendor/autoload.php';
@@ -16,7 +17,7 @@
*/
[$receiver, $sender] = Channel\unbounded();
-Async\Scheduler::delay(1, static function () use ($sender) {
+Async\Scheduler::delay(Duration::seconds(1), static function () use ($sender) {
$sender->send('Hello, World!');
});
diff --git a/examples/io/benchmark.php b/examples/io/benchmark.php
index 24454a36..219b3828 100644
--- a/examples/io/benchmark.php
+++ b/examples/io/benchmark.php
@@ -5,13 +5,15 @@
namespace Psl\Example\IO;
use Psl\Async;
+use Psl\DateTime;
use Psl\IO;
+use Psl\Math;
use Psl\Regex;
+
use function fopen;
use function getopt;
use function memory_get_peak_usage;
-use function microtime;
-use function round;
+
use const PHP_OS_FAMILY;
require __DIR__ . '/../../vendor/autoload.php';
@@ -26,7 +28,7 @@
$args = getopt('i:o:t:');
$input_file = $args['i'] ?? '/dev/zero';
$output_file = $args['o'] ?? '/dev/null';
- $seconds = (int)($args['t'] ?? 5);
+ $seconds = DateTime\Duration::seconds((int)($args['t'] ?? 5));
// passing file descriptors requires mapping paths (https://bugs.php.net/bug.php?id=53465)
$input_file = Regex\replace($input_file, '(^/dev/fd/)', 'php://fd/');
@@ -39,7 +41,7 @@
Async\Scheduler::delay($seconds, static fn() => $input->close());
- $start = microtime(true);
+ $start = DateTime\Timestamp::monotonic();
$i = 0;
try {
while ($chunk = $input->read(65536)) {
@@ -51,12 +53,12 @@
} catch (IO\Exception\AlreadyClosedException) {
}
- $seconds = microtime(true) - $start;
+ $duration = DateTime\Timestamp::monotonic()->since($start);
$bytes = $i * 65536;
- $bytes_formatted = round($bytes / 1024 / 1024 / $seconds, 1);
+ $bytes_formatted = Math\round($bytes / 1024 / 1024 / $duration->getTotalSeconds(), 1);
- IO\write_error_line('read %d byte(s) in %d second(s) => %dMiB/s', $bytes, round($seconds, 3), $bytes_formatted);
- IO\write_error_line('peak memory usage of %dMiB', round(memory_get_peak_usage(true) / 1024 / 1024, 1));
+ IO\write_error_line('read %d byte(s) in %s => %dMiB/s', $bytes, $duration->toString(), $bytes_formatted);
+ IO\write_error_line('peak memory usage of %dMiB', Math\round(memory_get_peak_usage(true) / 1024 / 1024, 1));
return 0;
});
diff --git a/examples/io/pipe.php b/examples/io/pipe.php
index 69f6d2bc..55e9b2c0 100644
--- a/examples/io/pipe.php
+++ b/examples/io/pipe.php
@@ -5,6 +5,7 @@
namespace Psl\Example\IO;
use Psl\Async;
+use Psl\DateTime\Duration;
use Psl\IO;
require __DIR__ . '/../../vendor/autoload.php';
@@ -16,7 +17,7 @@
static function() use($read): void {
IO\write_error_line("< sleeping.");
- Async\sleep(0.01);
+ Async\sleep(Duration::milliseconds(10));
IO\write_error_line("< waiting for content.");
@@ -30,7 +31,7 @@ static function() use($read): void {
static function() use($write): void {
IO\write_error_line('> sleeping.');
- Async\sleep(0.1);
+ Async\sleep(Duration::milliseconds(100));
IO\write_error_line('> writing.');
diff --git a/examples/io/queued.php b/examples/io/queued.php
index dc02ae57..ae344e77 100644
--- a/examples/io/queued.php
+++ b/examples/io/queued.php
@@ -15,7 +15,7 @@
$he = Async\run(static fn(): string => $read->readFixedSize(2));
- Async\sleep(0.001);
+ Async\sleep(Psl\DateTime\Duration::milliseconds(200));
$write->write("hello");
diff --git a/examples/run.php b/examples/run.php
index 45b60756..d2af7dbb 100644
--- a/examples/run.php
+++ b/examples/run.php
@@ -6,6 +6,7 @@
namespace Psl\Example\IO;
use Psl\Async;
+use Psl\DateTime;
use Psl\Filesystem;
use Psl\IO;
use Psl\Shell;
@@ -34,9 +35,9 @@
IO\write_error_line('- %s/%s -> started', $component, $script);
$awaitables[] = Async\run(static function() use($component, $script, $file): array {
- $start = microtime(true);
+ $start = DateTime\Timestamp::monotonic();
Shell\execute(PHP_BINARY, [$file]);
- $duration = microtime(true) - $start;
+ $duration = DateTime\Timestamp::monotonic()->since($start);
return [$component, $script, $duration];
});
@@ -46,7 +47,7 @@
foreach (Async\Awaitable::iterate($awaitables) as $awaitable) {
[$component, $script, $duration] = $awaitable->await();
- IO\write_error_line('+ %s/%s -> finished in %ds', $component, $script, $duration);
+ IO\write_error_line('+ %s/%s -> finished in %s', $component, $script, $duration->toString());
}
return 0;
diff --git a/examples/shell/timeout.php b/examples/shell/timeout.php
index 824068cc..8a9ebe68 100644
--- a/examples/shell/timeout.php
+++ b/examples/shell/timeout.php
@@ -5,6 +5,7 @@
namespace Psl\Example\Shell;
use Psl\Async;
+use Psl\DateTime;
use Psl\IO;
use Psl\Shell;
@@ -12,7 +13,7 @@
Async\main(static function (): void {
try {
- Shell\execute('sleep', ['1'], timeout: 0.5);
+ Shell\execute('sleep', ['1'], timeout: DateTime\Duration::milliseconds(500));
} catch (Shell\Exception\TimeoutException $exception) {
IO\write_error_line($exception->getMessage());
}
diff --git a/src/Psl/Async/OptionalIncrementalTimeout.php b/src/Psl/Async/OptionalIncrementalTimeout.php
index 4aa89451..78b511aa 100644
--- a/src/Psl/Async/OptionalIncrementalTimeout.php
+++ b/src/Psl/Async/OptionalIncrementalTimeout.php
@@ -5,8 +5,8 @@
namespace Psl\Async;
use Closure;
-
-use function microtime;
+use Psl\DateTime\Duration;
+use Psl\DateTime\Timestamp;
/**
* Manages optional incremental timeouts for asynchronous operations.
@@ -16,28 +16,40 @@
* particularly useful in asynchronous programming where operations
* might need to be interrupted or handled differently if they take
* too long to complete.
+ *
+ * @psalm-suppress MissingThrowsDocblock
*/
final class OptionalIncrementalTimeout
{
/**
- * @var ?float The end time in microseconds.
+ * @var ?Timestamp The end time.
*/
- private ?float $end;
+ private ?Timestamp $end;
/**
- * @var (Closure(): ?float) The handler to be called upon timeout.
+ * @var (Closure(): ?Duration) The handler to be called upon timeout.
*/
private Closure $handler;
/**
- * @param float|null $timeout The timeout duration in seconds. Null to disable timeout.
- * @param (Closure(): ?float) $handler The handler to be executed if the timeout is reached.
+ * @param null|Duration $timeout The timeout duration. Null to disable timeout.
+ * @param (Closure(): ?Duration) $handler The handler to be executed if the timeout is reached.
*/
- public function __construct(?float $timeout, Closure $handler)
+ public function __construct(?Duration $timeout, Closure $handler)
{
$this->handler = $handler;
- $this->end = $timeout !== null ? (microtime(true) + $timeout) : null;
+ if (null === $timeout) {
+ $this->end = null;
+
+ return;
+ }
+
+ if (!$timeout->isPositive()) {
+ $this->end = Timestamp::monotonic();
+ } else {
+ $this->end = Timestamp::monotonic()->plus($timeout);
+ }
}
/**
@@ -45,16 +57,16 @@ public function __construct(?float $timeout, Closure $handler)
*
* If the timeout has already been exceeded, the handler is invoked, and its return value is provided.
*
- * @return float|null The remaining time in seconds, null if no timeout is set, or the handler's return value if the timeout is exceeded.
+ * @return Duration|null The remaining time duration, null if no timeout is set, or the handler's return value if the timeout is exceeded.
*/
- public function getRemaining(): ?float
+ public function getRemaining(): ?Duration
{
if ($this->end === null) {
return null;
}
- $remaining = $this->end - microtime(true);
+ $remaining = $this->end->since(Timestamp::monotonic());
- return $remaining <= 0 ? ($this->handler)() : $remaining;
+ return $remaining->isPositive() ? $remaining : ($this->handler)();
}
}
diff --git a/src/Psl/Async/Scheduler.php b/src/Psl/Async/Scheduler.php
index 75c79dad..e499afbb 100644
--- a/src/Psl/Async/Scheduler.php
+++ b/src/Psl/Async/Scheduler.php
@@ -6,6 +6,7 @@
use Closure;
use Psl;
+use Psl\DateTime;
use Revolt\EventLoop;
use Revolt\EventLoop\Driver;
use Revolt\EventLoop\InvalidCallbackError;
@@ -114,35 +115,36 @@ public static function defer(Closure $callback): string
/**
* Delay the execution of a callback.
*
- * @param float $delay The amount of time, to delay the execution for in seconds.
+ * @param DateTime\Duration $delay The amount of time, to delay the execution for in seconds.
* @param Closure(string): void $callback The callback to delay.
*
* @return non-empty-string A unique identifier that can be used to cancel, enable or disable the callback.
*
* @see EventLoop::delay()
*/
- public static function delay(float $delay, Closure $callback): string
+ public static function delay(DateTime\Duration $delay, Closure $callback): string
{
/** @var non-empty-string */
- return EventLoop::delay($delay, $callback);
+ return EventLoop::delay($delay->getTotalSeconds(), $callback);
}
/**
* Repeatedly execute a callback.
*
- * @param float $interval The time interval, to wait between executions in seconds.
+ * @param DateTime\Duration $interval The time interval, to wait between executions in seconds.
* @param Closure(string): void $callback The callback to repeat.
*
* @return non-empty-string A unique identifier that can be used to cancel, enable or disable the callback.
*
* @see EventLoop::repeat()
*/
- public static function repeat(float $interval, Closure $callback): string
+ public static function repeat(DateTime\Duration $interval, Closure $callback): string
{
/** @var non-empty-string */
- return EventLoop::repeat($interval, $callback);
+ return EventLoop::repeat($interval->getTotalSeconds(), $callback);
}
+
/**
* Enable a callback to be active starting in the next tick.
*
diff --git a/src/Psl/Async/sleep.php b/src/Psl/Async/sleep.php
index 94a338c7..394ab5c7 100644
--- a/src/Psl/Async/sleep.php
+++ b/src/Psl/Async/sleep.php
@@ -4,15 +4,19 @@
namespace Psl\Async;
+use Psl\DateTime;
use Revolt\EventLoop;
/**
* Non-blocking sleep for the specified number of seconds.
*/
-function sleep(float $seconds): void
+function sleep(DateTime\Duration $duration): void
{
$suspension = EventLoop::getSuspension();
- $watcher = EventLoop::delay($seconds, static fn () => $suspension->resume());
+ $watcher = EventLoop::delay(
+ $duration->getTotalSeconds(),
+ static fn () => $suspension->resume(),
+ );
try {
$suspension->suspend();
diff --git a/src/Psl/DateTime/DateStyle.php b/src/Psl/DateTime/DateStyle.php
new file mode 100644
index 00000000..1821e2c5
--- /dev/null
+++ b/src/Psl/DateTime/DateStyle.php
@@ -0,0 +1,45 @@
+
+ */
+ private int $month;
+
+ /**
+ * @var int<1, 31>
+ */
+ private int $day;
+
+ /**
+ * @var int<0, 23>
+ */
+ private int $hours;
+
+ /**
+ * @var int<0, 59>
+ */
+ private int $minutes;
+
+ /**
+ * @var int<0, 59>
+ */
+ private int $seconds;
+
+ /**
+ * @var int<0, 999999999>
+ */
+ private int $nanoseconds;
+
+ /**
+ * Constructs a new date-time instance with specified components and timezone.
+ *
+ * This constructor initializes a date-time object with the provided year, month, day, hour, minute,
+ * second, and nanosecond components within the given timezone. It ensures that all components are within their
+ * valid ranges: nanoseconds [0, 999,999,999], seconds [0, 59], minutes [0, 59], hours [0, 23], month [1, 12],
+ * and day [1, 28-31] depending on the month and leap year status. The constructor validates these components and
+ * assigns them to the instance if they are valid. If any component is out of its valid range, an
+ * `Exception\InvalidDateTimeException` is thrown.
+ *
+ * @throws Exception\InvalidArgumentException If any of the date or time components are outside their valid ranges,
+ * indicating an invalid date-time configuration.
+ *
+ * @psalm-mutation-free
+ */
+ private function __construct(Timezone $timezone, Timestamp $timestamp, int $year, int $month, int $day, int $hours, int $minutes, int $seconds, int $nanoseconds)
+ {
+ if ($nanoseconds < 0 || $nanoseconds >= NANOSECONDS_PER_SECOND) {
+ throw Exception\InvalidArgumentException::forNanoseconds($nanoseconds);
+ }
+
+ if ($seconds < 0 || $seconds >= 60) {
+ throw Exception\InvalidArgumentException::forSeconds($seconds);
+ }
+
+ if ($minutes < 0 || $minutes >= 60) {
+ throw Exception\InvalidArgumentException::forMinutes($minutes);
+ }
+
+ if ($hours < 0 || $hours >= 24) {
+ throw Exception\InvalidArgumentException::forHours($hours);
+ }
+
+ if ($month < 1 || $month > 12) {
+ throw Exception\InvalidArgumentException::forMonth($month);
+ }
+
+ if ($day < 1 || $day > 31 || $day > Month::from($month)->getDaysForYear($year)) {
+ throw Exception\InvalidArgumentException::forDay($day, $month, $year);
+ }
+
+ $this->timestamp = $timestamp;
+ $this->timezone = $timezone;
+ $this->year = $year;
+ $this->month = $month;
+ $this->day = $day;
+ $this->hours = $hours;
+ $this->minutes = $minutes;
+ $this->seconds = $seconds;
+ $this->nanoseconds = $nanoseconds;
+ }
+
+ /**
+ * Creates a new {@see DateTime} instance representing the current moment.
+ *
+ * This static method returns a {@see DateTime} object set to the current date and time. If a specific timezone is
+ * provided, the returned {@see DateTime} will be adjusted to reflect the date and time in that timezone.
+ *
+ * @param null|Timezone $timezone Optional timezone. If null, uses the system's default timezone.
+ *
+ * @psalm-mutation-free
+ */
+ public static function now(?Timezone $timezone = null): DateTime
+ {
+ return self::fromTimestamp(Timestamp::now(), $timezone);
+ }
+
+ /**
+ * Creates a DateTime instance for a specific time on the current day within the specified timezone.
+ *
+ * This method facilitates the creation of a {@see DateTime} object representing a precise time on today's date. It is
+ * particularly useful when you need to set a specific time of day for the current date in a given timezone. The
+ * method combines the current date context with a specific time, offering a convenient way to specify times such
+ * as "today at 14:00" in code.
+ *
+ * The time components (hours, minutes, seconds, nanoseconds) must be within their valid ranges. The method
+ * enforces these constraints and throws an {@see Exception\InvalidArgumentException} if any component is out of bounds.
+ *
+ * @param int<0, 23> $hours The hour component of the time, ranging from 0 to 23.
+ * @param int<0, 59> $minutes The minute component of the time, ranging from 0 to 59.
+ * @param int<0, 59> $seconds The second component of the time, defaulting to 0, and ranging from 0 to 59.
+ * @param int<0, 999999999> $nanoseconds The nanosecond component of the time, defaulting to 0, and ranging from 0 to 999,999,999.
+ *
+ * @throws Exception\UnexpectedValueException If any of the provided time components do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public static function todayAt(int $hours, int $minutes, int $seconds = 0, int $nanoseconds = 0, ?Timezone $timezone = null): DateTime
+ {
+ return self::now($timezone)->withTime($hours, $minutes, $seconds, $nanoseconds);
+ }
+
+ /**
+ * Creates a {@see DateTime} instance from individual date and time components.
+ *
+ * This method constructs a DateTime object using the specified year, month, day, hour, minute, second,
+ * and nanosecond components within a given timezone. It validates each component against the Gregorian calendar
+ * to ensure the date and time are possible. For example, it checks for the correct range of months (1-12),
+ * days in a month (considering leap years), hours (0-23), minutes (0-59), and seconds (0-59).
+ *
+ * Note: In cases where the specified time occurs twice (such as during the end of daylight saving time), the earlier occurrence
+ * is returned. To obtain the later occurrence, you can adjust the returned instance using `->plusHours(1)`.
+ *
+ * @param Month|int<1, 12> $month
+ * @param int<1, 31> $day
+ * @param int<0, 23> $hours
+ * @param int<0, 59> $minutes
+ * @param int<0, 59> $seconds
+ * @param int<0, 999999999> $nanoseconds
+ *
+ * @throws Exception\UnexpectedValueException If any of the provided date or time components do not align with calendar expectations.
+ *
+ * @pure
+ *
+ * @psalm-suppress ImpureMethodCall
+ */
+ public static function fromParts(Timezone $timezone, int $year, Month|int $month, int $day, int $hours = 0, int $minutes = 0, int $seconds = 0, int $nanoseconds = 0): self
+ {
+ if ($month instanceof Month) {
+ $month = $month->value;
+ }
+
+ /**
+ * @var IntlCalendar $calendar
+ */
+ $calendar = IntlCalendar::createInstance(
+ Internal\to_intl_timezone($timezone),
+ );
+
+ $calendar->set($year, $month - 1, $day, $hours, $minutes, $seconds);
+
+ if ($seconds !== $calendar->get(IntlCalendar::FIELD_SECOND)) {
+ throw Exception\UnexpectedValueException::forSeconds($seconds, $calendar->get(IntlCalendar::FIELD_SECOND));
+ }
+
+ if ($minutes !== $calendar->get(IntlCalendar::FIELD_MINUTE)) {
+ throw Exception\UnexpectedValueException::forMinutes($minutes, $calendar->get(IntlCalendar::FIELD_MINUTE));
+ }
+
+ if ($hours !== $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY)) {
+ throw Exception\UnexpectedValueException::forHours($hours, $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY));
+ }
+
+ if ($day !== $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH)) {
+ throw Exception\UnexpectedValueException::forDay($day, $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH));
+ }
+
+ if ($month !== ($calendar->get(IntlCalendar::FIELD_MONTH) + 1)) {
+ throw Exception\UnexpectedValueException::forMonth($month, $calendar->get(IntlCalendar::FIELD_MONTH) + 1);
+ }
+
+ if ($year !== $calendar->get(IntlCalendar::FIELD_YEAR)) {
+ throw Exception\UnexpectedValueException::forYear($year, $calendar->get(IntlCalendar::FIELD_YEAR));
+ }
+
+ $timestamp_in_seconds = (int) ($calendar->getTime() / ((float) MILLISECONDS_PER_SECOND));
+ /** @psalm-suppress MissingThrowsDocblock */
+ $timestamp = Timestamp::fromParts($timestamp_in_seconds, $nanoseconds);
+
+ /** @psalm-suppress MissingThrowsDocblock */
+ return new self(
+ $timezone,
+ $timestamp,
+ $year,
+ $month,
+ $day,
+ $hours,
+ $minutes,
+ $seconds,
+ $nanoseconds
+ );
+ }
+
+ /**
+ * Creates a {@see DateTime} instance from a timestamp, representing the same point in time.
+ *
+ * This method converts a {@see Timestamp} into a {@see DateTime} instance calculated for the specified timezone.
+ *
+ * @param null|Timezone $timezone Optional timezone. If null, uses the system's default timezone.
+ *
+ * @see Timezone::default()
+ *
+ * @psalm-mutation-free
+ *
+ * @psalm-suppress ImpureMethodCall
+ */
+ public static function fromTimestamp(Timestamp $timestamp, ?Timezone $timezone = null): static
+ {
+ $timezone ??= Timezone::default();
+
+ /** @var IntlCalendar $calendar */
+ $calendar = IntlCalendar::createInstance(
+ Internal\to_intl_timezone($timezone),
+ );
+
+ $calendar->setTime(
+ $timestamp->getSeconds() * MILLISECONDS_PER_SECOND,
+ );
+
+ $year = $calendar->get(IntlCalendar::FIELD_YEAR);
+ $month = $calendar->get(IntlCalendar::FIELD_MONTH) + 1;
+ $day = $calendar->get(IntlCalendar::FIELD_DAY_OF_MONTH);
+ $hour = $calendar->get(IntlCalendar::FIELD_HOUR_OF_DAY);
+ $minute = $calendar->get(IntlCalendar::FIELD_MINUTE);
+ $second = $calendar->get(IntlCalendar::FIELD_SECOND);
+ $nanoseconds = $timestamp->getNanoseconds();
+
+ /** @psalm-suppress MissingThrowsDocblock */
+ return new static(
+ $timezone,
+ $timestamp,
+ $year,
+ $month,
+ $day,
+ $hour,
+ $minute,
+ $second,
+ $nanoseconds,
+ );
+ }
+
+
+ /**
+ * Parses a date and time string into an instance of {@see Timestamp} using a specific format pattern, with optional customization for timezone and locale.
+ *
+ * This method is specifically designed for cases where a custom format pattern is used to parse the input string.
+ *
+ * It allows for precise control over the parsing process by specifying the exact format pattern that matches the input string.
+ *
+ * Additionally, the method supports specifying a timezone and locale for parsing, enabling accurate interpretation of locale-specific formats.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $raw_string = '2023-03-15 12:00:00';
+ * $parsed_datetime = DateTime\DateTime::parse($raw_string, 'yyyy-MM-dd HH:mm:ss', DateTime\Timezone::Utc, Locale\Locale::English);
+ * ```
+ *
+ * @param string $raw_string The date and time string to parse.
+ * @param null|FormatPattern|string $pattern The custom format pattern for parsing the date and time. If null, uses a default pattern.
+ * @param null|Timezone $timezone Optional timezone for parsing. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for parsing. If null, uses the system's default locale.
+ *
+ * @throws Exception\RuntimeException If the parsing process fails.
+ *
+ * @return static Returns an instance of {@see Timestamp} representing the parsed date and time.
+ *
+ * @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
+ * @see TemporalInterface::format()
+ *
+ * @psalm-mutation-free
+ */
+ public static function parse(string $raw_string, null|FormatPattern|string $pattern = null, ?Timezone $timezone = null, null|Locale $locale = null): static
+ {
+ $timezone ??= Timezone::default();
+
+ return self::fromTimestamp(Timestamp::parse($raw_string, $pattern, $timezone, $locale), $timezone);
+ }
+
+ /**
+ * Creates an instance of {@see DateTime} from a date and time string, formatted according to specified styles for date and time,
+ * with optional customization for timezone and locale.
+ *
+ * This method provides a more abstracted approach to parsing, allowing users to specify styles rather than a custom pattern.
+ *
+ * This is particularly useful for parsing strings that follow common date and time formats.
+ *
+ * Additionally, the timezone and locale parameters enable accurate parsing of strings in locale-specific formats.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $raw_string = "March 15, 2023, 12:00 PM";
+ *
+ * $datetime = DateTime\DateTime::fromString($raw_string, FormatDateStyle::Long, FormatTimeStyle::Short, DateTime\Timezone::Utc, Locale\Locale::English);
+ * ```
+ *
+ * @param string $raw_string The date and time string to parse.
+ * @param null|DateStyle $date_style The style for the date portion of the string. If null, a default style is used.
+ * @param null|TimeStyle $time_style The style for the time portion of the string. If null, a default style is used.
+ * @param null|Timezone $timezone Optional timezone for parsing. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for parsing. If null, uses the system's default locale.
+ *
+ * @throws Exception\RuntimeException If the parsing process fails.
+ *
+ * @return static Returns an instance of {@see DateTime} representing the parsed date and time.
+ *
+ * @see DateTimeInterface::toString()
+ *
+ * @psalm-mutation-free
+ */
+ public static function fromString(string $raw_string, null|DateStyle $date_style = null, null|TimeStyle $time_style = null, null|Timezone $timezone = null, null|Locale $locale = null): static
+ {
+ $timezone ??= Timezone::default();
+
+ return self::fromTimestamp(Timestamp::fromString($raw_string, $date_style, $time_style, $timezone, $locale), $timezone);
+ }
+
+ /**
+ * Returns the timestamp representation of this date time object.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTimestamp(): Timestamp
+ {
+ return $this->timestamp;
+ }
+
+ /**
+ * Retrieves the year as an integer, following ISO-8601 conventions for numbering.
+ *
+ * This method returns the year part of the date. For years in the Anno Domini (AD) era, the returned value matches
+ * the Gregorian calendar year directly (e.g., 1 for AD 1, 2021 for AD 2021, etc.). For years before AD 1, the method
+ * adheres to the ISO-8601 standard, which does not use a year zero: 1 BC is represented as 0, 2 BC as -1, 3 BC as -2,
+ * and so forth. This ISO-8601 numbering facilitates straightforward mathematical operations on years across the AD/BC
+ * divide but may require conversion for user-friendly display or when interfacing with systems that use the traditional
+ * AD/BC notation.
+ *
+ * @return int The year, formatted according to ISO-8601 standards, where 1 AD is 1, 1 BC is 0, 2 BC is -1, etc.
+ *
+ * @psalm-mutation-free
+ */
+ public function getYear(): int
+ {
+ return $this->year;
+ }
+
+ /**
+ * Returns the month.
+ *
+ * @return int<1, 12>
+ *
+ * @psalm-mutation-free
+ */
+ public function getMonth(): int
+ {
+ return $this->month;
+ }
+
+ /**
+ * Returns the day.
+ *
+ * @return int<1, 31>
+ *
+ * @psalm-mutation-free
+ */
+ public function getDay(): int
+ {
+ return $this->day;
+ }
+
+ /**
+ * Returns the hours.
+ *
+ * @return int<0, 23>
+ *
+ * @psalm-mutation-free
+ */
+ public function getHours(): int
+ {
+ return $this->hours;
+ }
+
+ /**
+ * Returns the minutes.
+ *
+ * @return int<0, 59>
+ *
+ * @psalm-mutation-free
+ */
+ public function getMinutes(): int
+ {
+ return $this->minutes;
+ }
+
+ /**
+ * Returns the seconds.
+ *
+ * @return int<0, 59>
+ *
+ * @psalm-mutation-free
+ */
+ public function getSeconds(): int
+ {
+ return $this->seconds;
+ }
+
+ /**
+ * Returns the nanoseconds.
+ *
+ * @return int<0, 999999999>
+ *
+ * @psalm-mutation-free
+ */
+ public function getNanoseconds(): int
+ {
+ return $this->nanoseconds;
+ }
+
+ /**
+ * Gets the timezone associated with the date and time.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTimezone(): Timezone
+ {
+ return $this->timezone;
+ }
+
+ /**
+ * Returns a new instance with the specified date.
+ *
+ * @param Month|int<1, 12> $month
+ * @param int<1, 31> $day
+ *
+ * @throws Exception\UnexpectedValueException If any of the provided date components do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withDate(int $year, Month|int $month, int $day): static
+ {
+ return static::fromParts(
+ $this->getTimezone(),
+ $year,
+ $month,
+ $day,
+ $this->getHours(),
+ $this->getMinutes(),
+ $this->getSeconds(),
+ $this->getNanoseconds(),
+ );
+ }
+
+ /**
+ * Returns a new instance with the specified time.
+ *
+ * @param int<0, 23> $hours
+ * @param int<0, 59> $minutes
+ * @param int<0, 59> $seconds
+ * @param int<0, 999999999> $nanoseconds
+ *
+ * @throws Exception\UnexpectedValueException If any of the provided time components do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withTime(int $hours, int $minutes, int $seconds = 0, int $nanoseconds = 0): static
+ {
+ return static::fromParts(
+ $this->getTimezone(),
+ $this->getYear(),
+ $this->getMonth(),
+ $this->getDay(),
+ $hours,
+ $minutes,
+ $seconds,
+ $nanoseconds,
+ );
+ }
+
+ public function jsonSerialize(): array
+ {
+ return [
+ 'timezone' => $this->timezone,
+ 'timestamp' => $this->timestamp,
+ 'year' => $this->year,
+ 'month' => $this->month,
+ 'day' => $this->day,
+ 'hours' => $this->hours,
+ 'minutes' => $this->minutes,
+ 'seconds' => $this->seconds,
+ 'nanoseconds' => $this->nanoseconds,
+ ];
+ }
+}
diff --git a/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php b/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php
new file mode 100644
index 00000000..986637a8
--- /dev/null
+++ b/src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php
@@ -0,0 +1,621 @@
+equals($other) && $this->getTimezone() === $other->getTimezone();
+ }
+
+ /**
+ * Obtains the timezone offset as a {@see Duration} object.
+ *
+ * This method effectively returns the offset from UTC for the timezone of this instance at the specific date and time it represents.
+ *
+ * It is equivalent to executing `$dt->getTimezone()->getOffset($dt)`, which calculates the offset for the timezone of this instance.
+ *
+ * @return Duration The offset from UTC as a Duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTimezoneOffset(): Duration
+ {
+ return $this->getTimezone()->getOffset($this);
+ }
+
+ /**
+ * Determines whether this instance is currently in daylight saving time.
+ *
+ * This method checks if the date and time represented by this instance fall within the daylight saving time period of its timezone.
+ *
+ * It is equivalent to `!$dt->getTimezone()->getDaylightSavingTimeOffset($dt)->isZero()`, indicating whether there is a non-zero DST offset.
+ *
+ * @return bool True if in daylight saving time, false otherwise.
+ *
+ * @psalm-mutation-free
+ */
+ public function isDaylightSavingTime(): bool
+ {
+ return !$this->getTimezone()->getDaylightSavingTimeOffset($this)->isZero();
+ }
+
+ /**
+ * Converts the {@see DateTimeInterface} instance to the specified timezone.
+ *
+ * @param Timezone $timezone The timezone to convert to.
+ *
+ * @psalm-mutation-free
+ */
+ public function convertToTimezone(Timezone $timezone): static
+ {
+ return static::fromTimestamp($this->getTimestamp(), $timezone);
+ }
+
+ /**
+ * Returns a new instance with the specified year.
+ *
+ * @throws Exception\UnexpectedValueException If the provided year do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withYear(int $year): static
+ {
+ return $this->withDate($year, $this->getMonth(), $this->getDay());
+ }
+
+ /**
+ * Returns a new instance with the specified month.
+ *
+ * @param Month|int<1, 12> $month
+ *
+ * @throws Exception\UnexpectedValueException If the provided month do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withMonth(Month|int $month): static
+ {
+ return $this->withDate($this->getYear(), $month, $this->getDay());
+ }
+
+ /**
+ * Returns a new instance with the specified day.
+ *
+ * @param int<1, 31> $day
+ *
+ * @throws Exception\UnexpectedValueException If the provided day do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withDay(int $day): static
+ {
+ return $this->withDate($this->getYear(), $this->getMonth(), $day);
+ }
+
+ /**
+ * Returns a new instance with the specified hours.
+ *
+ * @param int<0, 23> $hours
+ *
+ * @throws Exception\UnexpectedValueException If the provided hours do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withHours(int $hours): static
+ {
+ return $this->withTime($hours, $this->getMinutes(), $this->getSeconds(), $this->getNanoseconds());
+ }
+
+ /**
+ * Returns a new instance with the specified minutes.
+ *
+ * @param int<0, 59> $minutes
+ *
+ * @throws Exception\UnexpectedValueException If the provided minutes do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withMinutes(int $minutes): static
+ {
+ return $this->withTime($this->getHours(), $minutes, $this->getSeconds(), $this->getNanoseconds());
+ }
+
+ /**
+ * Returns a new instance with the specified seconds.
+ *
+ * @param int<0, 59> $seconds
+ *
+ * @throws Exception\UnexpectedValueException If the provided seconds do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withSeconds(int $seconds): static
+ {
+ return $this->withTime($this->getHours(), $this->getMinutes(), $seconds, $this->getNanoseconds());
+ }
+
+ /**
+ * Returns a new instance with the specified nanoseconds.
+ *
+ * @param int<0, 999999999> $nanoseconds
+ *
+ * @throws Exception\UnexpectedValueException If the provided nanoseconds do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withNanoseconds(int $nanoseconds): static
+ {
+ return $this->withTime($this->getHours(), $this->getMinutes(), $this->getSeconds(), $nanoseconds);
+ }
+
+ /**
+ * Returns the date (year, month, day).
+ *
+ * @return array{int, int<1, 12>, int<1, 31>} The date.
+ *
+ * @psalm-mutation-free
+ */
+ public function getDate(): array
+ {
+ return [$this->getYear(), $this->getMonth(), $this->getDay()];
+ }
+
+ /**
+ * Returns the time (hours, minutes, seconds, nanoseconds).
+ *
+ * @return array{
+ * int<0, 23>,
+ * int<0, 59>,
+ * int<0, 59>,
+ * int<0, 999999999>,
+ * }
+ *
+ * @psalm-mutation-free
+ */
+ public function getTime(): array
+ {
+ return [
+ $this->getHours(),
+ $this->getMinutes(),
+ $this->getSeconds(),
+ $this->getNanoseconds(),
+ ];
+ }
+
+ /**
+ * Returns the {@see DateTimeInterface} parts (year, month, day, hours, minutes, seconds, nanoseconds).
+ *
+ * @return array{
+ * int,
+ * int<1, 12>,
+ * int<1, 31>,
+ * int<0, 23>,
+ * int<0, 59>,
+ * int<0, 59>,
+ * int<0, 999999999>,
+ * }
+ *
+ * @psalm-mutation-free
+ */
+ public function getParts(): array
+ {
+ return [
+ $this->getYear(),
+ $this->getMonth(),
+ $this->getDay(),
+ $this->getHours(),
+ $this->getMinutes(),
+ $this->getSeconds(),
+ $this->getNanoseconds(),
+ ];
+ }
+
+ /**
+ * Retrieves the era of the date represented by this DateTime instance.
+ *
+ * This method returns an instance of the `Era` enum, which indicates whether the date
+ * falls in the Anno Domini (AD) or Before Christ (BC) era. The era is determined based on the year
+ * of the date this object represents, with years designated as BC being negative
+ * and years in AD being positive.
+ *
+ * @psalm-mutation-free
+ */
+ public function getEra(): Era
+ {
+ return Era::fromYear($this->getYear());
+ }
+
+ /**
+ * Returns the century number for the year stored in this object.
+ *
+ * @psalm-mutation-free
+ */
+ public function getCentury(): int
+ {
+ return (int)($this->getYear() / 100) + 1;
+ }
+
+ /**
+ * Returns the short format of the year (last 2 digits).
+ *
+ * @return int<-99, 99> The short format of the year.
+ *
+ * @psalm-mutation-free
+ */
+ public function getYearShort(): int
+ {
+ /** @var int<-99, 99> */
+ return (int) $this->format(pattern: 'yy');
+ }
+
+ /**
+ * Returns the month as an instance of the {@see Month} enum.
+ *
+ * This method converts the numeric representation of the month into its corresponding
+ * case in the {@see Month} enum, providing a type-safe way to work with months.
+ *
+ * @return Month The month as an enum case.
+ *
+ * @psalm-mutation-free
+ */
+ public function getMonthEnum(): Month
+ {
+ return Month::from($this->getMonth());
+ }
+
+ /**
+ * Returns the hours using the 12-hour format (1 to 12) along with the meridiem indicator.
+ *
+ * @return array{int<1, 12>, Meridiem} The hours and meridiem indicator.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTwelveHours(): array
+ {
+ return [
+ ($this->getHours() % 12 ?: 12),
+ ($this->getHours() < 12 ? Meridiem::AnteMeridiem : Meridiem::PostMeridiem),
+ ];
+ }
+
+ /**
+ * Retrieves the ISO-8601 year and week number corresponding to the date.
+ *
+ * This method returns an array consisting of two integers: the first represents the year, and the second
+ * represents the week number according to ISO-8601 standards, which ranges from 1 to 53. The week numbering
+ * follows the ISO-8601 specification, where a week starts on a Monday and the first week of the year is the
+ * one that contains at least four days of the new year.
+ *
+ * Due to the ISO-8601 week numbering rules, the returned year might not always match the Gregorian year
+ * obtained from `$this->getYear()`. Specifically:
+ *
+ * - The first few days of January might belong to the last week of the previous year if they fall before
+ * the first Thursday of January.
+ *
+ * - Conversely, the last days of December might be part of the first week of the following year if they
+ * extend beyond the last Thursday of December.
+ *
+ * Examples:
+ * - For the date 2020-01-01, it returns [2020, 1], indicating the first week of 2020.
+ * - For the date 2021-01-01, it returns [2020, 53], showing that this day is part of the last week of 2020
+ * according to ISO-8601.
+ *
+ * @return array{int, int<1, 53>}
+ *
+ * @psalm-mutation-free
+ */
+ public function getISOWeekNumber(): array
+ {
+ /** @var int<1, 53> $week */
+ $week = (int)$this->format(pattern: 'w');
+ $year = (int)$this->format(pattern: 'Y');
+
+ return [$year, $week];
+ }
+
+ /**
+ * Gets the weekday of the date.
+ *
+ * @return Weekday The weekday.
+ *
+ * @psalm-mutation-free
+ */
+ public function getWeekday(): Weekday
+ {
+ return Weekday::from((int) $this->format(pattern: 'e'));
+ }
+
+ /**
+ * Checks if the year is a leap year.
+ *
+ * @psalm-mutation-free
+ */
+ public function isLeapYear(): bool
+ {
+ return namespace\is_leap_year($this->getYear());
+ }
+
+ /**
+ * Adds the specified years to this date-time object, returning a new instance with the added years.
+ *
+ * @throws Exception\UnderflowException If adding the years results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the years results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusYears(int $years): static
+ {
+ return $this->plusMonths($years * MONTHS_PER_YEAR);
+ }
+
+ /**
+ * Subtracts the specified years from this date-time object, returning a new instance with the subtracted years.
+ *
+ * @throws Exception\UnderflowException If subtracting the years results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the years results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusYears(int $years): static
+ {
+ return $this->minusMonths($years * MONTHS_PER_YEAR);
+ }
+
+ /**
+ * Adds the specified months to this date-time object, returning a new instance with the added months.
+ *
+ * @throws Exception\UnderflowException If adding the months results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the months results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusMonths(int $months): static
+ {
+ if (0 === $months) {
+ return $this;
+ }
+
+ if (0 > $months) {
+ return $this->minusMonths(-$months);
+ }
+
+ $current_year = $this->getYear();
+ $current_month = $this->getMonthEnum();
+ $days_to_add = 0;
+ for ($i = 0; $i < $months; $i++) {
+ $total_months = $current_month->value + $i;
+ $target_year = $current_year + Math\div($total_months - 1, MONTHS_PER_YEAR);
+ $target_month = $total_months % MONTHS_PER_YEAR;
+ if ($target_month === 0) {
+ $target_month = 1;
+ }
+
+ $days_to_add += Month::from($target_month)->getDaysForYear($target_year);
+ }
+
+ return $this->plus(Duration::days($days_to_add));
+ }
+
+ /**
+ * Subtracts the specified months from this date-time object, returning a new instance with the subtracted months.
+ *
+ * @throws Exception\UnderflowException If subtracting the months results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the months results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusMonths(int $months): static
+ {
+ if (0 === $months) {
+ return $this;
+ }
+
+ if (0 > $months) {
+ return $this->plusMonths(-$months);
+ }
+
+ $current_year = $this->getYear();
+ $current_month = $this->getMonthEnum();
+ $days_to_subtract = 0;
+ for ($i = 0; $i < $months; $i++) {
+ // When subtracting, we need to move the current month back before the calculation
+ $total_months = $current_month->value - $i;
+ while ($total_months <= 0) {
+ $total_months += MONTHS_PER_YEAR; // Adjust month to be within 1-12
+ $current_year--; // Adjust year when wrapping
+ }
+
+ $target_month = ($total_months % MONTHS_PER_YEAR) ?: MONTHS_PER_YEAR;
+ $target_year = $current_year + Math\div($total_months - 1, MONTHS_PER_YEAR);
+
+ // Subtract days of the month we are moving into
+ $days_to_subtract += Month::from($target_month)->getDaysForYear($target_year);
+ }
+
+ return $this->minus(Duration::days($days_to_subtract));
+ }
+
+ /**
+ * Adds the specified days to this date-time object, returning a new instance with the added days.
+ *
+ * @throws Exception\UnderflowException If adding the days results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the days results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusDays(int $days): static
+ {
+ return $this->plus(Duration::days($days));
+ }
+
+ /**
+ * Subtracts the specified days from this date-time object, returning a new instance with the subtracted days.
+ *
+ * @throws Exception\UnderflowException If subtracting the days results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the days results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusDays(int $days): static
+ {
+ return $this->minus(Duration::days($days));
+ }
+
+ /**
+ * Adds the specified duration to this date-time object, returning a new instance with the added duration.
+ *
+ * @throws Exception\UnderflowException If adding the duration results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the duration results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plus(Duration $duration): static
+ {
+ return static::fromTimestamp($this->getTimestamp()->plus($duration), $this->timezone);
+ }
+
+ /**
+ * Subtracts the specified duration from this date-time object, returning a new instance with the subtracted duration.
+ *
+ * @throws Exception\UnderflowException If subtracting the duration results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the duration results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minus(Duration $duration): static
+ {
+ return static::fromTimestamp($this->getTimestamp()->minus($duration), $this->timezone);
+ }
+
+ /**
+ * Formats this {@see DateTimeInterface} instance based on a specific pattern, with optional customization for timezone and locale.
+ *
+ * This method allows for detailed customization of the output string by specifying a format pattern. If no pattern is provided,
+ * a default, implementation-specific pattern will be used. Additionally, the method supports specifying a timezone and locale
+ * for further customization of the formatted output. If these are not provided, system defaults will be used.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $formatted = $temporal->format('yyyy-MM-dd HH:mm:ss', $timezone, $locale);
+ * ```
+ *
+ * @param null|FormatPattern|string $pattern Optional custom format pattern for the date and time. If null, uses a default pattern.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the current timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The formatted date and time string, according to the specified pattern, timezone, and locale.
+ *
+ * @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
+ * @see Locale::default()
+ *
+ * @psalm-mutation-free
+ */
+ public function format(null|FormatPattern|string $pattern = null, null|Timezone $timezone = null, null|Locale $locale = null): string
+ {
+ $timestamp = $this->getTimestamp();
+
+ /**
+ * @psalm-suppress InvalidOperand
+ * @psalm-suppress ImpureMethodCall
+ */
+ return Internal\create_intl_date_formatter(null, null, $pattern, $timezone ?? $this->getTimezone(), $locale)
+ ->format($timestamp->getSeconds() + ($timestamp->getNanoseconds() / NANOSECONDS_PER_SECOND));
+ }
+
+ /**
+ * Formats this {@see DateTimeInterface} instance to a string based on the RFC 3339 format, with additional
+ * options for second fractions and timezone representation.
+ *
+ * The RFC 3339 format is widely adopted in web and network protocols for its unambiguous representation of date, time,
+ * and timezone information. This method not only ensures universal readability but also the precise specification
+ * of time across various systems, being compliant with both RFC 3339 and ISO 8601 standards.
+ *
+ * Example usage:
+ *
+ * ```php
+ * // Default formatting
+ * $rfc_formatted_string = $datetime->toRfc3339();
+ * // Customized formatting with milliseconds and 'Z' for UTC
+ * $rfc_formatted_string_with_milliseconds_and_z = $datetime->toRfc3339(SecondsStyle::Milliseconds, true);
+ * ```
+ *
+ * @param null|SecondsStyle $seconds_style Optional parameter to specify the seconds formatting style. Automatically
+ * selected based on precision if null.
+ * @param bool $use_z Determines the representation of UTC timezone. True to use 'Z', false to use the standard offset format.
+ *
+ * @return string The formatted string of the {@see DateTimeInterface} instance, adhering to the RFC 3339 and compatible with ISO 8601 formats.
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc3339
+ *
+ * @psalm-mutation-free
+ */
+ public function toRfc3339(?SecondsStyle $seconds_style = null, bool $use_z = false): string
+ {
+ return Internal\format_rfc3339($this->getTimestamp(), $seconds_style, $use_z, $this->getTimezone());
+ }
+
+ /**
+ * Provides a string representation of this {@see TemporalInterface} instance, formatted according to specified styles for date and time,
+ * and optionally adjusted for a specific timezone and locale.
+ *
+ * This method offers a higher-level abstraction for formatting, allowing users to specify styles for date and time separately
+ * rather than a custom pattern. If no styles are provided, default styles will be used.
+ *
+ * Additionally, the timezone and locale can be specified for locale-sensitive formatting.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $string_representation = $temporal->toString(FormatDateStyle::Long, FormatTimeStyle::Short, $timezone, $locale);
+ * ```
+ *
+ * @param null|DateStyle $date_style Optional style for the date portion of the output. If null, a default style is used.
+ * @param null|TimeStyle $time_style Optional style for the time portion of the output. If null, a default style is used.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the current timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The string representation of the date and time, formatted according to the specified styles, timezone, and locale.
+ *
+ * @see DateStyle::default()
+ * @see TimeStyle::default()
+ * @see Locale::default()
+ *
+ * @psalm-mutation-free
+ */
+ public function toString(null|DateStyle $date_style = null, null|TimeStyle $time_style = null, null|Timezone $timezone = null, null|Locale $locale = null): string
+ {
+ $timestamp = $this->getTimestamp();
+
+ /**
+ * @psalm-suppress InvalidOperand
+ * @psalm-suppress ImpureMethodCall
+ */
+ return Internal\create_intl_date_formatter($date_style, $time_style, null, $timezone ?? $this->getTimezone(), $locale)
+ ->format($timestamp->getSeconds() + ($timestamp->getNanoseconds() / NANOSECONDS_PER_SECOND));
+ }
+}
diff --git a/src/Psl/DateTime/DateTimeInterface.php b/src/Psl/DateTime/DateTimeInterface.php
new file mode 100644
index 00000000..c1f4edb3
--- /dev/null
+++ b/src/Psl/DateTime/DateTimeInterface.php
@@ -0,0 +1,545 @@
+getTimezone()->getOffset($dt)`, which calculates the offset for the timezone of this instance.
+ *
+ * @return Duration The offset from UTC as a Duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTimezoneOffset(): Duration;
+
+ /**
+ * Determines whether this instance is currently in daylight saving time.
+ *
+ * This method checks if the date and time represented by this instance fall within the daylight saving time period of its timezone.
+ *
+ * It is equivalent to `!$dt->getTimezone()->getDaylightSavingTimeOffset($dt)->isZero()`, indicating whether there is a non-zero DST offset.
+ *
+ * @return bool True if in daylight saving time, false otherwise.
+ *
+ * @psalm-mutation-free
+ */
+ public function isDaylightSavingTime(): bool;
+
+ /**
+ * Returns a new instance with the specified date.
+ *
+ * @param Month|int<1, 12> $month
+ * @param int<1, 31> $day
+ *
+ * @throws Exception\UnexpectedValueException If any of the provided date components do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withDate(int $year, Month|int $month, int $day): static;
+
+ /**
+ * Returns a new instance with the specified time.
+ *
+ * @param int<0, 23> $hours
+ * @param int<0, 59> $minutes
+ * @param int<0, 59> $seconds
+ * @param int<0, 999999999> $nanoseconds
+ *
+ * @throws Exception\UnexpectedValueException If any of the provided time components do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withTime(int $hours, int $minutes, int $seconds = 0, int $nanoseconds = 0): static;
+
+ /**
+ * Returns a new instance with the specified year.
+ *
+ * @throws Exception\UnexpectedValueException If the provided year do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withYear(int $year): static;
+
+ /**
+ * Returns a new instance with the specified month.
+ *
+ * @param Month|int<1, 12> $month
+ *
+ * @throws Exception\UnexpectedValueException If the provided month do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withMonth(Month|int $month): static;
+
+ /**
+ * Returns a new instance with the specified day.
+ *
+ * @param int<1, 31> $day
+ *
+ * @throws Exception\UnexpectedValueException If the provided day do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withDay(int $day): static;
+
+ /**
+ * Returns a new instance with the specified hours.
+ *
+ * @param int<0, 23> $hours
+ *
+ * @throws Exception\UnexpectedValueException If the provided hours do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withHours(int $hours): static;
+
+ /**
+ * Returns a new instance with the specified minutes.
+ *
+ * @param int<0, 59> $minutes
+ *
+ * @throws Exception\UnexpectedValueException If the provided minutes do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withMinutes(int $minutes): static;
+
+ /**
+ * Returns a new instance with the specified seconds.
+ *
+ * @param int<0, 59> $seconds
+ *
+ * @throws Exception\UnexpectedValueException If the provided seconds do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withSeconds(int $seconds): static;
+
+ /**
+ * Returns a new instance with the specified nanoseconds.
+ *
+ * @param int<0, 999999999> $nanoseconds
+ *
+ * @throws Exception\UnexpectedValueException If the provided nanoseconds do not align with calendar expectations.
+ *
+ * @psalm-mutation-free
+ */
+ public function withNanoseconds(int $nanoseconds): static;
+
+ /**
+ * Returns the date (year, month, day).
+ *
+ * @return array{int, int<1, 12>, int<1, 31>} The date.
+ *
+ * @psalm-mutation-free
+ */
+ public function getDate(): array;
+
+ /**
+ * Returns the time (hours, minutes, seconds, nanoseconds).
+ *
+ * @return array{
+ * int<0, 23>,
+ * int<0, 59>,
+ * int<0, 59>,
+ * int<0, 999999999>,
+ * }
+ *
+ * @psalm-mutation-free
+ */
+ public function getTime(): array;
+
+ /**
+ * Returns the date and time parts (year, month, day, hours, minutes, seconds, nanoseconds).
+ *
+ * @return array{
+ * int,
+ * int<1, 12>,
+ * int<1, 31>,
+ * int<0, 23>,
+ * int<0, 59>,
+ * int<0, 59>,
+ * int<0, 999999999>,
+ * }
+ *
+ * @psalm-mutation-free
+ */
+ public function getParts(): array;
+
+ /**
+ * Retrieves the era of the date represented by this DateTime instance.
+ *
+ * This method returns an instance of the `Era` enum, which indicates whether the date
+ * falls in the Anno Domini (AD) or Before Christ (BC) era. The era is determined based on the year
+ * of the date this object represents, with years designated as BC being negative
+ * and years in AD being positive.
+ *
+ * @psalm-mutation-free
+ */
+ public function getEra(): Era;
+
+ /**
+ * Returns the century number for the year stored in this object.
+ *
+ * @psalm-mutation-free
+ */
+ public function getCentury(): int;
+
+ /**
+ * Retrieves the year as an integer, following ISO-8601 conventions for numbering.
+ *
+ * This method returns the year part of the date. For years in the Anno Domini (AD) era, the returned value matches
+ * the Gregorian calendar year directly (e.g., 1 for AD 1, 2021 for AD 2021, etc.). For years before AD 1, the method
+ * adheres to the ISO-8601 standard, which does not use a year zero: 1 BC is represented as 0, 2 BC as -1, 3 BC as -2,
+ * and so forth. This ISO-8601 numbering facilitates straightforward mathematical operations on years across the AD/BC
+ * divide but may require conversion for user-friendly display or when interfacing with systems that use the traditional
+ * AD/BC notation.
+ *
+ * @return int The year, formatted according to ISO-8601 standards, where 1 AD is 1, 1 BC is 0, 2 BC is -1, etc.
+ *
+ * @psalm-mutation-free
+ */
+ public function getYear(): int;
+
+ /**
+ * Returns the short format of the year (last 2 digits).
+ *
+ * @return int<-99, 99> The short format of the year.
+ *
+ * @psalm-mutation-free
+ */
+ public function getYearShort(): int;
+
+ /**
+ * Returns the month.
+ *
+ * @return int<1, 12>
+ *
+ * @psalm-mutation-free
+ */
+ public function getMonth(): int;
+
+ /**
+ * Returns the month as an instance of the {@see Month} enum.
+ *
+ * This method converts the numeric representation of the month into its corresponding
+ * case in the {@see Month} enum, providing a type-safe way to work with months.
+ *
+ * @return Month The month as an enum case.
+ *
+ * @psalm-mutation-free
+ */
+ public function getMonthEnum(): Month;
+
+ /**
+ * Returns the day.
+ *
+ * @return int<0, 31>
+ *
+ * @psalm-mutation-free
+ */
+ public function getDay(): int;
+
+ /**
+ * Returns the hours.
+ *
+ * @return int<0, 23>
+ *
+ * @psalm-mutation-free
+ */
+ public function getHours(): int;
+
+ /**
+ * Returns the hours using the 12-hour format (1 to 12) along with the meridiem indicator.
+ *
+ * @return array{int<1, 12>, Meridiem} The hours and meridiem indicator.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTwelveHours(): array;
+
+ /**
+ * Returns the minutes.
+ *
+ * @return int<0, 59>
+ *
+ * @psalm-mutation-free
+ */
+ public function getMinutes(): int;
+
+ /**
+ * Returns the seconds.
+ *
+ * @return int<0, 59>
+ *
+ * @psalm-mutation-free
+ */
+ public function getSeconds(): int;
+
+ /**
+ * Returns the nanoseconds.
+ *
+ * @return int<0, 999999999>
+ *
+ * @psalm-mutation-free
+ */
+ public function getNanoseconds(): int;
+
+ /**
+ * Retrieves the ISO-8601 year and week number corresponding to the date.
+ *
+ * This method returns an array consisting of two integers: the first represents the year, and the second
+ * represents the week number according to ISO-8601 standards, which ranges from 1 to 53. The week numbering
+ * follows the ISO-8601 specification, where a week starts on a Monday and the first week of the year is the
+ * one that contains at least four days of the new year.
+ *
+ * Due to the ISO-8601 week numbering rules, the returned year might not always match the Gregorian year
+ * obtained from `$this->getYear()`. Specifically:
+ *
+ * - The first few days of January might belong to the last week of the previous year if they fall before
+ * the first Thursday of January.
+ *
+ * - Conversely, the last days of December might be part of the first week of the following year if they
+ * extend beyond the last Thursday of December.
+ *
+ * Examples:
+ * - For the date 2020-01-01, it returns [2020, 1], indicating the first week of 2020.
+ * - For the date 2021-01-01, it returns [2020, 53], showing that this day is part of the last week of 2020
+ * according to ISO-8601.
+ *
+ * @return array{int, int<1, 53>}
+ *
+ * @psalm-mutation-free
+ */
+ public function getISOWeekNumber(): array;
+
+ /**
+ * Gets the weekday of the date.
+ *
+ * @return Weekday The weekday.
+ *
+ * @psalm-mutation-free
+ */
+ public function getWeekday(): Weekday;
+
+ /**
+ * Checks if the year is a leap year.
+ *
+ * @psalm-mutation-free
+ */
+ public function isLeapYear(): bool;
+
+ /**
+ * Adds the specified years to this date-time object, returning a new instance with the added years.
+ *
+ * @throws Exception\UnderflowException If adding the years results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the years results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusYears(int $years): static;
+
+ /**
+ * Adds the specified months to this date-time object, returning a new instance with the added months.
+ *
+ * @throws Exception\UnderflowException If adding the months results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the months results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusMonths(int $months): static;
+
+ /**
+ * Adds the specified days to this date-time object, returning a new instance with the added days.
+ *
+ * @throws Exception\UnderflowException If adding the days results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the days results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusDays(int $days): static;
+
+ /**
+ * Subtracts the specified years from this date-time object, returning a new instance with the subtracted years.
+ *
+ * @throws Exception\UnderflowException If subtracting the years results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the years results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusYears(int $years): static;
+
+ /**
+ * Subtracts the specified months from this date-time object, returning a new instance with the subtracted months.
+ *
+ * @throws Exception\UnderflowException If subtracting the months results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the months results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusMonths(int $months): static;
+
+ /**
+ * Subtracts the specified days from this date-time object, returning a new instance with the subtracted days.
+ *
+ * @throws Exception\UnderflowException If subtracting the days results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the days results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusDays(int $days): static;
+
+ /**
+ * Formats this {@see DateTimeInterface} instance based on a specific pattern, with optional customization for timezone and locale.
+ *
+ * This method allows for detailed customization of the output string by specifying a format pattern. If no pattern is provided,
+ * a default, implementation-specific pattern will be used. Additionally, the method supports specifying a timezone and locale
+ * for further customization of the formatted output. If these are not provided, system defaults will be used.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $formatted = $temporal->format('yyyy-MM-dd HH:mm:ss', $timezone, $locale);
+ * ```
+ *
+ * @param null|FormatPattern|string $pattern Optional custom format pattern for the date and time. If null, uses a default pattern.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the current timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The formatted date and time string, according to the specified pattern, timezone, and locale.
+ *
+ * @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
+ * @see Locale::default()
+ *
+ * @psalm-mutation-free
+ */
+ public function format(null|FormatPattern|string $pattern = null, null|Timezone $timezone = null, null|Locale $locale = null): string;
+
+ /**
+ * Provides a string representation of this {@see TemporalInterface} instance, formatted according to specified styles for date and time,
+ * and optionally adjusted for a specific timezone and locale.
+ *
+ * This method offers a higher-level abstraction for formatting, allowing users to specify styles for date and time separately
+ * rather than a custom pattern. If no styles are provided, default styles will be used.
+ *
+ * Additionally, the timezone and locale can be specified for locale-sensitive formatting.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $string_representation = $temporal->toString(FormatDateStyle::Long, FormatTimeStyle::Short, $timezone, $locale);
+ * ```
+ *
+ * @param null|DateStyle $date_style Optional style for the date portion of the output. If null, a default style is used.
+ * @param null|TimeStyle $time_style Optional style for the time portion of the output. If null, a default style is used.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the current timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The string representation of the date and time, formatted according to the specified styles, timezone, and locale.
+ *
+ * @see DateStyle::default()
+ * @see TimeStyle::default()
+ * @see Locale::default()
+ *
+ * @psalm-mutation-free
+ */
+ public function toString(null|DateStyle $date_style = null, null|TimeStyle $time_style = null, null|Timezone $timezone = null, null|Locale $locale = null): string;
+
+ /**
+ * Formats this {@see DateTimeInterface} instance to a string based on the RFC 3339 format, with additional
+ * options for second fractions and timezone representation.
+ *
+ * The RFC 3339 format is widely adopted in web and network protocols for its unambiguous representation of date, time,
+ * and timezone information. This method not only ensures universal readability but also the precise specification
+ * of time across various systems, being compliant with both RFC 3339 and ISO 8601 standards.
+ *
+ * Example usage:
+ *
+ * ```php
+ * // Default formatting
+ * $rfc_formatted_string = $datetime->toRfc3339();
+ * // Customized formatting with milliseconds and 'Z' for UTC
+ * $rfc_formatted_string_with_milliseconds_and_z = $datetime->toRfc3339(SecondsStyle::Milliseconds, true);
+ * ```
+ *
+ * @param null|SecondsStyle $seconds_style Optional parameter to specify the seconds formatting style. Automatically
+ * selected based on precision if null.
+ * @param bool $use_z Determines the representation of UTC timezone. True to use 'Z', false to use the standard offset format.
+ *
+ * @return string The formatted string of the {@see DateTimeInterface} instance, adhering to the RFC 3339 and compatible with ISO 8601 formats.
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc3339
+ *
+ * @psalm-mutation-free
+ */
+ public function toRfc3339(?SecondsStyle $seconds_style = null, bool $use_z = false): string;
+
+ /**
+ * Magic method that provides a default string representation of the date and time.
+ *
+ * This method is a shortcut for calling `toString()` with all null arguments, returning a string formatted
+ * with default styles, timezone, and locale. It is automatically called when the object is used in a string context.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $default_string_representation = (string) $temporal; // Uses __toString() for formatting
+ * ```
+ *
+ * @return string The default string representation of the date and time.
+ *
+ * @see TemporalInterface::toString()
+ *
+ * @psalm-mutation-free
+ */
+ public function __toString(): string;
+
+ /**
+ * Converts the {@see DateTimeInterface} instance to the specified timezone.
+ *
+ * @param Timezone $timezone The timezone to convert to.
+ *
+ * @psalm-mutation-free
+ */
+ public function convertToTimezone(Timezone $timezone): static;
+}
diff --git a/src/Psl/DateTime/Duration.php b/src/Psl/DateTime/Duration.php
new file mode 100644
index 00000000..e5dd7270
--- /dev/null
+++ b/src/Psl/DateTime/Duration.php
@@ -0,0 +1,729 @@
+
+ * @implements Comparison\Equable
+ *
+ * @immutable
+ */
+final readonly class Duration implements Comparison\Comparable, Comparison\Equable, JsonSerializable, Stringable
+{
+ /**
+ * Initializes a new instance of Duration with specified hours, minutes, seconds, and
+ * nanoseconds.
+ *
+ * @param int $hours
+ * @param int<-59, 59> $minutes
+ * @param int<-59, 59> $seconds
+ * @param int<-999999999, 999999999> $nanoseconds
+ *
+ * @pure
+ */
+ private function __construct(
+ private int $hours,
+ private int $minutes,
+ private int $seconds,
+ private int $nanoseconds
+ ) {
+ }
+
+ /**
+ * Returns an instance representing the specified number of hours (and
+ * optionally minutes, seconds, nanoseconds). Due to normalization, the
+ * actual values in the returned instance may differ from the provided ones.
+ *
+ * @pure
+ */
+ public static function fromParts(int $hours, int $minutes = 0, int $seconds = 0, int $nanoseconds = 0): self
+ {
+ // This is where the normalization happens.
+ $s = (SECONDS_PER_HOUR * $hours) + (SECONDS_PER_MINUTE * $minutes) + ($seconds + (int)($nanoseconds / NANOSECONDS_PER_SECOND));
+ $ns = $nanoseconds % NANOSECONDS_PER_SECOND;
+ if ($s < 0 && $ns > 0) {
+ ++$s;
+ $ns -= NANOSECONDS_PER_SECOND;
+ } elseif ($s > 0 && $ns < 0) {
+ --$s;
+ $ns += NANOSECONDS_PER_SECOND;
+ }
+
+ $m = (int)($s / 60);
+ $s %= 60;
+ $h = (int)($m / 60);
+ $m %= 60;
+ return new self($h, $m, $s, $ns);
+ }
+
+ /**
+ * Returns an instance representing the specified number of weeks, in hours.
+ *
+ * For example, `Duration::weeks(1)` is equivalent to `Duration::hours(168)`.
+ *
+ * @pure
+ */
+ public static function weeks(int $weeks): self
+ {
+ return self::fromParts($weeks * HOURS_PER_WEEK);
+ }
+
+ /**
+ * Returns an instance representing the specified number of days, in hours.
+ *
+ * For example, `Duration::days(2)` is equivalent to `Duration::hours(48)`.
+ *
+ * @pure
+ */
+ public static function days(int $days): self
+ {
+ return self::fromParts($days * HOURS_PER_DAY);
+ }
+
+ /**
+ * Returns an instance representing the specified number of hours.
+ *
+ * @pure
+ */
+ public static function hours(int $hours): self
+ {
+ return self::fromParts($hours);
+ }
+
+ /**
+ * Returns an instance representing the specified number of minutes. Due to
+ * normalization, the actual value in the returned instance may differ from
+ * the provided one, and the resulting instance may contain larger units.
+ *
+ * For example, `Duration::minutes(63)` normalizes to "1 hour(s), 3 minute(s)".
+ *
+ * @pure
+ */
+ public static function minutes(int $minutes): self
+ {
+ return self::fromParts(0, $minutes);
+ }
+
+ /**
+ * Returns an instance representing the specified number of seconds. Due to
+ * normalization, the actual value in the returned instance may differ from
+ * the provided one, and the resulting instance may contain larger units.
+ *
+ * For example, `Duration::seconds(63)` normalizes to "1 minute(s), 3 second(s)".
+ *
+ * @pure
+ */
+ public static function seconds(int $seconds): self
+ {
+ return self::fromParts(0, 0, $seconds);
+ }
+
+ /**
+ * Returns an instance representing the specified number of milliseconds (ms).
+ * The value is converted and stored as nanoseconds, since that is the only
+ * unit smaller than a second that we support. Due to normalization, the
+ * resulting instance may contain larger units.
+ *
+ * For example, `Duration::milliseconds(8042)` normalizes to "8 second(s), 42000000 nanosecond(s)".
+ *
+ * @pure
+ */
+ public static function milliseconds(int $milliseconds): self
+ {
+ return self::fromParts(0, 0, 0, NANOSECONDS_PER_MILLISECOND * $milliseconds);
+ }
+
+ /**
+ * Returns an instance representing the specified number of microseconds (us).
+ * The value is converted and stored as nanoseconds, since that is the only
+ * unit smaller than a second that we support. Due to normalization, the
+ * resulting instance may contain larger units.
+ *
+ * For example, `Duration::microseconds(8000042)` normalizes to "8 second(s), 42000 nanosecond(s)".
+ *
+ * @pure
+ */
+ public static function microseconds(int $microseconds): self
+ {
+ return self::fromParts(0, 0, 0, NANOSECONDS_PER_MICROSECOND * $microseconds);
+ }
+
+ /**
+ * Returns an instance representing the specified number of nanoseconds (ns).
+ * Due to normalization, the resulting instance may contain larger units.
+ *
+ * For example, `Duration::nanoseconds(8000000042)` normalizes to "8 second(s), 42 nanosecond(s)".
+ *
+ * @pure
+ */
+ public static function nanoseconds(int $nanoseconds): self
+ {
+ return self::fromParts(0, 0, 0, $nanoseconds);
+ }
+
+ /**
+ * Returns an instance with all parts equal to 0.
+ *
+ * @pure
+ */
+ public static function zero(): self
+ {
+ return new self(0, 0, 0, 0);
+ }
+
+ /**
+ * Compiles and returns the duration's components (hours, minutes, seconds, nanoseconds) in an
+ * array, in descending order of significance.
+ *
+ * @return array{int, int, int, int}
+ *
+ * @psalm-mutation-free
+ */
+ public function getParts(): array
+ {
+ return [$this->hours, $this->minutes, $this->seconds, $this->nanoseconds];
+ }
+
+ /**
+ * Returns the "hours" part of this time duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function getHours(): int
+ {
+ return $this->hours;
+ }
+
+ /**
+ * Returns the "minutes" part of this time duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function getMinutes(): int
+ {
+ return $this->minutes;
+ }
+
+ /**
+ * Returns the "seconds" part of this time duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function getSeconds(): int
+ {
+ return $this->seconds;
+ }
+
+ /**
+ * Returns the "nanoseconds" part of this time duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function getNanoseconds(): int
+ {
+ return $this->nanoseconds;
+ }
+
+ /**
+ * Computes, and returns the total duration of the instance in hours as a floating-point number,
+ * including any fractional parts.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTotalHours(): float
+ {
+ /** @psalm-suppress InvalidOperand */
+ return ($this->hours + ($this->minutes / MINUTES_PER_HOUR) +
+ ($this->seconds / SECONDS_PER_HOUR) +
+ ($this->nanoseconds / (SECONDS_PER_HOUR * NANOSECONDS_PER_SECOND)));
+ }
+
+ /**
+ * Computes, and returns the total duration of the instance in minutes as a floating-point number,
+ * including any fractional parts.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTotalMinutes(): float
+ {
+ /** @psalm-suppress InvalidOperand */
+ return (($this->hours * MINUTES_PER_HOUR) +
+ $this->minutes + ($this->seconds / SECONDS_PER_MINUTE) +
+ ($this->nanoseconds / (SECONDS_PER_MINUTE * NANOSECONDS_PER_SECOND)));
+ }
+
+ /**
+ * Computes, and returns the total duration of the instance in seconds as a floating-point number,
+ * including any fractional parts.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTotalSeconds(): float
+ {
+ /** @psalm-suppress InvalidOperand */
+ return ($this->seconds +
+ ($this->minutes * SECONDS_PER_MINUTE) +
+ ($this->hours * SECONDS_PER_HOUR) +
+ ($this->nanoseconds / NANOSECONDS_PER_SECOND));
+ }
+
+ /**
+ * Computes, and returns the total duration of the instance in milliseconds as a floating-point number,
+ * including any fractional parts.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTotalMilliseconds(): float
+ {
+ /** @psalm-suppress InvalidOperand */
+ return (($this->hours * SECONDS_PER_HOUR * MILLISECONDS_PER_SECOND) +
+ ($this->minutes * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND) +
+ ($this->seconds * MILLISECONDS_PER_SECOND) +
+ ($this->nanoseconds / NANOSECONDS_PER_MILLISECOND));
+ }
+
+ /**
+ * Computes, and returns the total duration of the instance in microseconds as a floating-point number,
+ * including any fractional parts.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTotalMicroseconds(): float
+ {
+ /** @psalm-suppress InvalidOperand */
+ return (($this->hours * SECONDS_PER_HOUR * MICROSECONDS_PER_SECOND) +
+ ($this->minutes * SECONDS_PER_MINUTE * MICROSECONDS_PER_SECOND) +
+ ($this->seconds * MICROSECONDS_PER_SECOND) +
+ ($this->nanoseconds / NANOSECONDS_PER_MICROSECOND));
+ }
+
+ /**
+ * Determines whether the instance represents a zero duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function isZero(): bool
+ {
+ return $this->hours === 0 &&
+ $this->minutes === 0 &&
+ $this->seconds === 0 &&
+ $this->nanoseconds === 0;
+ }
+
+ /**
+ * Checks if the duration is positive, implying that all non-zero components are positive.
+ *
+ * Due to normalization, it is guaranteed that a positive time duration will
+ * have all of its parts (hours, minutes, seconds, nanoseconds) positive or
+ * equal to 0.
+ *
+ * Note that this method returns false if all parts are equal to 0.
+ *
+ * @psalm-mutation-free
+ */
+ public function isPositive(): bool
+ {
+ return $this->hours > 0 ||
+ $this->minutes > 0 ||
+ $this->seconds > 0 ||
+ $this->nanoseconds > 0;
+ }
+
+ /**
+ * Checks if the duration is negative, implying that all non-zero components are negative.
+ *
+ * Due to normalization, it is guaranteed that a negative time duration will
+ * have all of its parts (hours, minutes, seconds, nanoseconds) negative or
+ * equal to 0.
+ *
+ * Note that this method returns false if all parts are equal to 0.
+ *
+ * @psalm-mutation-free
+ */
+ public function isNegative(): bool
+ {
+ return $this->hours < 0 ||
+ $this->minutes < 0 ||
+ $this->seconds < 0 ||
+ $this->nanoseconds < 0;
+ }
+
+ /**
+ * Returns a new instance with the "hours" part changed to the specified
+ * value.
+ *
+ * Note that due to normalization, the actual value in the returned
+ * instance may differ, and this may affect other parts of the returned
+ * instance too.
+ *
+ * For example, `Duration::hours(2, 30)->withHours(-1)` is equivalent to
+ * `Duration::hours(-1, 30)` which normalizes to "-30 minute(s)".
+ *
+ * @psalm-mutation-free
+ */
+ public function withHours(int $hours): self
+ {
+ return self::fromParts(
+ $hours,
+ $this->minutes,
+ $this->seconds,
+ $this->nanoseconds,
+ );
+ }
+
+ /**
+ * Returns a new instance with the "minutes" part changed to the specified
+ * value.
+ *
+ * Note that due to normalization, the actual value in the returned
+ * instance may differ, and this may affect other parts of the returned
+ * instance too.
+ *
+ * For example, `Duration::minutes(2, 30)->withMinutes(-1)` is equivalent to
+ * `Duration::minutes(-1, 30)` which normalizes to "-30 second(s)".
+ *
+ * @psalm-mutation-free
+ */
+ public function withMinutes(int $minutes): self
+ {
+ return self::fromParts(
+ $this->hours,
+ $minutes,
+ $this->seconds,
+ $this->nanoseconds,
+ );
+ }
+
+ /**
+ * Returns a new instance with the "seconds" part changed to the specified
+ * value.
+ *
+ * Note that due to normalization, the actual value in the returned
+ * instance may differ, and this may affect other parts of the returned
+ * instance too.
+ *
+ * For example, `Duration::minutes(2, 30)->withSeconds(-30)` is equivalent
+ * to `Duration::minutes(2, -30)` which normalizes to "1 minute(s), 30 second(s)".
+ *
+ * @psalm-mutation-free
+ */
+ public function withSeconds(int $seconds): self
+ {
+ return self::fromParts(
+ $this->hours,
+ $this->minutes,
+ $seconds,
+ $this->nanoseconds,
+ );
+ }
+
+ /**
+ * Returns a new instance with the "nanoseconds" part changed to the specified
+ * value.
+ *
+ * Note that due to normalization, the actual value in the returned
+ * instance may differ, and this may affect other parts of the returned
+ * instance too.
+ *
+ * For example, `Duration::seconds(2)->withNanoseconds(-1)` is equivalent
+ * to `Duration::seconds(2, -1)` which normalizes to "1 second(s), 999999999 nanosecond(s)".
+ *
+ * @psalm-mutation-free
+ */
+ public function withNanoseconds(int $nanoseconds): self
+ {
+ return self::fromParts(
+ $this->hours,
+ $this->minutes,
+ $this->seconds,
+ $nanoseconds,
+ );
+ }
+
+ /**
+ * Implements a comparison between this duration and another, based on their duration.
+ *
+ * @param Duration $other
+ *
+ * @psalm-mutation-free
+ */
+ public function compare(mixed $other): Comparison\Order
+ {
+ if ($this->hours !== $other->hours) {
+ return Comparison\Order::from($this->hours <=> $other->hours);
+ }
+
+ if ($this->minutes !== $other->minutes) {
+ return Comparison\Order::from($this->minutes <=> $other->minutes);
+ }
+
+ if ($this->seconds !== $other->seconds) {
+ return Comparison\Order::from($this->seconds <=> $other->seconds);
+ }
+
+ return Comparison\Order::from($this->nanoseconds <=> $other->nanoseconds);
+ }
+
+ /**
+ * Evaluates whether this duration is equivalent to another, considering all time components.
+ *
+ * @param Duration $other
+ *
+ * @psalm-mutation-free
+ */
+ public function equals(mixed $other): bool
+ {
+ return $this->compare($other) === Comparison\Order::Equal;
+ }
+
+ /**
+ * Determines if this duration is shorter than another.
+ *
+ * @psalm-mutation-free
+ */
+ public function shorter(self $other): bool
+ {
+ return $this->compare($other) === Comparison\Order::Less;
+ }
+
+ /**
+ * Determines if this duration is shorter than, or equivalent to another.
+ *
+ * @psalm-mutation-free
+ */
+ public function shorterOrEqual(self $other): bool
+ {
+ return $this->compare($other) !== Comparison\Order::Greater;
+ }
+
+ /**
+ * Determines if this duration is longer than another.
+ *
+ * @psalm-mutation-free
+ */
+ public function longer(self $other): bool
+ {
+ return $this->compare($other) === Comparison\Order::Greater;
+ }
+
+ /**
+ * Determines if this duration is longer than, or equivalent to another.
+ *
+ * @psalm-mutation-free
+ */
+ public function longerOrEqual(self $other): bool
+ {
+ return $this->compare($other) !== Comparison\Order::Less;
+ }
+
+ /**
+ * Returns true if this instance represents a time duration longer than $a but
+ * shorter than $b, or vice-versa (shorter than $a but longer than $b), or if
+ * this instance is equal to $a and/or $b. Returns false if this instance is
+ * shorter/longer than both.
+ *
+ * @psalm-mutation-free
+ */
+ public function betweenInclusive(self $a, self $b): bool
+ {
+ $ca = $this->compare($a);
+ $cb = $this->compare($b);
+
+ return $ca === Comparison\Order::Equal || $ca !== $cb;
+ }
+
+ /**
+ * Returns true if this instance represents a time duration longer than $a but
+ * shorter than $b, or vice-versa (shorter than $a but longer than $b).
+ * Returns false if this instance is equal to $a and/or $b, or shorter/longer
+ * than both.
+ *
+ * @psalm-mutation-free
+ */
+ public function betweenExclusive(self $a, self $b): bool
+ {
+ $ca = $this->compare($a);
+ $cb = $this->compare($b);
+
+ return $ca !== Comparison\Order::Equal && $cb !== Comparison\Order::Equal && $ca !== $cb;
+ }
+
+ /**
+ * Returns a new instance, converting a positive/negative duration to the
+ * opposite (negative/positive) duration of equal length. The resulting
+ * instance has all parts equivalent to the current instance's parts
+ * multiplied by -1.
+ *
+ * @psalm-mutation-free
+ */
+ public function invert(): self
+ {
+ if ($this->isZero()) {
+ return $this;
+ }
+
+ return new self(
+ -$this->hours,
+ -$this->minutes,
+ -$this->seconds,
+ -$this->nanoseconds,
+ );
+ }
+
+ /**
+ * Returns a new instance representing the sum of this instance and the
+ * provided `$other` instance. Note that time duration can be negative, so
+ * the resulting instance is not guaranteed to be shorter/longer than either
+ * of the inputs.
+ *
+ * This operation is commutative: `$a->plus($b) === $b->plus($a)`
+ *
+ * @psalm-mutation-free
+ */
+ public function plus(self $other): self
+ {
+ if ($other->isZero()) {
+ return $this;
+ }
+
+ if ($this->isZero()) {
+ return $other;
+ }
+
+ return self::fromParts(
+ $this->hours + $other->hours,
+ $this->minutes + $other->minutes,
+ $this->seconds + $other->seconds,
+ $this->nanoseconds + $other->nanoseconds,
+ );
+ }
+
+ /**
+ * Returns a new instance representing the difference between this instance
+ * and the provided `$other` instance (i.e. `$other` subtracted from `$this`).
+ * Note that time duration can be negative, so the resulting instance is not
+ * guaranteed to be shorter/longer than either of the inputs.
+ *
+ * This operation is not commutative: `$a->minus($b) !== $b->minus($a)`
+ * But: `$a->minus($b) === $b->minus($a)->invert()`
+ *
+ * @psalm-mutation-free
+ */
+ public function minus(self $other): self
+ {
+ if ($other->isZero()) {
+ return $this;
+ }
+
+ if ($this->isZero()) {
+ return $other->invert();
+ }
+
+ return self::fromParts(
+ $this->hours - $other->hours,
+ $this->minutes - $other->minutes,
+ $this->seconds - $other->seconds,
+ $this->nanoseconds - $other->nanoseconds,
+ );
+ }
+
+ /**
+ * Returns the time duration as string, useful e.g. for debugging. This is not
+ * meant to be a comprehensive way to format time durations for user-facing
+ * output.
+ *
+ * @param int<0, max> $max_decimals
+ *
+ * @psalm-mutation-free
+ *
+ * @psalm-suppress MissingThrowsDocblock
+ */
+ public function toString(int $max_decimals = 3): string
+ {
+ $decimal_part = '';
+ if ($max_decimals > 0) {
+ $decimal_part = (string)Math\abs($this->nanoseconds);
+ $decimal_part = Str\pad_left($decimal_part, 9, '0');
+ $decimal_part = Str\slice($decimal_part, 0, $max_decimals);
+ $decimal_part = Str\trim_right($decimal_part, '0');
+ }
+
+ if ($decimal_part !== '') {
+ $decimal_part = '.' . $decimal_part;
+ }
+
+ $sec_sign = $this->seconds < 0 || $this->nanoseconds < 0 ? '-' : '';
+ $sec = Math\abs($this->seconds);
+
+ /** @var list $values */
+ $values = [
+ [((string) $this->hours), 'hour(s)'],
+ [((string) $this->minutes), 'minute(s)'],
+ [$sec_sign . ((string) $sec) . $decimal_part, 'second(s)'],
+ ];
+
+ // $end is the sizeof($values), use static value for better performance.
+ $end = 3;
+ while ($end > 0 && $values[$end - 1][0] === '0') {
+ --$end;
+ }
+
+ $start = 0;
+ while ($start < $end && $values[$start][0] === '0') {
+ ++$start;
+ }
+
+ $output = [];
+ for ($i = $start; $i < $end; ++$i) {
+ $output[] = $values[$i][0] . ' ' . $values[$i][1];
+ }
+
+ return ([] === $output) ? '0 second(s)' : Str\join($output, ', ');
+ }
+
+ /**
+ * Returns a string representation of the time duration.
+ *
+ * @psalm-mutation-free
+ */
+ public function __toString(): string
+ {
+ return $this->toString();
+ }
+
+ /**
+ * Returns data which can be serialized by json_encode().
+ *
+ * @return array{hours: int, minutes: int, seconds: int, nanoseconds: int}
+ *
+ * @psalm-mutation-free
+ */
+ public function jsonSerialize(): array
+ {
+ return [
+ 'hours' => $this->hours,
+ 'minutes' => $this->minutes,
+ 'seconds' => $this->seconds,
+ 'nanoseconds' => $this->nanoseconds,
+ ];
+ }
+}
diff --git a/src/Psl/DateTime/Era.php b/src/Psl/DateTime/Era.php
new file mode 100644
index 00000000..0637a66a
--- /dev/null
+++ b/src/Psl/DateTime/Era.php
@@ -0,0 +1,44 @@
+ 0 ? self::AnnoDomini : self::BeforeChrist;
+ }
+
+ /**
+ * Toggles between AnnoDomini (AD) and BeforeChrist (BC).
+ *
+ * @return Era Returns BeforeChrist if the current instance is AnnoDomini, and vice versa.
+ *
+ * @psalm-mutation-free
+ */
+ public function toggle(): Era
+ {
+ return $this === self::AnnoDomini ? self::BeforeChrist : self::AnnoDomini;
+ }
+}
diff --git a/src/Psl/DateTime/Exception/ExceptionInterface.php b/src/Psl/DateTime/Exception/ExceptionInterface.php
new file mode 100644
index 00000000..ad1b04c6
--- /dev/null
+++ b/src/Psl/DateTime/Exception/ExceptionInterface.php
@@ -0,0 +1,11 @@
+getDaysForYear($year)
+ ));
+ }
+
+ /**
+ * An unexpected hours value.
+ *
+ * @param int $value The hours value that was provided.
+ *
+ * @return self Instance encapsulating the exception context.
+ *
+ * @psalm-mutation-free
+ *
+ * @internal
+ */
+ public static function forHours(int $value): self
+ {
+ return new self(Str\format('The hour \'%d\' exceeds the expected range of \'0\' to \'23\'.', $value));
+ }
+
+ /**
+ * An unexpected minutes value.
+ *
+ * @param int $value The minutes value that was provided.
+ *
+ * @return self Instance encapsulating the exception context.
+ *
+ * @psalm-mutation-free
+ *
+ * @internal
+ */
+ public static function forMinutes(int $value): self
+ {
+ return new self(Str\format('The minute \'%d\' steps beyond the bounds of \'0\' to \'59\'.', $value));
+ }
+
+ /**
+ * An unexpected seconds value.
+ *
+ * @param int $value The seconds value that was provided.
+ *
+ * @return self Instance encapsulating the exception context.
+ *
+ * @psalm-mutation-free
+ *
+ * @internal
+ */
+ public static function forSeconds(int $value): self
+ {
+ return new self(Str\format('The seconds \'%d\' stretch outside the acceptable range of \'0\' to \'59\'.', $value));
+ }
+
+ /**
+ * An unexpected nanoseconds value.
+ *
+ * @param int $value The nanoseconds value that was provided.
+ *
+ * @return self Instance encapsulating the exception context.
+ *
+ * @psalm-mutation-free
+ *
+ * @internal
+ */
+ public static function forNanoseconds(int $value): self
+ {
+ return new self(Str\format('The nanoseconds \'%d\' exceed the foreseen limit of \'0\' to \'999999999\'.', $value));
+ }
+}
diff --git a/src/Psl/DateTime/Exception/OverflowException.php b/src/Psl/DateTime/Exception/OverflowException.php
new file mode 100644
index 00000000..7b5094cd
--- /dev/null
+++ b/src/Psl/DateTime/Exception/OverflowException.php
@@ -0,0 +1,11 @@
+value;
+ }
+
+ $date_style ??= DateStyle::default();
+ $time_style ??= TimeStyle::default();
+ $locale ??= Locale::default();
+ $timezone ??= Timezone::default();
+
+ return new IntlDateFormatter(
+ $locale->value,
+ match ($date_style) {
+ DateStyle::None => IntlDateFormatter::NONE,
+ DateStyle::Short => IntlDateFormatter::SHORT,
+ DateStyle::Medium => IntlDateFormatter::MEDIUM,
+ DateStyle::Long => IntlDateFormatter::LONG,
+ DateStyle::Full => IntlDateFormatter::FULL,
+ },
+ match ($time_style) {
+ TimeStyle::None => IntlDateFormatter::NONE,
+ TimeStyle::Short => IntlDateFormatter::SHORT,
+ TimeStyle::Medium => IntlDateFormatter::MEDIUM,
+ TimeStyle::Long => IntlDateFormatter::LONG,
+ TimeStyle::Full => IntlDateFormatter::FULL,
+ },
+ namespace\to_intl_timezone($timezone),
+ IntlDateFormatter::GREGORIAN,
+ $pattern,
+ );
+}
diff --git a/src/Psl/DateTime/Internal/default_timezone.php b/src/Psl/DateTime/Internal/default_timezone.php
new file mode 100644
index 00000000..dab192f3
--- /dev/null
+++ b/src/Psl/DateTime/Internal/default_timezone.php
@@ -0,0 +1,29 @@
+getSeconds();
+ $nanoseconds = $timestamp->getNanoseconds();
+
+ // Intl formatter cannot handle nanoseconds and microseconds, do it manually instead.
+ $fraction = Byte\slice((string) $nanoseconds, 0, $seconds_style->value);
+ if ($fraction !== '') {
+ $fraction = '.' . $fraction;
+ }
+
+ $pattern = match ($use_z) {
+ true => 'yyyy-MM-dd\'T\'HH:mm:ss@ZZZZZ',
+ false => 'yyyy-MM-dd\'T\'HH:mm:ss@xxx',
+ };
+
+ $formatter = namespace\create_intl_date_formatter(pattern: $pattern, timezone: $timezone);
+ $rfc_string = $formatter->format($seconds);
+
+ return Byte\replace($rfc_string, '@', $fraction);
+}
diff --git a/src/Psl/DateTime/Internal/high_resolution_time.php b/src/Psl/DateTime/Internal/high_resolution_time.php
new file mode 100644
index 00000000..75db18ac
--- /dev/null
+++ b/src/Psl/DateTime/Internal/high_resolution_time.php
@@ -0,0 +1,63 @@
+= NANOSECONDS_PER_SECOND) {
+ ++$seconds;
+ $nanoseconds_adjusted -= NANOSECONDS_PER_SECOND;
+ } elseif ($nanoseconds_adjusted < 0) {
+ --$seconds;
+ $nanoseconds_adjusted += NANOSECONDS_PER_SECOND;
+ }
+
+ $seconds += $seconds_offset;
+ $nanoseconds = $nanoseconds_adjusted;
+
+ return [$seconds, $nanoseconds];
+}
diff --git a/src/Psl/DateTime/Internal/parse.php b/src/Psl/DateTime/Internal/parse.php
new file mode 100644
index 00000000..c728ae0d
--- /dev/null
+++ b/src/Psl/DateTime/Internal/parse.php
@@ -0,0 +1,52 @@
+parse($raw_string);
+ if ($timestamp === false) {
+ // Only show pattern in the exception if it was provided.
+ if (null !== $pattern) {
+ $formatter_pattern = $pattern instanceof FormatPattern ? $pattern->value : $pattern;
+
+ throw new ParserException(Str\format(
+ 'Unable to interpret \'%s\' as a valid date/time using pattern \'%s\'.',
+ $raw_string,
+ $formatter_pattern,
+ ));
+ }
+
+ throw new ParserException(
+ "Unable to interpret '$raw_string' as a valid date/time.",
+ );
+ }
+
+ return (int) $timestamp;
+}
diff --git a/src/Psl/DateTime/Internal/system_time.php b/src/Psl/DateTime/Internal/system_time.php
new file mode 100644
index 00000000..e93ff8ad
--- /dev/null
+++ b/src/Psl/DateTime/Internal/system_time.php
@@ -0,0 +1,32 @@
+value;
+ if (Byte\starts_with($value, '+') || Byte\starts_with($value, '-')) {
+ $value = 'GMT' . $value;
+ }
+
+ $tz = IntlTimeZone::createTimeZone($value);
+
+ Psl\invariant(
+ $tz !== null,
+ 'Failed to create intl timezone from timezone "%s" ( "%s" / "%s" ).',
+ $timezone->name,
+ $timezone->value,
+ $value,
+ );
+
+ Psl\invariant(
+ $tz->getID() !== 'Etc/Unknown' || $tz->getRawOffset() !== 0,
+ 'Failed to create a valid intl timezone, unknown timezone "%s" ( "%s" / "%s" ) given.',
+ $timezone->name,
+ $timezone->value,
+ $value,
+ );
+
+ return $tz;
+}
diff --git a/src/Psl/DateTime/Meridiem.php b/src/Psl/DateTime/Meridiem.php
new file mode 100644
index 00000000..587f798e
--- /dev/null
+++ b/src/Psl/DateTime/Meridiem.php
@@ -0,0 +1,44 @@
+ $hour The hour in a 24-hour format.
+ *
+ * @return Meridiem Returns AnteMeridiem for hours less than 12, and PostMeridiem for hours 12 and above.
+ *
+ * @pure
+ */
+ public static function fromHour(int $hour): Meridiem
+ {
+ return $hour < 12 ? self::AnteMeridiem : self::PostMeridiem;
+ }
+
+ /**
+ * Toggles between AnteMeridiem (AM) and PostMeridiem (PM).
+ *
+ * @return Meridiem Returns PostMeridiem if the current instance is AnteMeridiem, and vice versa.
+ *
+ * @psalm-mutation-free
+ */
+ public function toggle(): Meridiem
+ {
+ return $this === self::AnteMeridiem ? self::PostMeridiem : self::AnteMeridiem;
+ }
+}
diff --git a/src/Psl/DateTime/Month.php b/src/Psl/DateTime/Month.php
new file mode 100644
index 00000000..942770f2
--- /dev/null
+++ b/src/Psl/DateTime/Month.php
@@ -0,0 +1,149 @@
+ self::December,
+ self::February => self::January,
+ self::March => self::February,
+ self::April => self::March,
+ self::May => self::April,
+ self::June => self::May,
+ self::July => self::June,
+ self::August => self::July,
+ self::September => self::August,
+ self::October => self::September,
+ self::November => self::October,
+ self::December => self::November,
+ };
+ }
+
+ /**
+ * Returns the next month.
+ *
+ * This method calculates and returns the month succeeding the current instance of the Month enum.
+ *
+ * If the current instance is December, it wraps around and returns January.
+ *
+ * @return Month The next month.
+ *
+ * @psalm-mutation-free
+ */
+ public function getNext(): Month
+ {
+ return match ($this) {
+ self::January => self::February,
+ self::February => self::March,
+ self::March => self::April,
+ self::April => self::May,
+ self::May => self::June,
+ self::June => self::July,
+ self::July => self::August,
+ self::August => self::September,
+ self::September => self::October,
+ self::October => self::November,
+ self::November => self::December,
+ self::December => self::January,
+ };
+ }
+
+ /**
+ * Returns the number of days in the month for a given year.
+ *
+ * This method determines the number of days in the current month instance, considering whether the
+ * provided year is a leap year or not. It uses separate methods for leap years and non-leap years
+ * to get the appropriate day count.
+ *
+ * @param int $year The year for which the day count is needed.
+ *
+ * @return int<28, 31> The number of days in the month for the specified year.
+ *
+ * @psalm-mutation-free
+ */
+ public function getDaysForYear(int $year): int
+ {
+ if (namespace\is_leap_year($year)) {
+ return $this->getLeapYearDays();
+ }
+
+ return $this->getNonLeapYearDays();
+ }
+
+ /**
+ * Returns the number of days in the month for a non-leap year.
+ *
+ * This method provides the standard day count for the current month instance in a non-leap year.
+ *
+ * February returns 28, while April, June, September, and November return 30, and the rest return 31.
+ *
+ * @return int<28, 31> The number of days in the month for a non-leap year.
+ *
+ * @psalm-mutation-free
+ */
+ public function getNonLeapYearDays(): int
+ {
+ return match ($this) {
+ self::January, self::March, self::May, self::July, self::August, self::October, self::December => 31,
+ self::February => 28,
+ self::April, self::June, self::September, self::November => 30,
+ };
+ }
+
+ /**
+ * Returns the number of days in the month for a leap year.
+ *
+ * This method provides the day count for the current month instance in a leap year.
+ *
+ * February returns 29, while April, June, September, and November return 30, and the rest return 31.
+ *
+ * @return int<29, 31> The number of days in the month for a leap year.
+ *
+ * @psalm-mutation-free
+ */
+ public function getLeapYearDays(): int
+ {
+ return match ($this) {
+ self::January, self::March, self::May, self::July, self::August, self::October, self::December => 31,
+ self::February => 29,
+ self::April, self::June, self::September, self::November => 30,
+ };
+ }
+}
diff --git a/src/Psl/DateTime/SecondsStyle.php b/src/Psl/DateTime/SecondsStyle.php
new file mode 100644
index 00000000..d7c31398
--- /dev/null
+++ b/src/Psl/DateTime/SecondsStyle.php
@@ -0,0 +1,46 @@
+getNanoseconds();
+
+ return match (true) {
+ $nanoseconds === 0 => static::Seconds,
+ $nanoseconds % 1000000 === 0 => static::Milliseconds,
+ $nanoseconds % 1000 === 0 => static::Microseconds,
+ default => static::Nanoseconds,
+ };
+ }
+}
diff --git a/src/Psl/DateTime/TemporalConvenienceMethodsTrait.php b/src/Psl/DateTime/TemporalConvenienceMethodsTrait.php
new file mode 100644
index 00000000..6b6a94f2
--- /dev/null
+++ b/src/Psl/DateTime/TemporalConvenienceMethodsTrait.php
@@ -0,0 +1,390 @@
+getTimestamp()->toParts();
+ $b = $other->getTimestamp()->toParts();
+
+ return Comparison\Order::from($a[0] !== $b[0] ? $a[0] <=> $b[0] : $a[1] <=> $b[1]);
+ }
+
+ /**
+ * Checks if this {@see TemporalInterface} object represents the same time as the given one.
+ *
+ * Note: this method is an alias for {@see TemporalInterface::atTheSameTime()}.
+ *
+ * @param TemporalInterface $other
+ *
+ * @psalm-mutation-free
+ *
+ * @psalm-suppress MoreSpecificImplementedParamType
+ */
+ public function equals(mixed $other): bool
+ {
+ return $this->atTheSameTime($other);
+ }
+
+ /**
+ * Checks if this temporal object represents the same time as the given one.
+ *
+ * Note: this method is an alias for {@see TemporalInterface::equals()}.
+ *
+ * @psalm-mutation-free
+ */
+ public function atTheSameTime(TemporalInterface $other): bool
+ {
+ return $this->compare($other) === Comparison\Order::Equal;
+ }
+
+ /**
+ * Checks if this temporal object is before the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function before(TemporalInterface $other): bool
+ {
+ return $this->compare($other) === Comparison\Order::Less;
+ }
+
+ /**
+ * Checks if this temporal object is before or at the same time as the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function beforeOrAtTheSameTime(TemporalInterface $other): bool
+ {
+ return $this->compare($other) !== Comparison\Order::Greater;
+ }
+
+ /**
+ * Checks if this temporal object is after the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function after(TemporalInterface $other): bool
+ {
+ return $this->compare($other) === Comparison\Order::Greater;
+ }
+
+ /**
+ * Checks if this temporal object is after or at the same time as the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function afterOrAtTheSameTime(TemporalInterface $other): bool
+ {
+ return $this->compare($other) !== Comparison\Order::Less;
+ }
+
+ /**
+ * Checks if this temporal object is between the given times (inclusive).
+ *
+ * @psalm-mutation-free
+ */
+ public function betweenTimeInclusive(TemporalInterface $a, TemporalInterface $b): bool
+ {
+ $ca = $this->compare($a);
+ $cb = $this->compare($b);
+
+ return $ca === Comparison\Order::Equal || $ca !== $cb;
+ }
+
+ /**
+ * Checks if this temporal object is between the given times (exclusive).
+ *
+ * @psalm-mutation-free
+ */
+ public function betweenTimeExclusive(TemporalInterface $a, TemporalInterface $b): bool
+ {
+ $ca = $this->compare($a);
+ $cb = $this->compare($b);
+
+ return $ca !== Comparison\Order::Equal && $cb !== Comparison\Order::Equal && $ca !== $cb;
+ }
+
+ /**
+ * Adds the specified hours to this temporal object, returning a new instance with the added hours.
+ *
+ * @throws Exception\UnderflowException If adding the hours results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the hours results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusHours(int $hours): static
+ {
+ return $this->plus(Duration::hours($hours));
+ }
+
+ /**
+ * Adds the specified minutes to this temporal object, returning a new instance with the added minutes.
+ *
+ * @throws Exception\UnderflowException If adding the minutes results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the minutes results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusMinutes(int $minutes): static
+ {
+ return $this->plus(Duration::minutes($minutes));
+ }
+
+ /**
+ * Adds the specified seconds to this temporal object, returning a new instance with the added seconds.
+ *
+ * @throws Exception\UnderflowException If adding the seconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the seconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusSeconds(int $seconds): static
+ {
+ return $this->plus(Duration::seconds($seconds));
+ }
+
+ /**
+ * Adds the specified nanoseconds to this temporal object, returning a new instance with the added nanoseconds.
+ *
+ * @throws Exception\UnderflowException If adding the nanoseconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the nanoseconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusNanoseconds(int $nanoseconds): static
+ {
+ return $this->plus(Duration::nanoseconds($nanoseconds));
+ }
+
+ /**
+ * Subtracts the specified hours from this temporal object, returning a new instance with the subtracted hours.
+ *
+ * @throws Exception\UnderflowException If subtracting the hours results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the hours results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusHours(int $hours): static
+ {
+ return $this->minus(Duration::hours($hours));
+ }
+
+ /**
+ * Subtracts the specified minutes from this temporal object, returning a new instance with the subtracted minutes.
+ *
+ * @throws Exception\UnderflowException If subtracting the minutes results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the minutes results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusMinutes(int $minutes): static
+ {
+ return $this->minus(Duration::minutes($minutes));
+ }
+
+ /**
+ * Subtracts the specified seconds from this temporal object, returning a new instance with the subtracted seconds.
+ *
+ * @throws Exception\UnderflowException If subtracting the seconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the seconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusSeconds(int $seconds): static
+ {
+ return $this->minus(Duration::seconds($seconds));
+ }
+
+ /**
+ * Subtracts the specified nanoseconds from this temporal object, returning a new instance with the subtracted nanoseconds.
+ *
+ * @throws Exception\UnderflowException If subtracting the nanoseconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the nanoseconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusNanoseconds(int $nanoseconds): static
+ {
+ return $this->minus(Duration::nanoseconds($nanoseconds));
+ }
+
+ /**
+ * Calculates the duration between this temporal object and the given one.
+ *
+ * @param TemporalInterface $other The temporal object to calculate the duration to.
+ *
+ * @return Duration The duration between the two temporal objects.
+ *
+ * @psalm-mutation-free
+ */
+ public function since(TemporalInterface $other): Duration
+ {
+ $a = $this->getTimestamp()->toParts();
+ $b = $other->getTimestamp()->toParts();
+
+ return Duration::fromParts(0, 0, $a[0] - $b[0], $a[1] - $b[1]);
+ }
+
+ /**
+ * Converts the current temporal object to a new {@see DateTimeInterface} instance in a different timezone.
+ *
+ * @param Timezone $timezone The target timezone for the conversion.
+ *
+ * @psalm-mutation-free
+ */
+ public function convertToTimezone(Timezone $timezone): DateTimeInterface
+ {
+ return DateTime::fromTimestamp($this->getTimestamp(), $timezone);
+ }
+
+ /**
+ * Formats this {@see TemporalInterface} instance based on a specific pattern, with optional customization for timezone and locale.
+ *
+ * This method allows for detailed customization of the output string by specifying a format pattern. If no pattern is provided,
+ * a default, implementation-specific pattern will be used. Additionally, the method supports specifying a timezone and locale
+ * for further customization of the formatted output. If these are not provided, system defaults will be used.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $formatted = $temporal->format('yyyy-MM-dd HH:mm:ss', $timezone, $locale);
+ * ```
+ *
+ * @param null|FormatPattern|string $pattern Optional custom format pattern for the date and time. If null, uses a default pattern.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The formatted date and time string, according to the specified pattern, timezone, and locale.
+ *
+ * @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
+ *
+ * @psalm-mutation-free
+ */
+ public function format(null|FormatPattern|string $pattern = null, null|Timezone $timezone = null, null|Locale $locale = null): string
+ {
+ $timestamp = $this->getTimestamp();
+
+ /**
+ * @psalm-suppress InvalidOperand
+ * @psalm-suppress ImpureMethodCall
+ */
+ return Internal\create_intl_date_formatter(null, null, $pattern, $timezone, $locale)
+ ->format($timestamp->getSeconds() + ($timestamp->getNanoseconds() / NANOSECONDS_PER_SECOND));
+ }
+
+ /**
+ * Formats this {@see TemporalInterface} instance to a string based on the RFC 3339 format, with additional
+ * options for second fractions and timezone representation.
+ *
+ * The RFC 3339 format is widely adopted in web and network protocols for its unambiguous representation of date, time,
+ * and timezone information. This method not only ensures universal readability but also the precise specification
+ * of time across various systems, being compliant with both RFC 3339 and ISO 8601 standards.
+ *
+ * Example usage:
+ *
+ * ```php
+ * // Default formatting
+ * $rfc_formatted_string = $datetime->toRfc3339();
+ * // Customized formatting with milliseconds and 'Z' for UTC
+ * $rfc_formatted_string_with_milliseconds_and_z = $datetime->toRfc3339(SecondsStyle::Milliseconds, true);
+ * ```
+ *
+ * @param null|SecondsStyle $seconds_style Optional parameter to specify the seconds formatting style. Automatically
+ * selected based on precision if null.
+ * @param bool $use_z Determines the representation of UTC timezone. True to use 'Z', false to use the standard offset format.
+ *
+ * @return string The formatted string of the {@see TemporalInterface} instance, adhering to the RFC 3339 and compatible with ISO 8601 formats.
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc3339
+ *
+ * @psalm-mutation-free
+ */
+ public function toRfc3339(?SecondsStyle $seconds_style = null, bool $use_z = false): string
+ {
+ return Internal\format_rfc3339($this->getTimestamp(), $seconds_style, $use_z);
+ }
+
+ /**
+ * Provides a string representation of this {@see TemporalInterface} instance, formatted according to specified styles for date and time,
+ * and optionally adjusted for a specific timezone and locale.
+ *
+ * This method offers a higher-level abstraction for formatting, allowing users to specify styles for date and time separately
+ * rather than a custom pattern. If no styles are provided, default styles will be used.
+ *
+ * Additionally, the timezone and locale can be specified for locale-sensitive formatting.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $string_representation = $temporal->toString(FormatDateStyle::Long, FormatTimeStyle::Short, $timezone, $locale);
+ * ```
+ *
+ * @param null|DateStyle $date_style Optional style for the date portion of the output. If null, a default style is used.
+ * @param null|TimeStyle $time_style Optional style for the time portion of the output. If null, a default style is used.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The string representation of the date and time, formatted according to the specified styles, timezone, and locale.
+ *
+ * @see DateStyle::default()
+ * @see TimeStyle::default()
+ *
+ * @psalm-mutation-free
+ */
+ public function toString(null|DateStyle $date_style = null, null|TimeStyle $time_style = null, null|Timezone $timezone = null, null|Locale $locale = null): string
+ {
+ $timestamp = $this->getTimestamp();
+
+ /**
+ * @psalm-suppress InvalidOperand
+ * @psalm-suppress ImpureMethodCall
+ */
+ return Internal\create_intl_date_formatter($date_style, $time_style, null, $timezone, $locale)
+ ->format($timestamp->getSeconds() + ($timestamp->getNanoseconds() / NANOSECONDS_PER_SECOND));
+ }
+
+ /**
+ * Magic method that provides a default string representation of the date and time.
+ *
+ * This method is a shortcut for calling `toString()` with all null arguments, returning a string formatted
+ * with default styles, timezone, and locale. It is automatically called when the object is used in a string context.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $default_string_representation = (string) $temporal; // Uses __toString() for formatting
+ * ```
+ *
+ * @return string The default string representation of the date and time.
+ *
+ * @see TemporalInterface::toString()
+ *
+ * @psalm-mutation-free
+ */
+ public function __toString(): string
+ {
+ return $this->toString();
+ }
+}
diff --git a/src/Psl/DateTime/TemporalInterface.php b/src/Psl/DateTime/TemporalInterface.php
new file mode 100644
index 00000000..85d815e2
--- /dev/null
+++ b/src/Psl/DateTime/TemporalInterface.php
@@ -0,0 +1,324 @@
+
+ * @template-extends Equable
+ */
+interface TemporalInterface extends Comparable, Equable, JsonSerializable, Stringable
+{
+ /**
+ * Returns the timestamp representation of this temporal object.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTimestamp(): Timestamp;
+
+ /**
+ * Compare this {@see TemporalInterface} object to the given one.
+ *
+ * @param TemporalInterface $other
+ *
+ * @psalm-mutation-free
+ */
+ public function compare(mixed $other): Order;
+
+ /**
+ * Checks if this {@see TemporalInterface} object represents the same time as the given one.
+ *
+ * Note: this method is an alias for {@see TemporalInterface::atTheSameTime()}.
+ *
+ * @param TemporalInterface $other
+ *
+ * @psalm-mutation-free
+ */
+ public function equals(mixed $other): bool;
+
+ /**
+ * Checks if this temporal object represents the same time as the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function atTheSameTime(TemporalInterface $other): bool;
+
+ /**
+ * Checks if this temporal object is before the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function before(TemporalInterface $other): bool;
+
+ /**
+ * Checks if this temporal object is before or at the same time as the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function beforeOrAtTheSameTime(TemporalInterface $other): bool;
+
+ /**
+ * Checks if this temporal object is after the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function after(TemporalInterface $other): bool;
+
+ /**
+ * Checks if this temporal object is after or at the same time as the given one.
+ *
+ * @psalm-mutation-free
+ */
+ public function afterOrAtTheSameTime(TemporalInterface $other): bool;
+
+ /**
+ * Checks if this temporal object is between the given times (inclusive).
+ *
+ * @psalm-mutation-free
+ */
+ public function betweenTimeInclusive(TemporalInterface $a, TemporalInterface $b): bool;
+
+ /**
+ * Checks if this temporal object is between the given times (exclusive).
+ *
+ * @psalm-mutation-free
+ */
+ public function betweenTimeExclusive(TemporalInterface $a, TemporalInterface $b): bool;
+
+ /**
+ * Adds the specified duration to this temporal object, returning a new instance with the added duration.
+ *
+ * @throws Exception\UnderflowException If adding the duration results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the duration results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plus(Duration $duration): static;
+
+ /**
+ * Subtracts the specified duration from this temporal object, returning a new instance with the subtracted duration.
+ *
+ * @throws Exception\UnderflowException If subtracting the duration results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the duration results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minus(Duration $duration): static;
+
+ /**
+ * Adds the specified hours to this temporal object, returning a new instance with the added hours.
+ *
+ * @throws Exception\UnderflowException If adding the hours results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the hours results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusHours(int $hours): static;
+
+ /**
+ * Adds the specified minutes to this temporal object, returning a new instance with the added minutes.
+ *
+ * @throws Exception\UnderflowException If adding the minutes results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the minutes results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusMinutes(int $minutes): static;
+
+ /**
+ * Adds the specified seconds to this temporal object, returning a new instance with the added seconds.
+ *
+ * @throws Exception\UnderflowException If adding the seconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the seconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusSeconds(int $seconds): static;
+
+ /**
+ * Adds the specified nanoseconds to this temporal object, returning a new instance with the added nanoseconds.
+ *
+ * @throws Exception\UnderflowException If adding the nanoseconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the nanoseconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plusNanoseconds(int $nanoseconds): static;
+
+ /**
+ * Subtracts the specified hours from this temporal object, returning a new instance with the subtracted hours.
+ *
+ * @throws Exception\UnderflowException If subtracting the hours results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the hours results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusHours(int $hours): static;
+
+ /**
+ * Subtracts the specified minutes from this temporal object, returning a new instance with the subtracted minutes.
+ *
+ * @throws Exception\UnderflowException If subtracting the minutes results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the minutes results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusMinutes(int $minutes): static;
+
+ /**
+ * Subtracts the specified seconds from this temporal object, returning a new instance with the subtracted seconds.
+ *
+ * @throws Exception\UnderflowException If subtracting the seconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the seconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusSeconds(int $seconds): static;
+
+ /**
+ * Subtracts the specified nanoseconds from this temporal object, returning a new instance with the subtracted nanoseconds.
+ *
+ * @throws Exception\UnderflowException If subtracting the nanoseconds results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the nanoseconds results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minusNanoseconds(int $nanoseconds): static;
+
+ /**
+ * Calculates the duration between this temporal object and the given one.
+ *
+ * @param TemporalInterface $other The temporal object to calculate the duration to.
+ *
+ * @return Duration The duration between the two temporal objects.
+ *
+ * @psalm-mutation-free
+ */
+ public function since(TemporalInterface $other): Duration;
+
+ /**
+ * Formats this {@see TemporalInterface} instance based on a specific pattern, with optional customization for timezone and locale.
+ *
+ * This method allows for detailed customization of the output string by specifying a format pattern. If no pattern is provided,
+ * a default, implementation-specific pattern will be used. Additionally, the method supports specifying a timezone and locale
+ * for further customization of the formatted output. If these are not provided, system defaults will be used.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $formatted_string = $temporal->format('yyyy-MM-dd HH:mm:ss', $timezone, $locale);
+ * ```
+ *
+ * @param null|FormatPattern|string $pattern Optional custom format pattern for the date and time. If null, uses a default pattern.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The formatted {@see TemporalInterface} instance string, according to the specified pattern, timezone, and locale.
+ *
+ * @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
+ * @see Timezone::default()
+ * @see Locale::default()
+ *
+ * @psalm-mutation-free
+ */
+ public function format(null|FormatPattern|string $pattern = null, null|Timezone $timezone = null, null|Locale $locale = null): string;
+
+ /**
+ * Formats this {@see TemporalInterface} instance to a string based on the RFC 3339 format, with additional
+ * options for second fractions and timezone representation.
+ *
+ * The RFC 3339 format is widely adopted in web and network protocols for its unambiguous representation of date, time,
+ * and timezone information. This method not only ensures universal readability but also the precise specification
+ * of time across various systems, being compliant with both RFC 3339 and ISO 8601 standards.
+ *
+ * Example usage:
+ *
+ * ```php
+ * // Default formatting
+ * $rfc_formatted_string = $datetime->toRfc3339();
+ * // Customized formatting with milliseconds and 'Z' for UTC
+ * $rfc_formatted_string_with_milliseconds_and_z = $datetime->toRfc3339(SecondsStyle::Milliseconds, true);
+ * ```
+ *
+ * @param null|SecondsStyle $seconds_style Optional parameter to specify the seconds formatting style. Automatically
+ * selected based on precision if null.
+ * @param bool $use_z Determines the representation of UTC timezone. True to use 'Z', false to use the standard offset format.
+ *
+ * @return string The formatted string of the {@see TemporalInterface} instance, adhering to the RFC 3339 and compatible with ISO 8601 formats.
+ *
+ * @see https://datatracker.ietf.org/doc/html/rfc3339
+ *
+ * @psalm-mutation-free
+ */
+ public function toRfc3339(?SecondsStyle $seconds_style = null, bool $use_z = false): string;
+
+ /**
+ * Provides a string representation of this {@see TemporalInterface} instance, formatted according to specified styles for date and time,
+ * and optionally adjusted for a specific timezone and locale.
+ *
+ * This method offers a higher-level abstraction for formatting, allowing users to specify styles for date and time separately
+ * rather than a custom pattern. If no styles are provided, default styles will be used.
+ *
+ * Additionally, the timezone and locale can be specified for locale-sensitive formatting.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $string_representation = $temporal->toString(FormatDateStyle::Long, FormatTimeStyle::Short, $timezone, $locale);
+ * ```
+ *
+ * @param null|DateStyle $date_style Optional style for the date portion of the output. If null, a default style is used.
+ * @param null|TimeStyle $time_style Optional style for the time portion of the output. If null, a default style is used.
+ * @param null|Timezone $timezone Optional timezone for formatting. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for formatting. If null, uses the system's default locale.
+ *
+ * @return string The string representation of the date and time, formatted according to the specified styles, timezone, and locale.
+ *
+ * @see DateStyle::default()
+ * @see TimeStyle::default()
+ * @see Timezone::default()
+ * @see Locale::default()
+ *
+ * @psalm-mutation-free
+ */
+ public function toString(null|DateStyle $date_style = null, null|TimeStyle $time_style = null, null|Timezone $timezone = null, null|Locale $locale = null): string;
+
+ /**
+ * Magic method that provides a default string representation of the date and time.
+ *
+ * This method is a shortcut for calling `toString()` with all null arguments, returning a string formatted
+ * with default styles, timezone, and locale. It is automatically called when the object is used in a string context.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $default_string_representation = (string) $temporal; // Uses __toString() for formatting
+ * ```
+ *
+ * @return string The default string representation of the date and time.
+ *
+ * @see TemporalInterface::toString()
+ *
+ * @psalm-mutation-free
+ */
+ public function __toString(): string;
+
+ /**
+ * Converts the current temporal object to a new {@see DateTimeInterface} instance in a different timezone.
+ *
+ * @param Timezone $timezone The target timezone for the conversion.
+ *
+ * @psalm-mutation-free
+ */
+ public function convertToTimezone(Timezone $timezone): DateTimeInterface;
+}
diff --git a/src/Psl/DateTime/TimeStyle.php b/src/Psl/DateTime/TimeStyle.php
new file mode 100644
index 00000000..775e7fb2
--- /dev/null
+++ b/src/Psl/DateTime/TimeStyle.php
@@ -0,0 +1,45 @@
+ $nanoseconds
+ *
+ * @pure
+ */
+ private function __construct(
+ private int $seconds,
+ private int $nanoseconds,
+ ) {
+ }
+
+ /**
+ * Creates a timestamp from seconds and nanoseconds since the epoch.
+ *
+ * Normalizes so nanoseconds are within 0-999999999. For instance:
+ * - `fromRaw(42, -100)` becomes (41, 999999900).
+ * - `fromRaw(-42, -100)` becomes (-43, 999999900).
+ * - `fromRaw(42, 1000000100)` becomes (43, 100).
+ *
+ * @param int $seconds Seconds since the epoch.
+ * @param int $nanoseconds Additional nanoseconds to adjust by.
+ *
+ * @throws Exception\OverflowException
+ * @throws Exception\UnderflowException
+ *
+ * @pure
+ */
+ public static function fromParts(int $seconds, int $nanoseconds = 0): Timestamp
+ {
+ // Check for potential overflow or underflow before doing any operation
+ if ($seconds === Math\INT64_MAX && $nanoseconds >= NANOSECONDS_PER_SECOND) {
+ throw new Exception\OverflowException("Adding nanoseconds would cause an overflow.");
+ }
+
+ if ($seconds === Math\INT64_MIN && $nanoseconds <= -NANOSECONDS_PER_SECOND) {
+ throw new Exception\UnderflowException("Subtracting nanoseconds would cause an underflow.");
+ }
+
+ /** @psalm-suppress MissingThrowsDocblock */
+ $seconds_adjustment = Math\div($nanoseconds, NANOSECONDS_PER_SECOND);
+ $adjusted_seconds = $seconds + $seconds_adjustment;
+
+ $adjusted_nanoseconds = $nanoseconds % NANOSECONDS_PER_SECOND;
+ if ($adjusted_nanoseconds < 0) {
+ --$adjusted_seconds;
+ $adjusted_nanoseconds += NANOSECONDS_PER_SECOND;
+ }
+
+ return new self($adjusted_seconds, $adjusted_nanoseconds);
+ }
+
+ /**
+ * Create a high-precision instance representing the current time using the system clock.
+ *
+ * @psalm-mutation-free
+ */
+ public static function now(): self
+ {
+ [$seconds, $nanoseconds] = Internal\system_time();
+
+ /** @psalm-suppress MissingThrowsDocblock */
+ return self::fromParts($seconds, $nanoseconds);
+ }
+
+ /**
+ * Create a current time instance using a monotonic clock with high precision
+ * to the nanosecond for precise measurements.
+ *
+ * This method ensures that the time is always moving forward, unaffected by adjustments in the system clock,
+ * making it suitable for measuring durations or intervals accurately.
+ *
+ * @throws InvariantViolationException If the system does not provide a monotonic timer.
+ *
+ * @psalm-mutation-free
+ */
+ public static function monotonic(): self
+ {
+ [$seconds, $nanoseconds] = Internal\high_resolution_time();
+
+ /** @psalm-suppress MissingThrowsDocblock */
+ return self::fromParts($seconds, $nanoseconds);
+ }
+
+ /**
+ * Parses a date and time string into an instance of {@see Timestamp} using a specific format pattern, with optional customization for timezone and locale.
+ *
+ * This method is specifically designed for cases where a custom format pattern is used to parse the input string.
+ *
+ * It allows for precise control over the parsing process by specifying the exact format pattern that matches the input string.
+ *
+ * Additionally, the method supports specifying a timezone and locale for parsing, enabling accurate interpretation of locale-specific formats.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $raw_string = '2023-03-15 12:00:00';
+ * $parsed_timestamp = DateTime\Timestamp::parse($raw_string, 'yyyy-MM-dd HH:mm:ss', DateTime\Timezone::Utc, Locale\Locale::English);
+ * ```
+ *
+ * @param string $raw_string The date and time string to parse.
+ * @param null|FormatPattern|string $pattern The custom format pattern for parsing the date and time. If null, uses a default pattern.
+ * @param null|Timezone $timezone Optional timezone for parsing. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for parsing. If null, uses the system's default locale.
+ *
+ * @throws Exception\RuntimeException If the parsing process fails.
+ *
+ * @return static Returns an instance of {@see Timestamp} representing the parsed date and time.
+ *
+ * @see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax
+ * @see TemporalInterface::format()
+ *
+ * @psalm-mutation-free
+ */
+ public static function parse(string $raw_string, null|FormatPattern|string $pattern = null, null|Timezone $timezone = null, null|Locale $locale = null): static
+ {
+ /** @psalm-suppress MissingThrowsDocblock */
+ return self::fromParts(
+ Internal\parse(raw_string: $raw_string, pattern: $pattern, timezone: $timezone, locale: $locale)
+ );
+ }
+
+ /**
+ * Creates an instance of {@see Timestamp} from a date and time string, formatted according to specified styles for date and time,
+ * with optional customization for timezone and locale.
+ *
+ * This method provides a more abstracted approach to parsing, allowing users to specify styles rather than a custom pattern.
+ *
+ * This is particularly useful for parsing strings that follow common date and time formats.
+ *
+ * Additionally, the timezone and locale parameters enable accurate parsing of strings in locale-specific formats.
+ *
+ * Example usage:
+ *
+ * ```php
+ * $raw_string = "March 15, 2023, 12:00 PM";
+ *
+ * $timestamp = DateTime\Timestamp::fromString($raw_string, FormatDateStyle::Long, FormatTimeStyle::Short, DateTime\Timezone::Utc, Locale\Locale::English);
+ * ```
+ *
+ * @param string $raw_string The date and time string to parse.
+ * @param null|DateStyle $date_style The style for the date portion of the string. If null, a default style is used.
+ * @param null|TimeStyle $time_style The style for the time portion of the string. If null, a default style is used.
+ * @param null|Timezone $timezone Optional timezone for parsing. If null, uses the system's default timezone.
+ * @param null|Locale $locale Optional locale for parsing. If null, uses the system's default locale.
+ *
+ * @throws Exception\RuntimeException If the parsing process fails.
+ *
+ * @return static Returns an instance of {@see Timestamp} representing the parsed date and time.
+ *
+ * @see TemporalInterface::toString()
+ *
+ * @psalm-mutation-free
+ */
+ public static function fromString(string $raw_string, null|DateStyle $date_style = null, null|TimeStyle $time_style = null, null|Timezone $timezone = null, null|Locale $locale = null): static
+ {
+ /** @psalm-suppress MissingThrowsDocblock */
+ return self::fromParts(
+ Internal\parse(raw_string: $raw_string, date_style: $date_style, time_style: $time_style, timezone: $timezone, locale: $locale)
+ );
+ }
+
+ /**
+ * Returns this Timestamp instance itself, as it already represents a timestamp.
+ *
+ * @psalm-mutation-free
+ */
+ public function getTimestamp(): self
+ {
+ return $this;
+ }
+
+ /**
+ * Returns the {@see Timestamp} parts (seconds, nanoseconds).
+ *
+ * @return array{int, int<0, 999999999>}
+ *
+ * @psalm-mutation-free
+ */
+ public function toParts(): array
+ {
+ return [$this->seconds, $this->nanoseconds];
+ }
+
+ /**
+ * Returns the number of seconds since the Unix epoch represented by this timestamp.
+ *
+ * @return int Seconds since the epoch. Can be negative for times before the epoch.
+ *
+ * @psalm-mutation-free
+ */
+ public function getSeconds(): int
+ {
+ return $this->seconds;
+ }
+
+ /**
+ * Returns the nanoseconds part of this timestamp.
+ *
+ * @return int<0, 999999999> The nanoseconds part, ranging from 0 to 999999999.
+ *
+ * @psalm-mutation-free
+ */
+ public function getNanoseconds(): int
+ {
+ return $this->nanoseconds;
+ }
+
+ /**
+ * Adds the specified duration to this timestamp object, returning a new instance with the added duration.
+ *
+ * @throws Exception\UnderflowException If adding the duration results in an arithmetic underflow.
+ * @throws Exception\OverflowException If adding the duration results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function plus(Duration $duration): static
+ {
+ [$h, $m, $s, $ns] = $duration->getParts();
+ $totalSeconds = SECONDS_PER_MINUTE * $m + SECONDS_PER_HOUR * $h + $s;
+ $newSeconds = $this->seconds + $totalSeconds;
+ $newNanoseconds = $this->nanoseconds + $ns;
+
+ // No manual normalization required here due to fromRaw handling it
+ return self::fromParts($newSeconds, $newNanoseconds);
+ }
+
+ /**
+ * Subtracts the specified duration from this timestamp object, returning a new instance with the subtracted duration.
+ *
+ * @throws Exception\UnderflowException If subtracting the duration results in an arithmetic underflow.
+ * @throws Exception\OverflowException If subtracting the duration results in an arithmetic overflow.
+ *
+ * @psalm-mutation-free
+ */
+ public function minus(Duration $duration): static
+ {
+ [$h, $m, $s, $ns] = $duration->getParts();
+ $totalSeconds = SECONDS_PER_MINUTE * $m + SECONDS_PER_HOUR * $h + $s;
+ $newSeconds = $this->seconds - $totalSeconds;
+ $newNanoseconds = $this->nanoseconds - $ns;
+
+ // No manual normalization required here due to fromRaw handling it
+ return self::fromParts($newSeconds, $newNanoseconds);
+ }
+
+ public function jsonSerialize(): array
+ {
+ return [
+ 'seconds' => $this->seconds,
+ 'nanoseconds' => $this->nanoseconds,
+ ];
+ }
+}
diff --git a/src/Psl/DateTime/Timezone.php b/src/Psl/DateTime/Timezone.php
new file mode 100644
index 00000000..949b1c05
--- /dev/null
+++ b/src/Psl/DateTime/Timezone.php
@@ -0,0 +1,596 @@
+getTimestamp()->getSeconds() * MILLISECONDS_PER_SECOND;
+ /** @psalm-suppress ImpureMethodCall - mutation free */
+ $intl_timezone->getOffset($timestamp_millis, $local, $raw_offset, $dst_offset);
+
+ return Duration::milliseconds($raw_offset + $dst_offset);
+ }
+
+ /**
+ * Calculates the raw time zone offset for the current timezone, excluding any daylight saving time (DST) adjustments.
+ *
+ * This method retrieves the fixed offset from UTC for the timezone without considering any seasonal adjustments
+ * that might apply due to DST. It's particularly useful for understanding the base offset of a timezone.
+ *
+ * @psalm-mutation-free
+ */
+ public function getRawOffset(): Duration
+ {
+ /** @psalm-suppress ImpureMethodCall - mutation free */
+ return Duration::milliseconds(Internal\to_intl_timezone($this)->getRawOffset());
+ }
+
+ /**
+ * Calculates the daylight saving time (DST) offset for a given {@see TemporalInterface} instance at its specific time.
+ *
+ * This DST offset is the adjustment added to the raw timezone offset, if DST is in effect at the temporal instance's time.
+ *
+ * @param bool $local Indicates whether the temporal object's time should be treated as local time (`true`) or as UTC time (`false`).
+ *
+ * @return Duration The DST offset as a Duration instance. If DST is not in effect, the offset will be zero.
+ *
+ * @psalm-mutation-free
+ */
+ public function getDaylightSavingTimeOffset(TemporalInterface $temporal, bool $local = false): Duration
+ {
+ $intl_timezone = Internal\to_intl_timezone($this);
+ $timestamp_millis = $temporal->getTimestamp()->getSeconds() * MILLISECONDS_PER_SECOND;
+ /** @psalm-suppress ImpureMethodCall - mutation free */
+ $intl_timezone->getOffset($timestamp_millis, $local, $_, $dst_offset);
+
+ return Duration::milliseconds($dst_offset);
+ }
+
+ /**
+ * Determines whether the current timezone observes Daylight Saving Time (DST).
+ *
+ * This method checks if the timezone has any DST rules and if DST is applied at any point during the year.
+ *
+ * @return bool True if the timezone uses Daylight Saving Time at any point in the year, false otherwise.
+ *
+ * @psalm-mutation-free
+ */
+ public function usesDaylightSavingTime(): bool
+ {
+ /** @psalm-suppress ImpureMethodCall - mutation free */
+ return Internal\to_intl_timezone($this)->useDaylightTime();
+ }
+
+ /**
+ * Retrieves the amount of time added during Daylight Saving Time for the current timezone.
+ *
+ * This method returns the typical adjustment made to the local time when DST is in effect.
+ *
+ * If the timezone does not observe DST or if there is no current DST adjustment (e.g., outside of DST periods),
+ * the method will return a Duration of zero.
+ *
+ * @psalm-mutation-free
+ */
+ public function getDaylightSavingTimeSavings(): Duration
+ {
+ /** @psalm-suppress ImpureMethodCall - mutation free */
+ return Duration::milliseconds(Internal\to_intl_timezone($this)->getDSTSavings());
+ }
+
+ /**
+ * Determines whether the current timezone has the same rules as another specified timezone.
+ *
+ * @psalm-mutation-free
+ */
+ public function hasTheSameRulesAs(Timezone $other): bool
+ {
+ /** @psalm-suppress ImpureMethodCall - mutation free */
+ return Internal\to_intl_timezone($this)->hasSameRules(Internal\to_intl_timezone($other));
+ }
+}
diff --git a/src/Psl/DateTime/Weekday.php b/src/Psl/DateTime/Weekday.php
new file mode 100644
index 00000000..d0c6d5b4
--- /dev/null
+++ b/src/Psl/DateTime/Weekday.php
@@ -0,0 +1,68 @@
+ self::Sunday,
+ self::Tuesday => self::Monday,
+ self::Wednesday => self::Tuesday,
+ self::Thursday => self::Wednesday,
+ self::Friday => self::Thursday,
+ self::Saturday => self::Friday,
+ self::Sunday => self::Saturday,
+ };
+ }
+
+ /**
+ * Returns the next weekday.
+ *
+ * If the current instance is Sunday, it wraps around and returns Monday.
+ *
+ * @return Weekday The next weekday.
+ *
+ * @psalm-mutation-free
+ */
+ public function getNext(): Weekday
+ {
+ return match ($this) {
+ self::Monday => self::Tuesday,
+ self::Tuesday => self::Wednesday,
+ self::Wednesday => self::Thursday,
+ self::Thursday => self::Friday,
+ self::Friday => self::Saturday,
+ self::Saturday => self::Sunday,
+ self::Sunday => self::Monday,
+ };
+ }
+}
diff --git a/src/Psl/DateTime/constants.php b/src/Psl/DateTime/constants.php
new file mode 100644
index 00000000..c1e9936b
--- /dev/null
+++ b/src/Psl/DateTime/constants.php
@@ -0,0 +1,124 @@
+readHandle->read($max_bytes, $timeout);
}
diff --git a/src/Psl/File/ReadWriteHandle.php b/src/Psl/File/ReadWriteHandle.php
index 0795c412..1f6cbe1c 100644
--- a/src/Psl/File/ReadWriteHandle.php
+++ b/src/Psl/File/ReadWriteHandle.php
@@ -4,6 +4,7 @@
namespace Psl\File;
+use Psl\DateTime\Duration;
use Psl\Filesystem;
use Psl\IO;
use Psl\Str;
@@ -86,7 +87,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->readWriteHandle->read($max_bytes, $timeout);
}
@@ -102,7 +103,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->readWriteHandle->write($bytes, $timeout);
}
diff --git a/src/Psl/File/WriteHandle.php b/src/Psl/File/WriteHandle.php
index d1671fdb..c0651575 100644
--- a/src/Psl/File/WriteHandle.php
+++ b/src/Psl/File/WriteHandle.php
@@ -4,6 +4,7 @@
namespace Psl\File;
+use Psl\DateTime\Duration;
use Psl\Filesystem;
use Psl\IO;
use Psl\Str;
@@ -66,7 +67,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->writeHandle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/CloseReadStreamHandle.php b/src/Psl/IO/CloseReadStreamHandle.php
index 3ec42f13..f61dba4c 100644
--- a/src/Psl/IO/CloseReadStreamHandle.php
+++ b/src/Psl/IO/CloseReadStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -42,7 +43,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
diff --git a/src/Psl/IO/CloseReadWriteStreamHandle.php b/src/Psl/IO/CloseReadWriteStreamHandle.php
index 70d3fc6c..d3a56f14 100644
--- a/src/Psl/IO/CloseReadWriteStreamHandle.php
+++ b/src/Psl/IO/CloseReadWriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -43,7 +44,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
@@ -59,7 +60,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/CloseSeekReadStreamHandle.php b/src/Psl/IO/CloseSeekReadStreamHandle.php
index fe1269b8..b88fbd56 100644
--- a/src/Psl/IO/CloseSeekReadStreamHandle.php
+++ b/src/Psl/IO/CloseSeekReadStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -42,7 +43,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
diff --git a/src/Psl/IO/CloseSeekReadWriteStreamHandle.php b/src/Psl/IO/CloseSeekReadWriteStreamHandle.php
index 38928c26..02fc796e 100644
--- a/src/Psl/IO/CloseSeekReadWriteStreamHandle.php
+++ b/src/Psl/IO/CloseSeekReadWriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -43,7 +44,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
@@ -59,7 +60,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/CloseSeekWriteStreamHandle.php b/src/Psl/IO/CloseSeekWriteStreamHandle.php
index dead4b94..480ae3e0 100644
--- a/src/Psl/IO/CloseSeekWriteStreamHandle.php
+++ b/src/Psl/IO/CloseSeekWriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -34,7 +35,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/CloseWriteStreamHandle.php b/src/Psl/IO/CloseWriteStreamHandle.php
index ff86af6e..76017268 100644
--- a/src/Psl/IO/CloseWriteStreamHandle.php
+++ b/src/Psl/IO/CloseWriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -34,7 +35,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/Internal/ResourceHandle.php b/src/Psl/IO/Internal/ResourceHandle.php
index a2664bf9..a42789e3 100644
--- a/src/Psl/IO/Internal/ResourceHandle.php
+++ b/src/Psl/IO/Internal/ResourceHandle.php
@@ -6,6 +6,7 @@
use Psl;
use Psl\Async;
+use Psl\DateTime\Duration;
use Psl\IO;
use Psl\IO\Exception;
use Psl\Type;
@@ -51,14 +52,14 @@ class ResourceHandle implements IO\CloseSeekReadWriteStreamHandleInterface
protected mixed $stream;
/**
- * @var null|Async\Sequence>
+ * @var null|Async\Sequence>
*/
private ?Async\Sequence $writeSequence = null;
private ?Suspension $writeSuspension = null;
private string $writeWatcher = 'invalid';
/**
- * @var null|Async\Sequence, null|float}, string>
+ * @var null|Async\Sequence, null|Duration}, string>
*/
private ?Async\Sequence $readSequence = null;
private ?Suspension $readSuspension = null;
@@ -103,7 +104,7 @@ public function __construct(mixed $stream, bool $read, bool $write, bool $seek,
$this->readSequence = new Async\Sequence(
/**
- * @param array{null|int<1, max>, null|float} $input
+ * @param array{null|int<1, max>, null|Duration} $input
*/
function (array $input) use ($blocks): string {
[$max_bytes, $timeout] = $input;
@@ -116,7 +117,7 @@ function (array $input) use ($blocks): string {
EventLoop::enable($this->readWatcher);
$delay_watcher = null;
if (null !== $timeout) {
- $timeout = max($timeout, 0.0);
+ $timeout = max($timeout->getTotalSeconds(), 0.0);
$delay_watcher = EventLoop::delay(
$timeout,
static fn () => $suspension->throw(new Exception\TimeoutException('Reached timeout while the handle is still not readable.')),
@@ -159,7 +160,7 @@ function (array $input) use ($blocks): string {
$this->writeSequence = new Async\Sequence(
/**
- * @param array{string, null|float} $input
+ * @param array{string, null|Duration} $input
*
* @return int<0, max>
*/
@@ -175,7 +176,7 @@ function (array $input) use ($blocks): int {
EventLoop::enable($this->writeWatcher);
$delay_watcher = null;
if (null !== $timeout) {
- $timeout = max($timeout, 0.0);
+ $timeout = max($timeout->getTotalSeconds(), 0.0);
$delay_watcher = EventLoop::delay(
$timeout,
static fn () => $suspension->throw(new Exception\TimeoutException('Reached timeout while the handle is still not readable.')),
@@ -204,7 +205,7 @@ function (array $input) use ($blocks): int {
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
Psl\invariant($this->writeSequence !== null, 'The resource handle is not writable.');
@@ -283,7 +284,7 @@ public function reachedEndOfDataSource(): bool
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
Psl\invariant($this->readSequence !== null, 'The resource handle is not readable.');
diff --git a/src/Psl/IO/MemoryHandle.php b/src/Psl/IO/MemoryHandle.php
index a1fda6e9..f8182e43 100644
--- a/src/Psl/IO/MemoryHandle.php
+++ b/src/Psl/IO/MemoryHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\Math;
use function str_repeat;
@@ -76,7 +77,7 @@ public function tryRead(?int $max_bytes = null): string
*
* @psalm-external-mutation-free
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->tryRead($max_bytes);
}
@@ -110,7 +111,7 @@ public function tell(): int
*
* @psalm-external-mutation-free
*/
- public function tryWrite(string $bytes, ?float $timeout = null): int
+ public function tryWrite(string $bytes, ?Duration $timeout = null): int
{
$this->assertHandleIsOpen();
$length = strlen($this->buffer);
@@ -137,7 +138,7 @@ public function tryWrite(string $bytes, ?float $timeout = null): int
*
* @psalm-external-mutation-free
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->tryWrite($bytes);
}
diff --git a/src/Psl/IO/ReadHandleConvenienceMethodsTrait.php b/src/Psl/IO/ReadHandleConvenienceMethodsTrait.php
index e48fa411..8cede651 100644
--- a/src/Psl/IO/ReadHandleConvenienceMethodsTrait.php
+++ b/src/Psl/IO/ReadHandleConvenienceMethodsTrait.php
@@ -5,6 +5,7 @@
namespace Psl\IO;
use Psl;
+use Psl\DateTime\Duration;
use Psl\Str;
use function strlen;
@@ -30,7 +31,7 @@ trait ReadHandleConvenienceMethodsTrait
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- public function readAll(?int $max_bytes = null, ?float $timeout = null): string
+ public function readAll(?int $max_bytes = null, ?Duration $timeout = null): string
{
$to_read = $max_bytes;
@@ -78,7 +79,7 @@ static function () use ($data): void {
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- public function readFixedSize(int $size, ?float $timeout = null): string
+ public function readFixedSize(int $size, ?Duration $timeout = null): string
{
$data = $this->readAll($size, $timeout);
diff --git a/src/Psl/IO/ReadHandleInterface.php b/src/Psl/IO/ReadHandleInterface.php
index 01869e09..9de72326 100644
--- a/src/Psl/IO/ReadHandleInterface.php
+++ b/src/Psl/IO/ReadHandleInterface.php
@@ -4,6 +4,8 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
+
/**
* An `IO\Handle` that is readable.
*/
@@ -61,7 +63,7 @@ public function tryRead(?int $max_bytes = null): string;
* Up to `$max_bytes` may be allocated in a buffer; large values may lead to
* unnecessarily hitting the request memory limit.
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string;
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string;
/**
* Read until there is no more data to read.
@@ -79,7 +81,7 @@ public function read(?int $max_bytes = null, ?float $timeout = null): string;
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- public function readAll(?int $max_bytes = null, ?float $timeout = null): string;
+ public function readAll(?int $max_bytes = null, ?Duration $timeout = null): string;
/**
* Read a fixed amount of data.
@@ -94,5 +96,5 @@ public function readAll(?int $max_bytes = null, ?float $timeout = null): string;
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- public function readFixedSize(int $size, ?float $timeout = null): string;
+ public function readFixedSize(int $size, ?Duration $timeout = null): string;
}
diff --git a/src/Psl/IO/ReadStreamHandle.php b/src/Psl/IO/ReadStreamHandle.php
index 6c71c877..9530daba 100644
--- a/src/Psl/IO/ReadStreamHandle.php
+++ b/src/Psl/IO/ReadStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -42,7 +43,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
diff --git a/src/Psl/IO/ReadWriteStreamHandle.php b/src/Psl/IO/ReadWriteStreamHandle.php
index fb4a67ba..9f30d5c4 100644
--- a/src/Psl/IO/ReadWriteStreamHandle.php
+++ b/src/Psl/IO/ReadWriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -43,7 +44,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
@@ -59,7 +60,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/Reader.php b/src/Psl/IO/Reader.php
index 906e609f..ee5b3e0b 100644
--- a/src/Psl/IO/Reader.php
+++ b/src/Psl/IO/Reader.php
@@ -5,6 +5,7 @@
namespace Psl\IO;
use Psl\Async;
+use Psl\DateTime\Duration;
use Psl\Str;
use function strlen;
@@ -57,7 +58,7 @@ public function reachedEndOfDataSource(): bool
/**
* {@inheritDoc}
*/
- public function readFixedSize(int $size, ?float $timeout = null): string
+ public function readFixedSize(int $size, ?Duration $timeout = null): string
{
$timer = new Async\OptionalIncrementalTimeout(
$timeout,
@@ -100,7 +101,7 @@ function (): void {
* @throws Exception\RuntimeException If an error occurred during the operation, or reached end of file.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- public function readByte(?float $timeout = null): string
+ public function readByte(?Duration $timeout = null): string
{
if ($this->buffer === '' && !$this->eof) {
$this->fillBuffer(null, $timeout);
@@ -128,7 +129,7 @@ public function readByte(?float $timeout = null): string
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- public function readLine(?float $timeout = null): ?string
+ public function readLine(?Duration $timeout = null): ?string
{
$timer = new Async\OptionalIncrementalTimeout(
$timeout,
@@ -164,7 +165,7 @@ static function (): void {
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- public function readUntil(string $suffix, ?float $timeout = null): ?string
+ public function readUntil(string $suffix, ?Duration $timeout = null): ?string
{
$buf = $this->buffer;
$idx = strpos($buf, $suffix);
@@ -208,7 +209,7 @@ static function () use ($suffix): void {
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
if ($this->eof) {
return '';
@@ -262,7 +263,7 @@ public function getHandle(): ReadHandleInterface
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If $timeout is reached before being able to read from the handle.
*/
- private function fillBuffer(?int $desired_bytes, ?float $timeout): void
+ private function fillBuffer(?int $desired_bytes, ?Duration $timeout): void
{
$this->buffer .= $chunk = $this->handle->read($desired_bytes, $timeout);
if ($chunk === '') {
diff --git a/src/Psl/IO/SeekReadStreamHandle.php b/src/Psl/IO/SeekReadStreamHandle.php
index ca74f2e3..bc0a0392 100644
--- a/src/Psl/IO/SeekReadStreamHandle.php
+++ b/src/Psl/IO/SeekReadStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -42,7 +43,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
diff --git a/src/Psl/IO/SeekReadWriteStreamHandle.php b/src/Psl/IO/SeekReadWriteStreamHandle.php
index ea78e8ce..80b35fcc 100644
--- a/src/Psl/IO/SeekReadWriteStreamHandle.php
+++ b/src/Psl/IO/SeekReadWriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -43,7 +44,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
@@ -59,7 +60,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/SeekWriteStreamHandle.php b/src/Psl/IO/SeekWriteStreamHandle.php
index d12f0f7e..aaa6bb7e 100644
--- a/src/Psl/IO/SeekWriteStreamHandle.php
+++ b/src/Psl/IO/SeekWriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -34,7 +35,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/WriteHandleConvenienceMethodsTrait.php b/src/Psl/IO/WriteHandleConvenienceMethodsTrait.php
index 2b5ddd1f..fd851c66 100644
--- a/src/Psl/IO/WriteHandleConvenienceMethodsTrait.php
+++ b/src/Psl/IO/WriteHandleConvenienceMethodsTrait.php
@@ -5,6 +5,7 @@
namespace Psl\IO;
use Psl;
+use Psl\DateTime\Duration;
use Psl\Str;
use function strlen;
@@ -30,7 +31,7 @@ trait WriteHandleConvenienceMethodsTrait
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If reached timeout before completing the operation.
*/
- public function writeAll(string $bytes, ?float $timeout = null): void
+ public function writeAll(string $bytes, ?Duration $timeout = null): void
{
if ($bytes === '') {
return;
diff --git a/src/Psl/IO/WriteHandleInterface.php b/src/Psl/IO/WriteHandleInterface.php
index 6a47aed5..406c1f92 100644
--- a/src/Psl/IO/WriteHandleInterface.php
+++ b/src/Psl/IO/WriteHandleInterface.php
@@ -4,6 +4,8 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
+
/**
* An interface for a writable Handle.
*/
@@ -34,7 +36,7 @@ public function tryWrite(string $bytes): int;
*
* @return int<0, max> the number of bytes written, which may be less than the length of input string.
*/
- public function write(string $bytes, ?float $timeout = null): int;
+ public function write(string $bytes, ?Duration $timeout = null): int;
/**
* Write all of the requested data.
@@ -51,5 +53,5 @@ public function write(string $bytes, ?float $timeout = null): int;
* @throws Exception\RuntimeException If an error occurred during the operation.
* @throws Exception\TimeoutException If reached timeout before completing the operation.
*/
- public function writeAll(string $bytes, ?float $timeout = null): void;
+ public function writeAll(string $bytes, ?Duration $timeout = null): void;
}
diff --git a/src/Psl/IO/WriteStreamHandle.php b/src/Psl/IO/WriteStreamHandle.php
index 2bb4624d..bf746997 100644
--- a/src/Psl/IO/WriteStreamHandle.php
+++ b/src/Psl/IO/WriteStreamHandle.php
@@ -4,6 +4,7 @@
namespace Psl\IO;
+use Psl\DateTime\Duration;
use Psl\IO;
/**
@@ -34,7 +35,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/IO/streaming.php b/src/Psl/IO/streaming.php
index faead9e1..463d3c2d 100644
--- a/src/Psl/IO/streaming.php
+++ b/src/Psl/IO/streaming.php
@@ -7,10 +7,13 @@
use Generator;
use Psl;
use Psl\Channel;
+use Psl\DateTime\Duration;
use Psl\Result;
use Psl\Str;
use Revolt\EventLoop;
+use function max;
+
/**
* Streaming the output of the given read stream handles using a generator.
*
@@ -35,7 +38,7 @@
*
* @return Generator
*/
-function streaming(iterable $handles, ?float $timeout = null): Generator
+function streaming(iterable $handles, ?Duration $timeout = null): Generator
{
/**
* @psalm-suppress UnnecessaryVarAnnotation
@@ -72,6 +75,8 @@ function streaming(iterable $handles, ?float $timeout = null): Generator
$timeout_watcher = null;
if ($timeout !== null) {
+ $timeout = max($timeout->getTotalSeconds(), 0.0);
+
$timeout_watcher = EventLoop::delay($timeout, static function () use ($sender): void {
/** @var Result\ResultInterface $failure */
$failure = new Result\Failure(
diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php
index 4d9ea29e..9a1e2729 100644
--- a/src/Psl/Internal/Loader.php
+++ b/src/Psl/Internal/Loader.php
@@ -54,6 +54,23 @@ final class Loader
'Psl\\Str\\ALPHABET' => 'Psl/Str/constants.php',
'Psl\\Str\\ALPHABET_ALPHANUMERIC' => 'Psl/Str/constants.php',
'Psl\\Filesystem\\SEPARATOR' => 'Psl/Filesystem/constants.php',
+ 'Psl\\DateTime\\NANOSECONDS_PER_MICROSECOND' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\NANOSECONDS_PER_MILLISECOND' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\NANOSECONDS_PER_SECOND' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\MICROSECONDS_PER_MILLISECOND' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\MICROSECONDS_PER_SECOND' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\MILLISECONDS_PER_SECOND' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\SECONDS_PER_MINUTE' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\SECONDS_PER_HOUR' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\SECONDS_PER_DAY' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\SECONDS_PER_WEEK' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\MINUTES_PER_HOUR' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\MINUTES_PER_DAY' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\MINUTES_PER_WEEK' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\HOURS_PER_DAY' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\HOURS_PER_WEEK' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\DAYS_PER_WEEK' => 'Psl/DateTime/constants.php',
+ 'Psl\\DateTime\\MONTHS_PER_YEAR' => 'Psl/DateTime/constants.php',
];
public const FUNCTIONS = [
@@ -521,6 +538,14 @@ final class Loader
'Psl\\Range\\to' => 'Psl/Range/to.php',
'Psl\\Range\\between' => 'Psl/Range/between.php',
'Psl\\Range\\full' => 'Psl/Range/full.php',
+ 'Psl\\DateTime\\is_leap_year' => 'Psl/DateTime/is_leap_year.php',
+ 'Psl\\DateTime\\Internal\\to_intl_timezone' => 'Psl/DateTime/Internal/to_intl_timezone.php',
+ 'Psl\\DateTime\\Internal\\default_timezone' => 'Psl/DateTime/Internal/default_timezone.php',
+ 'Psl\\DateTime\\Internal\\system_time' => 'Psl/DateTime/Internal/system_time.php',
+ 'Psl\\DateTime\\Internal\\high_resolution_time' => 'Psl/DateTime/Internal/high_resolution_time.php',
+ 'Psl\\DateTime\\Internal\\create_intl_date_formatter' => 'Psl/DateTime/Internal/create_intl_date_formatter.php',
+ 'Psl\\DateTime\\Internal\\parse' => 'Psl/DateTime/Internal/parse.php',
+ 'Psl\\DateTime\\Internal\\format_rfc3339' => 'Psl/DateTime/Internal/format_rfc3339.php',
];
public const INTERFACES = [
@@ -613,6 +638,9 @@ final class Loader
'Psl\\Range\\LowerBoundRangeInterface' => 'Psl/Range/LowerBoundRangeInterface.php',
'Psl\\Range\\UpperBoundRangeInterface' => 'Psl/Range/UpperBoundRangeInterface.php',
'Psl\\Default\\DefaultInterface' => 'Psl/Default/DefaultInterface.php',
+ 'Psl\\DateTime\\Exception\\ExceptionInterface' => 'Psl/DateTime/Exception/ExceptionInterface.php',
+ 'Psl\\DateTime\\TemporalInterface' => 'Psl/DateTime/TemporalInterface.php',
+ 'Psl\\DateTime\\DateTimeInterface' => 'Psl/DateTime/DateTimeInterface.php',
];
public const TRAITS = [
@@ -620,6 +648,8 @@ final class Loader
'Psl\\IO\\ReadHandleConvenienceMethodsTrait' => 'Psl/IO/ReadHandleConvenienceMethodsTrait.php',
'Psl\\IO\\WriteHandleConvenienceMethodsTrait' => 'Psl/IO/WriteHandleConvenienceMethodsTrait.php',
'Psl\\Channel\\Internal\\ChannelSideTrait' => 'Psl/Channel/Internal/ChannelSideTrait.php',
+ 'Psl\\DateTime\\TemporalConvenienceMethodsTrait' => 'Psl/DateTime/TemporalConvenienceMethodsTrait.php',
+ 'Psl\\DateTime\\DateTimeConvenienceMethodsTrait' => 'Psl/DateTime/DateTimeConvenienceMethodsTrait.php',
];
public const CLASSES = [
@@ -639,6 +669,7 @@ final class Loader
'Psl\\Encoding\\Base64\\Internal\\Base64DotSlashOrdered' => 'Psl/Encoding/Base64/Internal/Base64DotSlashOrdered.php',
'Psl\\Exception\\OverflowException' => 'Psl/Exception/OverflowException.php',
'Psl\\Exception\\InvalidArgumentException' => 'Psl/Exception/InvalidArgumentException.php',
+ 'Psl\\Exception\\UnexpectedValueException' => 'Psl/Exception/UnexpectedValueException.php',
'Psl\\Exception\\RuntimeException' => 'Psl/Exception/RuntimeException.php',
'Psl\\Exception\\InvariantViolationException' => 'Psl/Exception/InvariantViolationException.php',
'Psl\\Exception\\UnderflowException' => 'Psl/Exception/UnderflowException.php',
@@ -815,6 +846,14 @@ final class Loader
'Psl\\Range\\ToRange' => 'Psl/Range/ToRange.php',
'Psl\\Range\\BetweenRange' => 'Psl/Range/BetweenRange.php',
'Psl\\Range\\FullRange' => 'Psl/Range/FullRange.php',
+ 'Psl\\DateTime\\Exception\\InvalidArgumentException' => 'Psl/DateTime/Exception/InvalidArgumentException.php',
+ 'Psl\\DateTime\\Exception\\OverflowException' => 'Psl/DateTime/Exception/OverflowException.php',
+ 'Psl\\DateTime\\Exception\\RuntimeException' => 'Psl/DateTime/Exception/RuntimeException.php',
+ 'Psl\\DateTime\\Exception\\ParserException' => 'Psl/DateTime/Exception/ParserException.php',
+ 'Psl\\DateTime\\Exception\\UnderflowException' => 'Psl/DateTime/Exception/UnderflowException.php',
+ 'Psl\\DateTime\\DateTime' => 'Psl/DateTime/DateTime.php',
+ 'Psl\\DateTime\\Duration' => 'Psl/DateTime/Interval.php',
+ 'Psl\\DateTime\\Timestamp' => 'Psl/DateTime/Timestamp.php',
];
public const ENUMS = [
@@ -831,6 +870,15 @@ final class Loader
'Psl\\Password\\Algorithm' => 'Psl/Password/Algorithm.php',
'Psl\\Shell\\ErrorOutputBehavior' => 'Psl/Shell/ErrorOutputBehavior.php',
'Psl\\Locale\\Locale' => 'Psl/Locale/Locale.php',
+ 'Psl\\DateTime\\FormatPattern' => 'Psl/DateTime/FormatPattern.php',
+ 'Psl\\DateTime\\TimeStyle' => 'Psl/DateTime/TimeStyle.php',
+ 'Psl\\DateTime\\DateStyle' => 'Psl/DateTime/DateStyle.php',
+ 'Psl\\DateTime\\SecondsStyle' => 'Psl/DateTime/SecondsStyle.php',
+ 'Psl\\DateTime\\Era' => 'Psl/DateTime/Era.php',
+ 'Psl\\DateTime\\Meridiem' => 'Psl/DateTime/Meridiem.php',
+ 'Psl\\DateTime\\Month' => 'Psl/DateTime/Weekday.php',
+ 'Psl\\DateTime\\Timezone' => 'Psl/DateTime/Timezone.php',
+ 'Psl\\DateTime\\Weekday' => 'Psl/DateTime/Weekday.php',
];
public const TYPE_CONSTANTS = 1;
diff --git a/src/Psl/Locale/Locale.php b/src/Psl/Locale/Locale.php
index e93d9ed2..bd72fa38 100644
--- a/src/Psl/Locale/Locale.php
+++ b/src/Psl/Locale/Locale.php
@@ -810,6 +810,8 @@ enum Locale: string
* @return self The default locale as an enum instance, sourced from PHP settings or `self::English` as the fallback.
*
* @see https://www.php.net/manual/en/locale.getdefault.php
+ *
+ * @psalm-mutation-free
*/
public static function default(): self
{
diff --git a/src/Psl/Network/Internal/Socket.php b/src/Psl/Network/Internal/Socket.php
index 25f61908..43f5327f 100644
--- a/src/Psl/Network/Internal/Socket.php
+++ b/src/Psl/Network/Internal/Socket.php
@@ -4,6 +4,7 @@
namespace Psl\Network\Internal;
+use Psl\DateTime\Duration;
use Psl\IO;
use Psl\IO\Exception;
use Psl\IO\Internal;
@@ -51,7 +52,7 @@ public function tryRead(?int $max_bytes = null): string
/**
* {@inheritDoc}
*/
- public function read(?int $max_bytes = null, ?float $timeout = null): string
+ public function read(?int $max_bytes = null, ?Duration $timeout = null): string
{
return $this->handle->read($max_bytes, $timeout);
}
@@ -67,7 +68,7 @@ public function tryWrite(string $bytes): int
/**
* {@inheritDoc}
*/
- public function write(string $bytes, ?float $timeout = null): int
+ public function write(string $bytes, ?Duration $timeout = null): int
{
return $this->handle->write($bytes, $timeout);
}
diff --git a/src/Psl/Network/Internal/socket_connect.php b/src/Psl/Network/Internal/socket_connect.php
index 50420a27..088e92bd 100644
--- a/src/Psl/Network/Internal/socket_connect.php
+++ b/src/Psl/Network/Internal/socket_connect.php
@@ -4,12 +4,14 @@
namespace Psl\Network\Internal;
+use Psl\DateTime\Duration;
use Psl\Internal;
use Psl\Network\Exception;
use Revolt\EventLoop;
use function fclose;
use function is_resource;
+use function max;
use function stream_context_create;
use function stream_socket_client;
@@ -28,7 +30,7 @@
*
* @codeCoverageIgnore
*/
-function socket_connect(string $uri, array $context = [], ?float $timeout = null): mixed
+function socket_connect(string $uri, array $context = [], ?Duration $timeout = null): mixed
{
return Internal\suppress(static function () use ($uri, $context, $timeout): mixed {
$context = stream_context_create($context);
@@ -42,6 +44,7 @@ function socket_connect(string $uri, array $context = [], ?float $timeout = null
$write_watcher = '';
$timeout_watcher = '';
if (null !== $timeout) {
+ $timeout = max($timeout->getTotalSeconds(), 0.0);
$timeout_watcher = EventLoop::delay($timeout, static function () use ($suspension, &$write_watcher, $socket) {
EventLoop::cancel($write_watcher);
diff --git a/src/Psl/Shell/execute.php b/src/Psl/Shell/execute.php
index 03519e9d..090651c3 100644
--- a/src/Psl/Shell/execute.php
+++ b/src/Psl/Shell/execute.php
@@ -4,6 +4,7 @@
namespace Psl\Shell;
+use Psl\DateTime\Duration;
use Psl\Dict;
use Psl\Env;
use Psl\Filesystem;
@@ -44,7 +45,7 @@ function execute(
?string $working_directory = null,
array $environment = [],
ErrorOutputBehavior $error_output_behavior = ErrorOutputBehavior::Discard,
- ?float $timeout = null
+ ?Duration $timeout = null
): string {
$arguments = Vec\map($arguments, Internal\escape_argument(...));
$commandline = Str\join([$command, ...$arguments], ' ');
diff --git a/src/Psl/TCP/connect.php b/src/Psl/TCP/connect.php
index e69f8a53..ed26a37d 100644
--- a/src/Psl/TCP/connect.php
+++ b/src/Psl/TCP/connect.php
@@ -4,6 +4,7 @@
namespace Psl\TCP;
+use Psl\DateTime\Duration;
use Psl\Network;
/**
@@ -15,12 +16,8 @@
* @throws Network\Exception\RuntimeException If failed to connect to client on the given address.
* @throws Network\Exception\TimeoutException If $timeout is non-null, and the operation timed-out.
*/
-function connect(
- string $host,
- int $port = 0,
- ?ConnectOptions $options = null,
- ?float $timeout = null,
-): Network\StreamSocketInterface {
+function connect(string $host, int $port = 0, ?ConnectOptions $options = null, ?Duration $timeout = null): Network\StreamSocketInterface
+{
$options ??= ConnectOptions::create();
$context = [
@@ -29,7 +26,7 @@ function connect(
]
];
- $socket = Network\Internal\socket_connect("tcp://{$host}:{$port}", $context, $timeout);
+ $socket = Network\Internal\socket_connect("tcp://$host:$port", $context, $timeout);
/** @psalm-suppress MissingThrowsDocblock */
return new Network\Internal\Socket($socket);
diff --git a/src/Psl/Unix/connect.php b/src/Psl/Unix/connect.php
index bdcbf34b..9ceb71ed 100644
--- a/src/Psl/Unix/connect.php
+++ b/src/Psl/Unix/connect.php
@@ -4,6 +4,7 @@
namespace Psl\Unix;
+use Psl\DateTime\Duration;
use Psl\Network;
use Psl\OS;
@@ -15,7 +16,7 @@
* @throws Network\Exception\RuntimeException If failed to connect to client on the given address.
* @throws Network\Exception\TimeoutException If $timeout is non-null, and the operation timed-out.
*/
-function connect(string $path, ?float $timeout = null): Network\StreamSocketInterface
+function connect(string $path, ?Duration $timeout = null): Network\StreamSocketInterface
{
// @codeCoverageIgnoreStart
if (OS\is_windows()) {
diff --git a/tests/unit/Async/AllTest.php b/tests/unit/Async/AllTest.php
index e06a486c..26957cd3 100644
--- a/tests/unit/Async/AllTest.php
+++ b/tests/unit/Async/AllTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use Psl;
use Psl\Async;
+use Psl\DateTime;
use Psl\Exception\InvariantViolationException;
final class AllTest extends TestCase
@@ -15,17 +16,17 @@ public function testAll(): void
{
$awaitables = [
'a' => Async\run(static function (): string {
- Async\sleep(0.003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
return 'a';
}),
'b' => Async\run(static function (): string {
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
return 'b';
}),
'c' => Async\run(static function (): string {
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
return 'c';
}),
@@ -81,23 +82,23 @@ public function testAllAwaitablesAreCompletedAtALaterTime(): void
throw new InvariantViolationException('a');
}),
Async\run(static function () use ($ref): void {
- Async\sleep(0.02);
+ Async\sleep(DateTime\Duration::milliseconds(20));
$ref->value .= 'b';
throw new InvariantViolationException('b');
}),
Async\run(static function () use ($ref): void {
- Async\sleep(0.05);
+ Async\sleep(DateTime\Duration::milliseconds(50));
$ref->value .= 'c';
}),
Async\run(static function () use ($ref): void {
- Async\sleep(0.00005);
+ Async\sleep(DateTime\Duration::microseconds(5));
Async\later();
- Async\sleep(0.00005);
+ Async\sleep(DateTime\Duration::microseconds(5));
$ref->value .= 'd';
}),
diff --git a/tests/unit/Async/AnyTest.php b/tests/unit/Async/AnyTest.php
index 1359d137..a054a6d3 100644
--- a/tests/unit/Async/AnyTest.php
+++ b/tests/unit/Async/AnyTest.php
@@ -6,6 +6,7 @@
use PHPUnit\Framework\TestCase;
use Psl\Async;
+use Psl\DateTime;
use Psl\Exception\InvariantViolationException;
final class AnyTest extends TestCase
@@ -14,26 +15,26 @@ public function testAny(): void
{
$result = Async\any([
Async\run(static function (): string {
- Async\sleep(0.0001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
throw new InvariantViolationException('a');
}),
Async\run(static function (): string {
- Async\sleep(0.0002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
throw new InvariantViolationException('b');
}),
Async\run(static function (): string {
- Async\sleep(0.0003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
return 'c';
}),
Async\run(static function (): string {
- Async\sleep(0.00005);
+ Async\sleep(DateTime\Duration::microseconds(500));
Async\later();
- Async\sleep(0.00005);
+ Async\sleep(DateTime\Duration::microseconds(500));
return 'c';
}),
diff --git a/tests/unit/Async/AwaitableTest.php b/tests/unit/Async/AwaitableTest.php
index 1784c735..b694e75a 100644
--- a/tests/unit/Async/AwaitableTest.php
+++ b/tests/unit/Async/AwaitableTest.php
@@ -10,6 +10,7 @@
use Psl\Async\Awaitable;
use Psl\Async\Exception\UnhandledAwaitableException;
use Psl\Async\Internal\State;
+use Psl\DateTime;
use Psl\Dict;
use Psl\Exception\InvariantViolationException;
use Psl\Str;
@@ -94,12 +95,12 @@ public function testIterate(): void
'foo' => Awaitable::complete('foo'),
'bar' => Awaitable::error(new InvariantViolationException('bar')),
'baz' => Async\run(static function () {
- Async\sleep(0.0001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
throw new InvariantViolationException('baz');
}),
'qux' => Async\run(static function () {
- Async\sleep(0.003);
+ Async\sleep(DateTime\Duration::milliseconds(30));
return 'qux';
}),
@@ -141,7 +142,7 @@ public function testIterateGenerator(): void
$generator1 = Async\run(static function (): iterable {
yield 'foo' => 'foo';
- Async\sleep(0.0003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
yield 'bar' => 'bar';
});
@@ -149,7 +150,7 @@ public function testIterateGenerator(): void
$generator2 = Async\run(static function (): iterable {
yield 'baz' => 'baz';
- Async\sleep(0.0001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
yield 'qux' => 'qux';
});
@@ -157,7 +158,7 @@ public function testIterateGenerator(): void
$generator3 = Async\run(static function () use ($generator1, $generator2): iterable {
yield 'gen1' => $generator1;
- Async\sleep(0.0002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
yield 'gen2' => $generator2;
})->await();
diff --git a/tests/unit/Async/DeferredTest.php b/tests/unit/Async/DeferredTest.php
index e1995e51..f9a80b5d 100644
--- a/tests/unit/Async/DeferredTest.php
+++ b/tests/unit/Async/DeferredTest.php
@@ -6,6 +6,7 @@
use PHPUnit\Framework\TestCase;
use Psl\Async;
+use Psl\DateTime;
use Psl\Exception\InvariantViolationException;
final class DeferredTest extends TestCase
@@ -15,7 +16,7 @@ public function testComplete(): void
$deferred = new Async\Deferred();
$placeholder = Async\run(static function () use ($deferred) {
- Async\sleep(0.0001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
$deferred->complete('hello');
});
@@ -34,7 +35,7 @@ public function testError(): void
$deferred = new Async\Deferred();
$placeholder = Async\run(static function () use ($deferred) {
- Async\sleep(0.0001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
$deferred->error(new InvariantViolationException('hello'));
});
diff --git a/tests/unit/Async/FirstTest.php b/tests/unit/Async/FirstTest.php
index 4702b5c2..48f6be5f 100644
--- a/tests/unit/Async/FirstTest.php
+++ b/tests/unit/Async/FirstTest.php
@@ -6,6 +6,7 @@
use PHPUnit\Framework\TestCase;
use Psl\Async;
+use Psl\DateTime;
final class FirstTest extends TestCase
{
@@ -13,26 +14,26 @@ public function testFirst(): void
{
$result = Async\first([
Async\run(static function (): string {
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
return 'a';
}),
Async\run(static function (): string {
- Async\sleep(0.002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
return 'b';
}),
Async\run(static function (): string {
- Async\sleep(0.003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
return 'c';
}),
Async\run(static function (): string {
- Async\sleep(0.0005);
+ Async\sleep(DateTime\Duration::milliseconds(5));
Async\later();
- Async\sleep(0.0005);
+ Async\sleep(DateTime\Duration::milliseconds(5));
return 'c';
}),
diff --git a/tests/unit/Async/KeyedSemaphoreTest.php b/tests/unit/Async/KeyedSemaphoreTest.php
index b731c419..cc9821c5 100644
--- a/tests/unit/Async/KeyedSemaphoreTest.php
+++ b/tests/unit/Async/KeyedSemaphoreTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use Psl;
use Psl\Async;
+use Psl\DateTime;
final class KeyedSemaphoreTest extends TestCase
{
@@ -26,7 +27,7 @@ public function testSequenceOperationWaitsForPendingOperationsWhenLimitIsNotReac
$spy = new Psl\Ref([]);
/**
- * @var Async\KeyedSemaphore
+ * @var Async\KeyedSemaphore
*/
$ks = new Async\KeyedSemaphore(1, static function (string $key, array $data) use ($spy): void {
static::assertSame('operation', $key);
@@ -38,9 +39,9 @@ public function testSequenceOperationWaitsForPendingOperationsWhenLimitIsNotReac
$spy->value[] = $data['value'];
});
- Async\run(static fn() => $ks->waitFor('operation', ['time' => 0.003, 'value' => 'a']));
- Async\run(static fn() => $ks->waitFor('operation', ['time' => 0.004, 'value' => 'b']));
- Async\run(static fn() => $ks->waitFor('operation', ['time' => 0.005, 'value' => 'c']));
+ Async\run(static fn() => $ks->waitFor('operation', ['time' => DateTime\Duration::milliseconds(3), 'value' => 'a']));
+ Async\run(static fn() => $ks->waitFor('operation', ['time' => DateTime\Duration::milliseconds(4), 'value' => 'b']));
+ Async\run(static fn() => $ks->waitFor('operation', ['time' => DateTime\Duration::milliseconds(5), 'value' => 'c']));
$last = Async\run(static fn() => $ks->waitFor('operation', ['time' => null, 'value' => 'd']));
$last->await();
@@ -52,7 +53,7 @@ public function testOperationWaitsForPendingOperationsWhenLimitIsNotReached(): v
$spy = new Psl\Ref([]);
/**
- * @var Async\KeyedSemaphore
+ * @var Async\KeyedSemaphore
*/
$ks = new Async\KeyedSemaphore(2, static function (string $_, array $data) use ($spy): void {
if ($data['time'] !== null) {
@@ -62,9 +63,9 @@ public function testOperationWaitsForPendingOperationsWhenLimitIsNotReached(): v
$spy->value[] = $data['value'];
});
- Async\run(static fn() => $ks->waitFor('key', ['time' => 0.003, 'value' => 'a']));
- Async\run(static fn() => $ks->waitFor('key', ['time' => 0.004, 'value' => 'b']));
- $beforeLast = Async\run(static fn() => $ks->waitFor('key', ['time' => 0.005, 'value' => 'c']));
+ Async\run(static fn() => $ks->waitFor('key', ['time' => DateTime\Duration::milliseconds(3), 'value' => 'a']));
+ Async\run(static fn() => $ks->waitFor('key', ['time' => DateTime\Duration::milliseconds(4), 'value' => 'b']));
+ $beforeLast = Async\run(static fn() => $ks->waitFor('key', ['time' => DateTime\Duration::milliseconds(5), 'value' => 'c']));
Async\run(static fn() => $ks->waitFor('key', ['time' => null, 'value' => 'd']));
$beforeLast->await();
@@ -82,7 +83,7 @@ public function testOperationIsStartedIfLimitIsNotReached(): void
$ks = new Async\KeyedSemaphore(1, static function (string $_, string $input) use ($spy): void {
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
});
$awaitable = Async\run(static fn() => $ks->waitFor('x', 'hello'));
@@ -104,13 +105,13 @@ public function testOperationIsNotStartedIfLimitIsReached(): void
$semaphore = new Async\KeyedSemaphore(1, static function (string $_, string $input) use ($spy): void {
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
});
Async\run(static fn() => $semaphore->waitFor('x', 'hello'));
$awaitable = Async\run(static fn() => $semaphore->waitFor('x', 'world'));
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
static::assertNotContains('world', $spy->value);
@@ -137,7 +138,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
* @var Async\KeyedSemaphore
*/
$ks = new Async\KeyedSemaphore(1, static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -145,7 +146,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
$one = Async\run(static fn() => $ks->waitFor('foo', 'one'));
$two = Async\run(static fn() => $ks->waitFor('foo', 'two'));
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
$ks->cancel('foo', new Async\Exception\TimeoutException('The semaphore is destroyed.'));
@@ -163,7 +164,7 @@ public function testCancelAllPendingOperations(): void
* @var Async\KeyedSemaphore
*/
$ks = new Async\KeyedSemaphore(1, static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -180,7 +181,7 @@ public function testCancelAllPendingOperations(): void
Async\run(static fn() => $ks->waitFor('baz', 'pending'))
];
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
$ks->cancelAll(new Async\Exception\TimeoutException('The semaphore is destroyed.'));
@@ -203,7 +204,7 @@ public function testSemaphoreStatus(): void
* @var Async\KeyedSemaphore
*/
$ks = new Async\KeyedSemaphore(1, static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -255,7 +256,7 @@ public function testWaitForRoom(): void
* @var Async\KeyedSemaphore
*/
$ks = new Async\KeyedSemaphore(1, static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -273,7 +274,7 @@ public function testConcurrencyLimitOnDifferentKeys(): void
* @var Async\KeyedSemaphore
*/
$ks = new Async\KeyedSemaphore(1, static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
static::assertSame(1, $ks->getConcurrencyLimit());
diff --git a/tests/unit/Async/KeyedSequenceTest.php b/tests/unit/Async/KeyedSequenceTest.php
index 0be9737e..5f1074d1 100644
--- a/tests/unit/Async/KeyedSequenceTest.php
+++ b/tests/unit/Async/KeyedSequenceTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use Psl;
use Psl\Async;
+use Psl\DateTime;
final class KeyedSequenceTest extends TestCase
{
@@ -38,9 +39,9 @@ public function testSequenceOperationWaitsForPendingOperationsWhenLimitIsNotReac
$spy->value[] = $data['value'];
});
- Async\run(static fn() => $ks->waitFor('operation', ['time' => 0.003, 'value' => 'a']));
- Async\run(static fn() => $ks->waitFor('operation', ['time' => 0.004, 'value' => 'b']));
- Async\run(static fn() => $ks->waitFor('operation', ['time' => 0.005, 'value' => 'c']));
+ Async\run(static fn() => $ks->waitFor('operation', ['time' => DateTime\Duration::milliseconds(3), 'value' => 'a']));
+ Async\run(static fn() => $ks->waitFor('operation', ['time' => DateTime\Duration::milliseconds(4), 'value' => 'b']));
+ Async\run(static fn() => $ks->waitFor('operation', ['time' => DateTime\Duration::milliseconds(5), 'value' => 'c']));
$last = Async\run(static fn() => $ks->waitFor('operation', ['time' => null, 'value' => 'd']));
$last->await();
@@ -54,10 +55,12 @@ public function testOperationIsStartedIfLimitIsNotReached(): void
/**
* @var Async\KeyedSequence
*/
- $ks = new Async\KeyedSequence(static function (string $_, string $input) use ($spy): void {
+ $ks = new Async\KeyedSequence(static function (string $key, string $input) use ($spy): void {
+ static::assertSame('x', $key);
+
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
});
$awaitable = Async\run(static fn() => $ks->waitFor('x', 'hello'));
@@ -79,13 +82,13 @@ public function testOperationIsNotStartedIfLimitIsReached(): void
$ks = new Async\KeyedSequence(static function (string $_, string $input) use ($spy): void {
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
});
Async\run(static fn() => $ks->waitFor('x', 'hello'));
$awaitable = Async\run(static fn() => $ks->waitFor('x', 'world'));
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
static::assertNotContains('world', $spy->value);
@@ -112,7 +115,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
* @var Async\KeyedSequence
*/
$ks = new Async\KeyedSequence(static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -120,7 +123,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
$one = Async\run(static fn() => $ks->waitFor('foo', 'one'));
$two = Async\run(static fn() => $ks->waitFor('foo', 'two'));
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
$ks->cancel('foo', new Async\Exception\TimeoutException('The semaphore is destroyed.'));
@@ -138,7 +141,7 @@ public function testCancelAllPendingOperations(): void
* @var Async\KeyedSequence
*/
$ks = new Async\KeyedSequence(static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -155,7 +158,7 @@ public function testCancelAllPendingOperations(): void
Async\run(static fn() => $ks->waitFor('baz', 'pending'))
];
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
$ks->cancelAll(new Async\Exception\TimeoutException('The semaphore is destroyed.'));
@@ -178,7 +181,7 @@ public function testSemaphoreStatus(): void
* @var Async\KeyedSequence
*/
$ks = new Async\KeyedSequence(static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -226,7 +229,8 @@ public function testWaitForRoom(): void
* @var Async\KeyedSequence
*/
$ks = new Async\KeyedSequence(static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
+
return $input;
});
@@ -244,7 +248,8 @@ public function testConcurrencyLimitOnDifferentKeys(): void
* @var Async\KeyedSequence
*/
$ks = new Async\KeyedSequence(static function (string $_, string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
+
return $input;
});
diff --git a/tests/unit/Async/ParallelTest.php b/tests/unit/Async/ParallelTest.php
index faac7241..b90fe5fc 100644
--- a/tests/unit/Async/ParallelTest.php
+++ b/tests/unit/Async/ParallelTest.php
@@ -8,6 +8,7 @@
use PHPUnit\Util\Exception;
use Psl;
use Psl\Async;
+use Psl\DateTime;
final class ParallelTest extends TestCase
{
@@ -17,17 +18,17 @@ public function testParallel(): void
Async\concurrently([
static function () use ($spy): void {
- Async\sleep(0.03);
+ Async\sleep(DateTime\Duration::milliseconds(30));
$spy->value .= '1';
},
static function () use ($spy): void {
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
$spy->value .= '2';
},
static function () use ($spy): void {
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
$spy->value .= '3';
},
@@ -45,12 +46,12 @@ public function testParallelThrowsForTheFirstAndDoesNotCallTheRest(): void
try {
Async\concurrently([
static function (): void {
- Async\sleep(0.003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
throw new Exception('foo');
},
static function () use ($spy): void {
- Async\sleep(0.004);
+ Async\sleep(DateTime\Duration::milliseconds(4));
$spy->value = 'thrown';
diff --git a/tests/unit/Async/ReflectTest.php b/tests/unit/Async/ReflectTest.php
index 94e9f979..37609c84 100644
--- a/tests/unit/Async/ReflectTest.php
+++ b/tests/unit/Async/ReflectTest.php
@@ -7,6 +7,7 @@
use Exception;
use PHPUnit\Framework\TestCase;
use Psl\Async;
+use Psl\DateTime;
use Psl\Result;
final class ReflectTest extends TestCase
@@ -15,7 +16,7 @@ public function testReflectParallel(): void
{
[$one, $two] = Async\concurrently([
Result\reflect(static function (): void {
- Async\sleep(0.0001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
throw new Exception('failure');
}),
diff --git a/tests/unit/Async/RunTest.php b/tests/unit/Async/RunTest.php
index 671c3d3d..8fdba0da 100644
--- a/tests/unit/Async/RunTest.php
+++ b/tests/unit/Async/RunTest.php
@@ -6,6 +6,7 @@
use PHPUnit\Framework\TestCase;
use Psl\Async;
+use Psl\DateTime;
final class RunTest extends TestCase
{
@@ -13,9 +14,9 @@ public function testRun(): void
{
$awaitable = Async\run(static function (): string {
Async\concurrently([
- static fn() => Async\sleep(0.001),
- static fn() => Async\sleep(0.001),
- static fn() => Async\sleep(0.001),
+ static fn() => Async\sleep(DateTime\Duration::milliseconds(1)),
+ static fn() => Async\sleep(DateTime\Duration::milliseconds(1)),
+ static fn() => Async\sleep(DateTime\Duration::milliseconds(1)),
]);
return 'hello';
diff --git a/tests/unit/Async/SemaphoreTest.php b/tests/unit/Async/SemaphoreTest.php
index c8168196..5b7443d8 100644
--- a/tests/unit/Async/SemaphoreTest.php
+++ b/tests/unit/Async/SemaphoreTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use Psl;
use Psl\Async;
+use Psl\DateTime;
final class SemaphoreTest extends TestCase
{
@@ -24,7 +25,7 @@ public function testSequenceOperationWaitsForPendingOperationsWhenLimitIsNotReac
$spy = new Psl\Ref([]);
/**
- * @var Async\Semaphore
+ * @var Async\Semaphore
*/
$semaphore = new Async\Semaphore(1, static function (array $data) use ($spy): void {
if ($data['time'] !== null) {
@@ -34,9 +35,9 @@ public function testSequenceOperationWaitsForPendingOperationsWhenLimitIsNotReac
$spy->value[] = $data['value'];
});
- Async\run(static fn() => $semaphore->waitFor(['time' => 0.003, 'value' => 'a']));
- Async\run(static fn() => $semaphore->waitFor(['time' => 0.004, 'value' => 'b']));
- Async\run(static fn() => $semaphore->waitFor(['time' => 0.005, 'value' => 'c']));
+ Async\run(static fn() => $semaphore->waitFor(['time' => DateTime\Duration::milliseconds(3), 'value' => 'a']));
+ Async\run(static fn() => $semaphore->waitFor(['time' => DateTime\Duration::milliseconds(4), 'value' => 'b']));
+ Async\run(static fn() => $semaphore->waitFor(['time' => DateTime\Duration::milliseconds(5), 'value' => 'c']));
$last = Async\run(static fn() => $semaphore->waitFor(['time' => null, 'value' => 'd']));
$last->await();
@@ -48,7 +49,7 @@ public function testOperationWaitsForPendingOperationsWhenLimitIsNotReached(): v
$spy = new Psl\Ref([]);
/**
- * @var Async\Semaphore
+ * @var Async\Semaphore
*/
$semaphore = new Async\Semaphore(2, static function (array $data) use ($spy): void {
if ($data['time'] !== null) {
@@ -58,9 +59,9 @@ public function testOperationWaitsForPendingOperationsWhenLimitIsNotReached(): v
$spy->value[] = $data['value'];
});
- Async\run(static fn() => $semaphore->waitFor(['time' => 0.003, 'value' => 'a']));
- Async\run(static fn() => $semaphore->waitFor(['time' => 0.004, 'value' => 'b']));
- $beforeLast = Async\run(static fn() => $semaphore->waitFor(['time' => 0.005, 'value' => 'c']));
+ Async\run(static fn() => $semaphore->waitFor(['time' => Datetime\Duration::milliseconds(3), 'value' => 'a']));
+ Async\run(static fn() => $semaphore->waitFor(['time' => Datetime\Duration::milliseconds(4), 'value' => 'b']));
+ $beforeLast = Async\run(static fn() => $semaphore->waitFor(['time' => Datetime\Duration::milliseconds(5), 'value' => 'c']));
Async\run(static fn() => $semaphore->waitFor(['time' => null, 'value' => 'd']));
$beforeLast->await();
@@ -78,12 +79,12 @@ public function testOperationIsStartedIfLimitIsNotReached(): void
$semaphore = new Async\Semaphore(1, static function (string $input) use ($spy): void {
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(Datetime\Duration::milliseconds(2));
});
$awaitable = Async\run(static fn() => $semaphore->waitFor('hello'));
- Async\sleep(0.001);
+ Async\sleep(Datetime\Duration::milliseconds(1));
static::assertSame(['hello'], $spy->value);
@@ -100,13 +101,13 @@ public function testOperationIsNotStartedIfLimitIsReached(): void
$semaphore = new Async\Semaphore(1, static function (string $input) use ($spy): void {
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(Datetime\Duration::milliseconds(2));
});
Async\run(static fn() => $semaphore->waitFor('hello'));
$awaitable = Async\run(static fn() => $semaphore->waitFor('world'));
- Async\sleep(0.001);
+ Async\sleep(Datetime\Duration::milliseconds(1));
static::assertNotContains('world', $spy->value);
@@ -133,7 +134,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
* @var Async\Semaphore
*/
$semaphore = new Async\Semaphore(1, static function (string $input): string {
- Async\sleep(0.04);
+ Async\sleep(Datetime\Duration::milliseconds(40));
return $input;
});
@@ -143,7 +144,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
$one = Async\run(static fn() => $semaphore->waitFor('one'));
$two = Async\run(static fn() => $semaphore->waitFor('two'));
- Async\sleep(0.01);
+ Async\sleep(Datetime\Duration::milliseconds(10));
$semaphore->cancel(new Async\Exception\TimeoutException('The semaphore is destroyed.'));
@@ -161,7 +162,7 @@ public function testSemaphoreStatus(): void
* @var Async\Semaphore
*/
$semaphore = new Async\Semaphore(1, static function (string $input): string {
- Async\sleep(0.04);
+ Async\sleep(Datetime\Duration::milliseconds(40));
return $input;
});
@@ -195,7 +196,7 @@ public function testWaitForPending(): void
* @var Async\Semaphore
*/
$semaphore = new Async\Semaphore(1, static function (string $input): string {
- Async\sleep(0.04);
+ Async\sleep(Datetime\Duration::milliseconds(40));
return $input;
});
diff --git a/tests/unit/Async/SequenceTest.php b/tests/unit/Async/SequenceTest.php
index c10c6803..a3fb9dc5 100644
--- a/tests/unit/Async/SequenceTest.php
+++ b/tests/unit/Async/SequenceTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use Psl;
use Psl\Async;
+use Psl\DateTime;
use Psl\Str;
use function microtime;
@@ -37,9 +38,9 @@ public function testSequenceOperationWaitsForPendingOperationsWhenLimitIsNotReac
$spy->value[] = $data['value'];
});
- Async\run(static fn() => $sequence->waitFor(['time' => 0.003, 'value' => 'a']));
- Async\run(static fn() => $sequence->waitFor(['time' => 0.004, 'value' => 'b']));
- Async\run(static fn() => $sequence->waitFor(['time' => 0.005, 'value' => 'c']));
+ Async\run(static fn() => $sequence->waitFor(['time' => DateTime\Duration::milliseconds(3), 'value' => 'a']));
+ Async\run(static fn() => $sequence->waitFor(['time' => DateTime\Duration::milliseconds(4), 'value' => 'b']));
+ Async\run(static fn() => $sequence->waitFor(['time' => DateTime\Duration::milliseconds(5), 'value' => 'c']));
$last = Async\run(static fn() => $sequence->waitFor(['time' => null, 'value' => 'd']));
$last->await();
@@ -56,12 +57,12 @@ public function testOperationIsStartedIfLimitIsNotReached(): void
$sequence = new Async\Sequence(static function (string $input) use ($spy): void {
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
});
$awaitable = Async\run(static fn() => $sequence->waitFor('hello'));
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
static::assertSame(['hello'], $spy->value);
@@ -78,13 +79,13 @@ public function testOperationIsNotStartedIfLimitIsReached(): void
$sequence = new Async\Sequence(static function (string $input) use ($spy): void {
$spy->value[] = $input;
- Async\sleep(0.002);
+ Async\sleep(DateTime\Duration::milliseconds(2));
});
Async\run(static fn() => $sequence->waitFor('hello'));
$awaitable = Async\run(static fn() => $sequence->waitFor('world'));
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
static::assertNotContains('world', $spy->value);
@@ -111,7 +112,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
* @var Async\Sequence
*/
$sequence = new Async\Sequence(static function (string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -119,7 +120,7 @@ public function testCancelPendingOperationsButNotTheOngoingOne(): void
$one = Async\run(static fn() => $sequence->waitFor('one'));
$two = Async\run(static fn() => $sequence->waitFor('two'));
- Async\sleep(0.01);
+ Async\sleep(DateTime\Duration::milliseconds(10));
$sequence->cancel(new Async\Exception\TimeoutException('The semaphore is destroyed.'));
@@ -138,8 +139,8 @@ public function testBug327(): void
{
$ref = new Psl\Ref('');
- $sequence = new Async\Sequence(static function (float $value) use ($ref): void {
- $ref->value .= Str\format('%f', $value);
+ $sequence = new Async\Sequence(static function (DateTime\Duration $value) use ($ref): void {
+ $ref->value .= Str\format('%f', $value->getTotalSeconds());
Async\sleep($value);
});
@@ -148,10 +149,10 @@ public function testBug327(): void
Async\concurrently([
static function () use ($sequence): void {
- $sequence->waitFor(0.02);
- $sequence->waitFor(0.02);
+ $sequence->waitFor(DateTime\Duration::milliseconds(20));
+ $sequence->waitFor(DateTime\Duration::milliseconds(20));
},
- static fn() => $sequence->waitFor(0.02),
+ static fn() => $sequence->waitFor(DateTime\Duration::milliseconds(20)),
]);
$duration = microtime(true) - $time;
@@ -166,7 +167,7 @@ public function testStatus(): void
* @var Async\Sequence
*/
$s = new Async\Sequence(static function (string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
@@ -196,7 +197,7 @@ public function testWaitForPending(): void
* @var Async\Sequence
*/
$s = new Async\Sequence(static function (string $input): string {
- Async\sleep(0.04);
+ Async\sleep(DateTime\Duration::milliseconds(40));
return $input;
});
diff --git a/tests/unit/Async/SeriesTest.php b/tests/unit/Async/SeriesTest.php
index 27049e27..30325893 100644
--- a/tests/unit/Async/SeriesTest.php
+++ b/tests/unit/Async/SeriesTest.php
@@ -8,6 +8,7 @@
use PHPUnit\Util\Exception;
use Psl;
use Psl\Async;
+use Psl\DateTime;
final class SeriesTest extends TestCase
{
@@ -17,17 +18,17 @@ public function testSeries(): void
Async\series([
static function () use ($spy): void {
- Async\sleep(0.003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
$spy->value .= '1';
},
static function () use ($spy): void {
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
$spy->value .= '2';
},
static function () use ($spy): void {
- Async\sleep(0.001);
+ Async\sleep(DateTime\Duration::milliseconds(1));
$spy->value .= '3';
},
@@ -45,7 +46,7 @@ public function testSeriesThrowsForTheFirstAndDoesNotCallTheRest(): void
try {
Async\series([
static function (): void {
- Async\sleep(0.003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
throw new Exception('foo');
},
diff --git a/tests/unit/Channel/BoundedChannelTest.php b/tests/unit/Channel/BoundedChannelTest.php
index 20dab41e..5242e9f5 100644
--- a/tests/unit/Channel/BoundedChannelTest.php
+++ b/tests/unit/Channel/BoundedChannelTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use Psl\Async;
use Psl\Channel;
+use Psl\DateTime;
final class BoundedChannelTest extends TestCase
{
@@ -109,10 +110,9 @@ public function testIsFull(): void
public function testTrySendThrowsOnFullChannel(): void
{
/**
- * @var Channel\ReceiverInterface $receiver
* @var Channel\SenderInterface $sender
*/
- [$receiver, $sender] = Channel\bounded(1);
+ [$_, $sender] = Channel\bounded(1);
$sender->send('hello');
@@ -131,7 +131,7 @@ public function testSendWaitsForFullChannel(): void
$sender->send('hello');
- Async\Scheduler::delay(0.001, static function () use ($receiver) {
+ Async\Scheduler::delay(DateTime\Duration::milliseconds(1), static function () use ($receiver) {
$receiver->receive();
});
@@ -166,7 +166,7 @@ public function testSendThrowsForLateClosedChannel(): void
$sender->send('hello');
- Async\Scheduler::delay(0.001, static function () use ($receiver): void {
+ Async\Scheduler::delay(DateTime\Duration::milliseconds(1), static function () use ($receiver): void {
$receiver->close();
});
@@ -233,7 +233,7 @@ public function testReceiveThrowsForLateClosedChannel(): void
*/
[$receiver, $sender] = Channel\bounded(1);
- Async\Scheduler::delay(0.0001, static function () use ($sender): void {
+ Async\Scheduler::delay(DateTime\Duration::milliseconds(1), static function () use ($sender): void {
$sender->close();
});
@@ -267,9 +267,10 @@ public function testReceiveWaitsWhenChannelIsEmpty(): void
*/
[$receiver, $sender] = Channel\bounded(1);
- Async\Scheduler::delay(0.001, static function () use ($sender) {
- $sender->send('hello');
- });
+ Async\Scheduler::delay(
+ DateTime\Duration::milliseconds(1),
+ static fn() => $sender->send('hello'),
+ );
static::assertTrue($receiver->isEmpty());
@@ -280,9 +281,8 @@ public function testTryReceiveThrowsForEmptyChannel(): void
{
/**
* @var Channel\ReceiverInterface $receiver
- * @var Channel\SenderInterface $sender
*/
- [$receiver, $sender] = Channel\bounded(1);
+ [$receiver, $_] = Channel\bounded(1);
$this->expectException(Channel\Exception\EmptyChannelException::class);
$this->expectExceptionMessage('Attempted to receiver from an empty channel.');
diff --git a/tests/unit/DateTime/DateTimeTest.php b/tests/unit/DateTime/DateTimeTest.php
new file mode 100644
index 00000000..5bc1f399
--- /dev/null
+++ b/tests/unit/DateTime/DateTimeTest.php
@@ -0,0 +1,61 @@
+getTimestamp();
+
+ static::assertEqualsWithDelta(time(), $timestamp->getSeconds(), 1);
+ }
+
+ public function testTodayAt(): void
+ {
+ $now = DateTime::now();
+ $today = DateTime::todayAt(14, 00, 00);
+
+ static::assertSame($now->getDate(), $today->getDate());
+ static::assertNotSame($now->getTime(), $today->getTime());
+ static::assertSame(14, $today->getHours());
+ static::assertSame(0, $today->getMinutes());
+ static::assertSame(0, $today->getSeconds());
+ }
+
+ public function testFromParts(): void
+ {
+ $datetime = DateTime::fromParts(Timezone::UTC, 2024, Month::February, 4, 14, 0, 0, 1);
+
+ static::assertSame(Timezone::UTC, $datetime->getTimezone());
+ static::assertSame(2024, $datetime->getYear());
+ static::assertSame(2, $datetime->getMonth());
+ static::assertSame(4, $datetime->getDay());
+ static::assertSame(Weekday::Sunday, $datetime->getWeekday());
+ static::assertSame(14, $datetime->getHours());
+ static::assertSame(0, $datetime->getMinutes());
+ static::assertSame(0, $datetime->getSeconds());
+ static::assertSame(1, $datetime->getNanoseconds());
+ }
+
+ public function testFromPartsWithInvalidComponent(): void
+ {
+ $this->expectException(UnexpectedValueException::class);
+ $this->expectExceptionMessage('Unexpected hours value encountered. Provided "999", but the calendar expects "15". Ensure the hour falls within a 24-hour day.');
+
+ DateTime::fromParts(Timezone::UTC, 2024, Month::February, 4, 999, 0, 0, 1);
+ }
+}
diff --git a/tests/unit/DateTime/DateTimeTestTrait.php b/tests/unit/DateTime/DateTimeTestTrait.php
new file mode 100644
index 00000000..20a6febd
--- /dev/null
+++ b/tests/unit/DateTime/DateTimeTestTrait.php
@@ -0,0 +1,26 @@
+timezone = date_default_timezone_get();
+ $this->locale = locale_get_default();
+
+ date_default_timezone_set('Europe/London');
+ locale_set_default('en_GB');
+ }
+
+ protected function tearDown(): void
+ {
+ date_default_timezone_set($this->timezone);
+ locale_set_default($this->locale);
+ }
+}
diff --git a/tests/unit/DateTime/DurationTest.php b/tests/unit/DateTime/DurationTest.php
new file mode 100644
index 00000000..0e76acc5
--- /dev/null
+++ b/tests/unit/DateTime/DurationTest.php
@@ -0,0 +1,353 @@
+getHours());
+ static::assertEquals(2, $t->getMinutes());
+ static::assertEquals(3, $t->getSeconds());
+ static::assertEquals(4, $t->getNanoseconds());
+ static::assertEquals([1, 2, 3, 4], $t->getParts());
+ }
+
+ public function provideGetTotalHours(): array
+ {
+ return [
+ [0, 0, 0, 0, 0.0],
+ [0, 0, 0, 1, 2.777777777777778E-13],
+ [1, 0, 0, 0, 1.0],
+ [1, 30, 0, 0, 1.5],
+ [2, 15, 30, 0, 2.2583333333333333],
+ [-1, 0, 0, 0, -1.0],
+ [-1, -30, 0, 0, -1.5],
+ [-2, -15, -30, 0, -2.2583333333333333],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetTotalHours
+ */
+ public function testGetTotalHours(int $hours, int $minutes, int $seconds, int $nanoseconds, float $expectedHours): void
+ {
+ $time = DateTime\Duration::fromParts($hours, $minutes, $seconds, $nanoseconds);
+ static::assertEquals($expectedHours, $time->getTotalHours());
+ }
+
+ public function provideGetTotalMinutes(): array
+ {
+ return [
+ [0, 0, 0, 0, 0.0],
+ [0, 0, 0, 1, 1.6666666666666667E-11],
+ [1, 0, 0, 0, 60.0],
+ [1, 30, 0, 0, 90.0],
+ [2, 15, 30, 0, 135.5],
+ [-1, 0, 0, 0, -60.0],
+ [-1, -30, 0, 0, -90.0],
+ [-2, -15, -30, 0, -135.5],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetTotalMinutes
+ */
+ public function testGetTotalMinutes(int $hours, int $minutes, int $seconds, int $nanoseconds, float $expectedMinutes): void
+ {
+ $time = DateTime\Duration::fromParts($hours, $minutes, $seconds, $nanoseconds);
+ static::assertEquals($expectedMinutes, $time->getTotalMinutes());
+ }
+
+ public function provideGetTotalSeconds(): array
+ {
+ return [
+ [0, 0, 0, 0, 0.0],
+ [0, 0, 0, 1, 0.000000001],
+ [1, 0, 0, 0, 3600.0],
+ [1, 30, 0, 0, 5400.0],
+ [2, 15, 30, 0, 8130.0],
+ [-1, 0, 0, 0, -3600.0],
+ [-1, -30, 0, 0, -5400.0],
+ [-2, -15, -30, 0, -8130.0],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetTotalSeconds
+ */
+ public function testGetTotalSeconds(int $hours, int $minutes, int $seconds, int $nanoseconds, float $expectedSeconds): void
+ {
+ $time = DateTime\Duration::fromParts($hours, $minutes, $seconds, $nanoseconds);
+ static::assertEquals($expectedSeconds, $time->getTotalSeconds());
+ }
+
+ public function provideGetTotalMilliseconds(): array
+ {
+ return [
+ [0, 0, 0, 0, 0.0],
+ [0, 0, 0, 1, 0.000001],
+ [1, 0, 0, 0, 3600000.0],
+ [1, 30, 0, 0, 5400000.0],
+ [2, 15, 30, 0, 8130000.0],
+ [-1, 0, 0, 0, -3600000.0],
+ [-1, -30, 0, 0, -5400000.0],
+ [-2, -15, -30, 0, -8130000.0],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetTotalMilliseconds
+ */
+ public function testGetTotalMilliseconds(int $hours, int $minutes, int $seconds, int $nanoseconds, float $expectedMilliseconds): void
+ {
+ $time = DateTime\Duration::fromParts($hours, $minutes, $seconds, $nanoseconds);
+ static::assertEquals($expectedMilliseconds, $time->getTotalMilliseconds());
+ }
+
+ public function provideGetTotalMicroseconds(): array
+ {
+ return [
+ [0, 0, 0, 0, 0.0],
+ [0, 0, 0, 1, 0.001],
+ [1, 0, 0, 0, 3600000000.0],
+ [1, 30, 0, 0, 5400000000.0],
+ [2, 15, 30, 0, 8130000000.0],
+ [-1, 0, 0, 0, -3600000000.0],
+ [-1, -30, 0, 0, -5400000000.0],
+ [-2, -15, -30, 0, -8130000000.0],
+ ];
+ }
+
+ /**
+ * @dataProvider provideGetTotalMicroseconds
+ */
+ public function testGetTotalMicroseconds(int $hours, int $minutes, int $seconds, int $nanoseconds, float $expectedMicroseconds): void
+ {
+ $time = DateTime\Duration::fromParts($hours, $minutes, $seconds, $nanoseconds);
+ static::assertEquals($expectedMicroseconds, $time->getTotalMicroseconds());
+ }
+
+ public function testSetters(): void
+ {
+ $t = DateTime\Duration::fromParts(1, 2, 3, 4);
+
+ static::assertEquals([42, 2, 3, 4], $t->withHours(42)->getParts());
+ static::assertEquals([1, 42, 3, 4], $t->withMinutes(42)->getParts());
+ static::assertEquals([1, 2, 42, 4], $t->withSeconds(42)->getParts());
+ static::assertEquals([1, 2, 3, 42], $t->withNanoseconds(42)->getParts());
+ static::assertEquals([2, 3, 3, 4], $t->withMinutes(63)->getParts());
+ static::assertEquals([1, 3, 3, 4], $t->withSeconds(63)->getParts());
+ static::assertEquals([1, 2, 4, 42], $t->withNanoseconds(DateTime\NANOSECONDS_PER_SECOND + 42)->getParts());
+ static::assertEquals([1, 2, 3, 4], $t->getParts());
+ }
+ public function testFractionsOfSecond(): void
+ {
+ static::assertEquals([0, 0, 0, 0], DateTime\Duration::zero()->getParts());
+ static::assertEquals([0, 0, 0, 42], DateTime\Duration::nanoseconds(42)->getParts());
+ static::assertEquals([0, 0, 1, 42], DateTime\Duration::nanoseconds(DateTime\NANOSECONDS_PER_SECOND + 42)->getParts());
+ static::assertEquals([0, 0, 0, 42000], DateTime\Duration::microseconds(42)->getParts());
+ static::assertEquals([0, 0, 1, 42000], DateTime\Duration::microseconds(1000042)->getParts());
+ static::assertEquals([0, 0, 0, 42000000], DateTime\Duration::milliseconds(42)->getParts());
+ static::assertEquals([0, 0, 1, 42000000], DateTime\Duration::milliseconds(1042)->getParts());
+ }
+
+ /**
+ * @return list
+ */
+ public static function provideNormalized(): array
+ {
+ return [
+ // input seconds, input ns, normalized seconds, normalized ns
+ [0, 0, 0, 0],
+ [0, 3, 0, 3],
+ [3, 0, 3, 0],
+ [1, 3, 1, 3],
+ [1, -3, 0, DateTime\NANOSECONDS_PER_SECOND - 3],
+ [-1, 3, 0, -(DateTime\NANOSECONDS_PER_SECOND - 3)],
+ [-1, -3, -1, -3],
+ [1, DateTime\NANOSECONDS_PER_SECOND + 42, 2, 42],
+ [1, -(DateTime\NANOSECONDS_PER_SECOND + 42), 0, -42],
+ [2, -3, 1, DateTime\NANOSECONDS_PER_SECOND - 3],
+ ];
+ }
+ /**
+ * @dataProvider provideNormalized
+ */
+ public function testNormalized(int $input_s, int $input_ns, int $normalized_s, int $normalized_ns): void
+ {
+ static::assertEquals(
+ [0, 0, $normalized_s, $normalized_ns],
+ DateTime\Duration::fromParts(0, 0, $input_s, $input_ns)->getParts()
+ );
+ }
+
+ public function testNormalizedHMS(): void
+ {
+ static::assertEquals([3, 5, 4, 0], DateTime\Duration::fromParts(2, 63, 124)->getParts());
+ static::assertEquals([0, 59, 4, 0], DateTime\Duration::fromParts(2, -63, 124)->getParts());
+ static::assertEquals([-1, 0, -55, -(DateTime\NANOSECONDS_PER_SECOND - 42)], DateTime\Duration::fromParts(0, -63, 124, 42)->getParts());
+ static::assertEquals([42, 0, 0, 0], DateTime\Duration::hours(42)->getParts());
+ static::assertEquals([1, 3, 0, 0], DateTime\Duration::minutes(63)->getParts());
+ static::assertEquals([0, -1, -3, 0], DateTime\Duration::seconds(-63)->getParts());
+ static::assertEquals([0, 0, -1, 0], DateTime\Duration::nanoseconds(-DateTime\NANOSECONDS_PER_SECOND)->getParts());
+ }
+
+ /**
+ * @return list
+ */
+ public static function providePositiveNegative(): array
+ {
+ return [
+ // h, m, s, ns, expected sign
+ [0, 0, 0, 0, 0],
+ [0, 42, 0, 0, 1],
+ [0, 0, -42, 0, -1],
+ [1, -63, 0, 0, -1],
+ ];
+ }
+ /**
+ * @dataProvider providePositiveNegative
+ */
+ public function testPositiveNegative(int $h, int $m, int $s, int $ns, int $expected_sign): void
+ {
+ $t = DateTime\Duration::fromParts($h, $m, $s, $ns);
+ static::assertEquals($expected_sign === 0, $t->isZero());
+ static::assertEquals($expected_sign === 1, $t->isPositive());
+ static::assertEquals($expected_sign === -1, $t->isNegative());
+ }
+
+ /**
+ * @return list
+ */
+ public static function provideCompare(): array
+ {
+ return [
+ [DateTime\Duration::hours(1), DateTime\Duration::minutes(42), Order::Greater],
+ [DateTime\Duration::minutes(2), DateTime\Duration::seconds(120), Order::Equal],
+ [DateTime\Duration::zero(), DateTime\Duration::nanoseconds(1), Order::Less],
+ ];
+ }
+ /**
+ * @dataProvider provideCompare
+ */
+ public function testCompare(DateTime\Duration $a, DateTime\Duration $b, Order $expected): void
+ {
+ $opposite = Order::from(-$expected->value);
+
+ static::assertEquals($expected, $a->compare($b));
+ static::assertEquals($opposite, $b->compare($a));
+ static::assertEquals($expected === Order::Equal, $a->equals($b));
+ static::assertEquals($expected === Order::Less, $a->shorter($b));
+ static::assertEquals($expected !== Order::Greater, $a->shorterOrEqual($b));
+ static::assertEquals($expected === Order::Greater, $a->longer($b));
+ static::assertEquals($expected !== Order::Less, $a->longerOrEqual($b));
+ static::assertFalse($a->betweenExclusive($a, $a));
+ static::assertFalse($a->betweenExclusive($a, $b));
+ static::assertFalse($a->betweenExclusive($b, $a));
+ static::assertFalse($a->betweenExclusive($b, $b));
+ static::assertTrue($a->betweenInclusive($a, $a));
+ static::assertTrue($a->betweenInclusive($a, $b));
+ static::assertTrue($a->betweenInclusive($b, $a));
+ static::assertEquals($expected === Order::Equal, $a->betweenInclusive($b, $b));
+ }
+
+ public function testIsBetween(): void
+ {
+ $a = DateTime\Duration::hours(1);
+ $b = DateTime\Duration::minutes(64);
+ $c = DateTime\Duration::fromParts(1, 30);
+ static::assertTrue($b->betweenExclusive($a, $c));
+ static::assertTrue($b->betweenExclusive($c, $a));
+ static::assertTrue($b->betweenInclusive($a, $c));
+ static::assertTrue($b->betweenInclusive($c, $a));
+ static::assertFalse($a->betweenExclusive($b, $c));
+ static::assertFalse($a->betweenInclusive($c, $b));
+ static::assertFalse($c->betweenInclusive($a, $b));
+ static::assertFalse($c->betweenExclusive($b, $a));
+ }
+
+ public function testOperations(): void
+ {
+ $z = DateTime\Duration::zero();
+ $a = DateTime\Duration::fromParts(0, 2, 25);
+ $b = DateTime\Duration::fromParts(0, 0, -63, 42);
+ static::assertEquals([0, 0, 0, 0], $z->invert()->getParts());
+ static::assertEquals([0, -2, -25, 0], $a->invert()->getParts());
+ static::assertEquals([0, 1, 2, DateTime\NANOSECONDS_PER_SECOND - 42], $b->invert()->getParts());
+ static::assertEquals($a->getParts(), $z->plus($a)->getParts());
+ static::assertEquals($b->getParts(), $b->plus($z)->getParts());
+ static::assertEquals($b->invert()->getParts(), $z->minus($b)->getParts());
+ static::assertEquals($a->getParts(), $a->minus($z)->getParts());
+ static::assertEquals([0, 1, 22, 42], $a->plus($b)->getParts());
+ static::assertEquals([0, 1, 22, 42], $b->plus($a)->getParts());
+ static::assertEquals([0, 3, 27, DateTime\NANOSECONDS_PER_SECOND - 42], $a->minus($b)->getParts());
+ static::assertEquals([0, -3, -27, -(DateTime\NANOSECONDS_PER_SECOND - 42)], $b->minus($a)->getParts());
+ static::assertEquals($b->invert()->plus($a)->getParts(), $a->minus($b)->getParts());
+ }
+
+ /**
+ * @return list
+ */
+ public static function provideToString(): array
+ {
+ return [
+ // h, m, s, ns, expected output
+ [42, 0, 0, 0, '42 hour(s)'],
+ [0, 42, 0, 0, '42 minute(s)'],
+ [0, 0, 42, 0, '42 second(s)'],
+ [0, 0, 0, 0, '0 second(s)'],
+ [0, 0, 0, 42, '0 second(s)'], // rounded because default $max_decimals = 3
+ [0, 0, 1, 42, '1 second(s)'],
+ [0, 0, 1, 20000000, '1.02 second(s)'],
+ [1, 2, 0, 0, '1 hour(s), 2 minute(s)'],
+ [1, 0, 3, 0, '1 hour(s), 0 minute(s), 3 second(s)'],
+ [0, 2, 3, 0, '2 minute(s), 3 second(s)'],
+ [1, 2, 3, 0, '1 hour(s), 2 minute(s), 3 second(s)'],
+ [1, 0, 0, 42000000, '1 hour(s), 0 minute(s), 0.042 second(s)'],
+ [-42, 0, -42, 0, '-42 hour(s), 0 minute(s), -42 second(s)'],
+ [-42, 0, -42, -420000000, '-42 hour(s), 0 minute(s), -42.42 second(s)'],
+ [0, 0, 0, -420000000, '-0.42 second(s)'],
+ ];
+ }
+
+ /**
+ * @dataProvider provideToString
+ */
+ public function testToString(int $h, int $m, int $s, int $ns, string $expected): void
+ {
+ static::assertEquals($expected, DateTime\Duration::fromParts($h, $m, $s, $ns)->toString());
+ }
+
+ public function testSerialization(): void
+ {
+ $timeInterval = DateTime\Duration::fromParts(1, 30, 45, 500000000);
+ $serialized = serialize($timeInterval);
+ $deserialized = unserialize($serialized);
+
+ static::assertEquals($timeInterval, $deserialized);
+ }
+
+ public function testJsonEncoding(): void
+ {
+ $timeInterval = DateTime\Duration::fromParts(1, 30, 45, 500000000);
+ $jsonEncoded = Json\encode($timeInterval);
+ $jsonDecoded = Json\decode($jsonEncoded);
+
+ static::assertSame(['hours' => 1, 'minutes' => 30, 'seconds' => 45, 'nanoseconds' => 500000000], $jsonDecoded);
+ }
+}
diff --git a/tests/unit/DateTime/EraTest.php b/tests/unit/DateTime/EraTest.php
new file mode 100644
index 00000000..aadea030
--- /dev/null
+++ b/tests/unit/DateTime/EraTest.php
@@ -0,0 +1,36 @@
+toggle());
+ static::assertSame(Era::BeforeChrist, Era::AnnoDomini->toggle());
+ }
+}
diff --git a/tests/unit/DateTime/FormatPatternTest.php b/tests/unit/DateTime/FormatPatternTest.php
new file mode 100644
index 00000000..53213fc6
--- /dev/null
+++ b/tests/unit/DateTime/FormatPatternTest.php
@@ -0,0 +1,37 @@
+toggle());
+ static::assertSame(Meridiem::AnteMeridiem, Meridiem::PostMeridiem->toggle());
+ }
+}
diff --git a/tests/unit/DateTime/IsLeapYearTest.php b/tests/unit/DateTime/IsLeapYearTest.php
new file mode 100644
index 00000000..6dac23b6
--- /dev/null
+++ b/tests/unit/DateTime/IsLeapYearTest.php
@@ -0,0 +1,35 @@
+toggle());
+ static::assertSame(Meridiem::AnteMeridiem, Meridiem::PostMeridiem->toggle());
+ }
+}
diff --git a/tests/unit/DateTime/MonthTest.php b/tests/unit/DateTime/MonthTest.php
new file mode 100644
index 00000000..aadea030
--- /dev/null
+++ b/tests/unit/DateTime/MonthTest.php
@@ -0,0 +1,36 @@
+toggle());
+ static::assertSame(Era::BeforeChrist, Era::AnnoDomini->toggle());
+ }
+}
diff --git a/tests/unit/DateTime/TimestampTest.php b/tests/unit/DateTime/TimestampTest.php
new file mode 100644
index 00000000..716cedeb
--- /dev/null
+++ b/tests/unit/DateTime/TimestampTest.php
@@ -0,0 +1,406 @@
+getSeconds(), 1);
+ }
+
+ public function testMonotonic(): void
+ {
+ $timestamp = Timestamp::monotonic();
+
+ static::assertEqualsWithDelta(time(), $timestamp->getSeconds(), 1);
+ }
+
+ public function testMonotonicIsPrecise(): void
+ {
+ $a = Timestamp::monotonic();
+
+ Async\sleep(Duration::milliseconds(100));
+
+ $b = Timestamp::monotonic();
+
+ $difference = $b->since($a);
+
+ static::assertGreaterThan(100.0, $difference->getTotalMilliseconds());
+ }
+
+ public function testFromRowOverflow(): void
+ {
+ $this->expectException(OverflowException::class);
+ $this->expectExceptionMessage('Adding nanoseconds would cause an overflow.');
+
+ Timestamp::fromParts(Math\INT64_MAX, NANOSECONDS_PER_SECOND);
+ }
+
+ public function testFromRowUnderflow(): void
+ {
+ $this->expectException(UnderflowException::class);
+ $this->expectExceptionMessage('Subtracting nanoseconds would cause an underflow.');
+
+ Timestamp::fromParts(Math\INT64_MIN, -NANOSECONDS_PER_SECOND);
+ }
+
+ public function testFromRowSimplifiesNanoseconds(): void
+ {
+ $timestamp = Timestamp::fromParts(0, NANOSECONDS_PER_SECOND * 20);
+
+ static::assertEquals(20, $timestamp->getSeconds());
+ static::assertEquals(0, $timestamp->getNanoseconds());
+
+ $timestamp = Timestamp::fromParts(0, 100 + NANOSECONDS_PER_SECOND * 20);
+
+ static::assertEquals(20, $timestamp->getSeconds());
+ static::assertEquals(100, $timestamp->getNanoseconds());
+
+ $timestamp = Timestamp::fromParts(30, -NANOSECONDS_PER_SECOND * 20);
+
+ static::assertEquals(10, $timestamp->getSeconds());
+ static::assertEquals(0, $timestamp->getNanoseconds());
+
+ $timestamp = Timestamp::fromParts(10, 100 + -NANOSECONDS_PER_SECOND * 20);
+
+ static::assertEquals(-10, $timestamp->getSeconds());
+ static::assertEquals(100, $timestamp->getNanoseconds());
+ }
+
+ public function testParsingFromPattern(): void
+ {
+ $timestamp = Timestamp::parse(
+ raw_string: '2024 091',
+ pattern: FormatPattern::JulianDay,
+ );
+
+ $datetime = DateTime::fromTimestamp($timestamp, Timezone::UTC);
+
+ static::assertSame(2024, $datetime->getYear());
+ static::assertSame(3, $datetime->getMonth());
+ static::assertSame(31, $datetime->getDay());
+ }
+
+ public function testFromPatternFails(): void
+ {
+ $this->expectException(ParserException::class);
+ $this->expectExceptionMessage('Unable to interpret \'2\' as a valid date/time using pattern \'yyyy DDD\'.');
+
+ Timestamp::parse('2', pattern: FormatPattern::JulianDay);
+ }
+
+ public function testParseFormat(): void
+ {
+ $a = Timestamp::now();
+ $string = $a->format();
+
+ $b = Timestamp::parse($string);
+
+ static::assertSame($a->getSeconds(), $b->getSeconds());
+ }
+
+ public function testFromStringToString(): void
+ {
+ $a = Timestamp::now();
+ $string = $a->toString();
+
+ $b = Timestamp::fromString($string);
+
+ static::assertSame($a->getSeconds(), $b->getSeconds());
+ }
+
+ public function testParseFails(): void
+ {
+ $this->expectException(ParserException::class);
+ $this->expectExceptionMessage('Unable to interpret \'x\' as a valid date/time.');
+
+ Timestamp::parse('x');
+ }
+
+ public function provideFormatParsingData(): iterable
+ {
+ yield [1711917897, FormatPattern::FullDateTime, Timezone::UTC, Locale::English, 'Sunday, March 31, 2024 20:44:57'];
+ yield [1711917897, FormatPattern::FullDateTime, Timezone::AsiaShanghai, Locale::ChineseTraditional, '星期一, 4月 01, 2024 04:44:57'];
+ yield [1711917897, FormatPattern::Cookie, Timezone::AmericaNewYork, Locale::EnglishUnitedStates, 'Sunday, 31-Mar-2024 16:44:57 EDT'];
+ yield [1711917897, FormatPattern::Http, Timezone::EuropeVienna, Locale::GermanAustria, 'So., 31 März 2024 22:44:57 MESZ'];
+ yield [1711917897, FormatPattern::Email, Timezone::EuropeMadrid, Locale::SpanishSpain, 'dom, 31 mar 2024 22:44:57 GMT+02:00'];
+ yield [1711917897, FormatPattern::SqlDateTime, Timezone::AfricaTunis, Locale::ArabicTunisia, '2024-03-31 21:44:57'];
+ yield [1711832400, FormatPattern::IsoOrdinalDate, Timezone::EuropeMoscow, Locale::RussianRussia, '2024-091'];
+ yield [1711917897, FormatPattern::Iso8601, Timezone::EuropeLondon, Locale::EnglishUnitedKingdom, '2024-03-31T21:44:57.000+01:00'];
+ }
+
+ /**
+ * @dataProvider provideFormatParsingData
+ */
+ public function testFormattingAndPatternParsing(int $timestamp, string|FormatPattern $pattern, Timezone $timezone, Locale $locale, string $expected): void
+ {
+ $timestamp = Timestamp::fromParts($timestamp);
+
+ $result = $timestamp->format(pattern: $pattern, timezone: $timezone, locale: $locale);
+
+ static::assertSame($expected, $result);
+
+ $other = Timestamp::parse($result, pattern: $pattern, timezone: $timezone, locale: $locale);
+
+ static::assertSame($timestamp->getSeconds(), $other->getSeconds());
+ static::assertSame($timestamp->getNanoseconds(), $other->getNanoseconds());
+ }
+
+ public function testToRaw(): void
+ {
+ $timestamp = Timestamp::fromParts(12, 10);
+ $parts = $timestamp->toParts();
+
+ static::assertSame(12, $parts[0]);
+ static::assertSame(10, $parts[1]);
+ }
+
+ /**
+ * @return list
+ */
+ public static function provideCompare(): array
+ {
+ return [
+ [Timestamp::fromParts(100), Timestamp::fromParts(42), Order::Greater],
+ [Timestamp::fromParts(42), Timestamp::fromParts(42), Order::Equal],
+ [Timestamp::fromParts(42), Timestamp::fromParts(100), Order::Less],
+ ];
+ }
+ /**
+ * @dataProvider provideCompare
+ */
+ public function testCompare(Timestamp $a, Timestamp $b, Order $expected): void
+ {
+ $opposite = Order::from(-$expected->value);
+
+ static::assertEquals($expected, $a->compare($b));
+ static::assertEquals($opposite, $b->compare($a));
+ static::assertEquals($expected === Order::Equal, $a->equals($b));
+ static::assertEquals($expected === Order::Less, $a->before($b));
+ static::assertEquals($expected !== Order::Greater, $a->beforeOrAtTheSameTime($b));
+ static::assertEquals($expected === Order::Greater, $a->after($b));
+ static::assertEquals($expected !== Order::Less, $a->afterOrAtTheSameTime($b));
+ static::assertFalse($a->betweenTimeExclusive($a, $a));
+ static::assertFalse($a->betweenTimeExclusive($a, $b));
+ static::assertFalse($a->betweenTimeExclusive($b, $a));
+ static::assertFalse($a->betweenTimeExclusive($b, $b));
+ static::assertTrue($a->betweenTimeInclusive($a, $a));
+ static::assertTrue($a->betweenTimeInclusive($a, $b));
+ static::assertTrue($a->betweenTimeInclusive($b, $a));
+ static::assertEquals($expected === Order::Equal, $a->betweenTimeInclusive($b, $b));
+ }
+
+ public function testNanosecondsModifications(): void
+ {
+ $timestamp = Timestamp::fromParts(0, 100);
+
+ static::assertSame(100, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->plus(Duration::nanoseconds(10));
+
+ static::assertSame(110, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->plus(Duration::nanoseconds(-10));
+
+ static::assertSame(100, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->minus(Duration::nanoseconds(-10));
+
+ static::assertSame(110, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->minus(Duration::nanoseconds(10));
+
+ static::assertSame(100, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->plusNanoseconds(10);
+
+ static::assertSame(110, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->plusNanoseconds(-10);
+
+ static::assertSame(100, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->minusNanoseconds(-10);
+
+ static::assertSame(110, $timestamp->getNanoseconds());
+
+ $timestamp = $timestamp->minusNanoseconds(10);
+
+ static::assertSame(100, $timestamp->getNanoseconds());
+ }
+
+ public function testSecondsModifications(): void
+ {
+ $timestamp = Timestamp::fromParts(5);
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plus(Duration::seconds(1));
+
+ static::assertSame(6, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plus(Duration::seconds(-1));
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minus(Duration::seconds(-1));
+
+ static::assertSame(6, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minus(Duration::seconds(1));
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plusSeconds(1);
+
+ static::assertSame(6, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plusSeconds(-1);
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minusSeconds(-1);
+
+ static::assertSame(6, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minusSeconds(1);
+
+ static::assertSame(5, $timestamp->getSeconds());
+ }
+
+ public function testMinuteModifications(): void
+ {
+ $timestamp = Timestamp::fromParts(5);
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plus(Duration::minutes(1));
+
+ static::assertSame(65, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plus(Duration::minutes(-1));
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minus(Duration::minutes(-1));
+
+ static::assertSame(65, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minus(Duration::minutes(1));
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plusMinutes(1);
+
+ static::assertSame(65, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plusMinutes(-1);
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minusMinutes(-1);
+
+ static::assertSame(65, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minusMinutes(1);
+
+ static::assertSame(5, $timestamp->getSeconds());
+ }
+
+ public function testHourModifications(): void
+ {
+ $timestamp = Timestamp::fromParts(5);
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plus(Duration::hours(1));
+
+ static::assertSame(3605, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plus(Duration::hours(-1));
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minus(Duration::hours(-1));
+
+ static::assertSame(3605, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minus(Duration::hours(1));
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plusHours(1);
+
+ static::assertSame(3605, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->plusHours(-1);
+
+ static::assertSame(5, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minusHours(-1);
+
+ static::assertSame(3605, $timestamp->getSeconds());
+
+ $timestamp = $timestamp->minusHours(1);
+
+ static::assertSame(5, $timestamp->getSeconds());
+ }
+
+ public function testConvertToTimezone(): void
+ {
+ $timestamp = Timestamp::fromParts(1_711_917_232, 501_000_000);
+
+ static::assertSame(
+ '2024-03-31T20:33:52.501Z',
+ $timestamp->convertToTimezone(Timezone::UTC)->format(pattern: FormatPattern::Iso8601),
+ );
+
+ static::assertSame(
+ '2024-03-31T21:33:52.501+01:00',
+ $timestamp->convertToTimezone(Timezone::AfricaTunis)->format(pattern: FormatPattern::Iso8601),
+ );
+
+ static::assertSame(
+ '2024-03-31T16:33:52.501-04:00',
+ $timestamp->convertToTimezone(Timezone::AmericaNewYork)->format(pattern: FormatPattern::Iso8601),
+ );
+
+ static::assertSame(
+ '2024-04-01T04:33:52.501+08:00',
+ $timestamp->convertToTimezone(Timezone::AsiaShanghai)->format(pattern: FormatPattern::Iso8601),
+ );
+ }
+
+ public function testJsonSerialization(): void
+ {
+ $serialized = Timestamp::fromParts(1711917232, 12)->jsonSerialize();
+
+ static::assertSame(1711917232, $serialized['seconds']);
+ static::assertSame(12, $serialized['nanoseconds']);
+ }
+}
diff --git a/tests/unit/DateTime/TimezoneTest.php b/tests/unit/DateTime/TimezoneTest.php
new file mode 100644
index 00000000..07301f88
--- /dev/null
+++ b/tests/unit/DateTime/TimezoneTest.php
@@ -0,0 +1,19 @@
+getPrevious());
+ static::assertSame(Weekday::Tuesday, Weekday::Wednesday->getPrevious());
+ static::assertSame(Weekday::Wednesday, Weekday::Thursday->getPrevious());
+ static::assertSame(Weekday::Thursday, Weekday::Friday->getPrevious());
+ static::assertSame(Weekday::Friday, Weekday::Saturday->getPrevious());
+ static::assertSame(Weekday::Saturday, Weekday::Sunday->getPrevious());
+ static::assertSame(Weekday::Sunday, Weekday::Monday->getPrevious());
+ }
+
+ public function testGetNext(): void
+ {
+ static::assertSame(Weekday::Tuesday, Weekday::Monday->getNext());
+ static::assertSame(Weekday::Wednesday, Weekday::Tuesday->getNext());
+ static::assertSame(Weekday::Thursday, Weekday::Wednesday->getNext());
+ static::assertSame(Weekday::Friday, Weekday::Thursday->getNext());
+ static::assertSame(Weekday::Saturday, Weekday::Friday->getNext());
+ static::assertSame(Weekday::Sunday, Weekday::Saturday->getNext());
+ static::assertSame(Weekday::Monday, Weekday::Sunday->getNext());
+ }
+}
diff --git a/tests/unit/Filesystem/AbstractFilesystemTest.php b/tests/unit/Filesystem/AbstractFilesystemTest.php
index 1feabac4..7436026d 100644
--- a/tests/unit/Filesystem/AbstractFilesystemTest.php
+++ b/tests/unit/Filesystem/AbstractFilesystemTest.php
@@ -20,8 +20,8 @@ abstract class AbstractFilesystemTest extends TestCase
protected function setUp(): void
{
- if (OS\is_windows() || OS\is_darwin()) {
- static::markTestSkipped('Filesystem tests are only executed on linux.');
+ if (OS\is_windows()) {
+ static::markTestSkipped('Test can only be executed under *nix OS.');
}
$this->cacheDirectory = Type\string()->assert(Filesystem\canonicalize(Str\join([
diff --git a/tests/unit/IO/PipeTest.php b/tests/unit/IO/PipeTest.php
index 54b12984..a0c06ab8 100644
--- a/tests/unit/IO/PipeTest.php
+++ b/tests/unit/IO/PipeTest.php
@@ -7,6 +7,7 @@
use PHPUnit\Framework\TestCase;
use Psl;
use Psl\Async;
+use Psl\DateTime;
use Psl\IO;
final class PipeTest extends TestCase
@@ -39,7 +40,7 @@ public function testReadWriteInParallel(): void
$read_awaitable = Async\run(static function () use ($read, $spy): string {
$spy->value .= '[read:sleep]';
- Async\sleep(0.003);
+ Async\sleep(DateTime\Duration::milliseconds(3));
$spy->value .= '[read:start]';
$content = $read->readAll(1000);
$spy->value .= '[read:complete]';
@@ -50,7 +51,7 @@ public function testReadWriteInParallel(): void
Async\run(static function () use ($write, $spy): void {
$spy->value .= '[write:sleep]';
- Async\sleep(0.0035);
+ Async\sleep(DateTime\Duration::milliseconds(5));
$spy->value .= '[write:start]';
$write->writeAll('hello');
$spy->value .= '[write:complete]';
@@ -96,7 +97,7 @@ public function testReadAllTimedOut(): void
$this->expectException(IO\Exception\TimeoutException::class);
$this->expectExceptionMessage('Reached timeout while the handle is still not readable.');
- $read->readAll(timeout: 0.001);
+ $read->readAll(timeout: DateTime\Duration::milliseconds(1));
}
public function testReadOnAlreadyClosedPipe(): void
diff --git a/tests/unit/TCP/ServerTest.php b/tests/unit/TCP/ServerTest.php
index 8d73774d..b62084bf 100644
--- a/tests/unit/TCP/ServerTest.php
+++ b/tests/unit/TCP/ServerTest.php
@@ -6,6 +6,7 @@
use PHPUnit\Framework\TestCase;
use Psl\Async;
+use Psl\DateTime;
use Psl\Network;
use Psl\Network\Exception\AlreadyStoppedException;
use Psl\TCP;
@@ -78,7 +79,7 @@ public function testIncoming(): void
{
$server = TCP\Server::create('127.0.0.1');
$incoming = $server->incoming();
- Async\Scheduler::delay(0.01, static fn() => $server->close());
+ Async\Scheduler::delay(DateTime\Duration::milliseconds(1), static fn() => $server->close());
Async\Scheduler::defer(static function () use ($server) {
TCP\connect('127.0.0.1', $server->getLocalAddress()->port);
});