diff --git a/composer.json b/composer.json index c416aa0..3c4269a 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,8 @@ "name": "wiz-develop/php-value-object", "description": "📦 The PHP Value Object library offers immutable, type-safe, and self-validating objects to model domain values using the Value Object pattern.", "require": { - "php": ">=8.4" + "php": ">=8.4", + "wiz-develop/php-monad": "^2.2" }, "type": "library", "license": "MIT", @@ -28,7 +29,6 @@ "wiz-develop/php-cs-fixer-config": "^8.3", "phpunit/php-code-coverage": "^11.0", "phpunit/phpunit": "^11.3", - "wiz-develop/php-monad": "^2.2", "phpstan/phpstan": "^2.1", "phpstan/extension-installer": "^1.4", "phpstan/phpstan-strict-rules": "^2.0" diff --git a/composer.lock b/composer.lock index 97bcda5..b9caa7d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,61 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a276d7269cc3ec2f9fe1450eae7ab16f", - "packages": [], + "content-hash": "c8642d7f36fe68cfe7e4cd0498917523", + "packages": [ + { + "name": "wiz-develop/php-monad", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/wiz-develop/php-monad.git", + "reference": "e52ac1d9b5f6b5c8125325b27a87df1dd6067532" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wiz-develop/php-monad/zipball/e52ac1d9b5f6b5c8125325b27a87df1dd6067532", + "reference": "e52ac1d9b5f6b5c8125325b27a87df1dd6067532", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/php-code-coverage": "^11.0", + "phpunit/phpunit": "^11.3", + "wiz-develop/php-cs-fixer-config": "^8.3", + "wiz-develop/php-value-object": "^0.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "WizDevelop\\PhpMonad\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "kakiuchi-shigenao", + "email": "87973394+endou-mame@users.noreply.github.com" + } + ], + "description": "📦 Implemented the concept of monads in functional programming in PHP.", + "support": { + "issues": "https://github.com/wiz-develop/php-monad/issues", + "source": "https://github.com/wiz-develop/php-monad/tree/v2.6.3" + }, + "time": "2025-05-20T01:42:36+00:00" + } + ], "packages-dev": [ { "name": "clue/ndjson-react", @@ -571,16 +624,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "shasum": "" }, "require": { @@ -623,9 +676,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "phar-io/manifest", @@ -795,16 +848,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.14", + "version": "2.1.17", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2" + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2", - "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", "shasum": "" }, "require": { @@ -849,7 +902,7 @@ "type": "github" } ], - "time": "2025-05-02T15:32:28+00:00" + "time": "2025-05-21T20:55:28+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -1224,16 +1277,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.20", + "version": "11.5.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e6bdea63ecb7a8287d2cdab25bdde3126e0cfe6f" + "reference": "4cd72faaa8f811e4cc63040cba167757660a5538" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e6bdea63ecb7a8287d2cdab25bdde3126e0cfe6f", - "reference": "e6bdea63ecb7a8287d2cdab25bdde3126e0cfe6f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4cd72faaa8f811e4cc63040cba167757660a5538", + "reference": "4cd72faaa8f811e4cc63040cba167757660a5538", "shasum": "" }, "require": { @@ -1256,7 +1309,7 @@ "sebastian/code-unit": "^3.0.3", "sebastian/comparator": "^6.3.1", "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/exporter": "^6.3.0", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", @@ -1305,7 +1358,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.22" }, "funding": [ { @@ -1329,7 +1382,7 @@ "type": "tidelift" } ], - "time": "2025-05-11T06:39:52+00:00" + "time": "2025-06-06T02:48:05+00:00" }, { "name": "psr/container", @@ -2387,23 +2440,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -2439,15 +2492,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", @@ -2990,23 +3055,24 @@ }, { "name": "symfony/console", - "version": "v7.2.6", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0e2e3f38c192e93e622e41ec37f4ca70cfedf218" + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0e2e3f38c192e93e622e41ec37f4ca70cfedf218", - "reference": "0e2e3f38c192e93e622e41ec37f4ca70cfedf218", + "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -3063,7 +3129,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.6" + "source": "https://github.com/symfony/console/tree/v7.3.0" }, "funding": [ { @@ -3079,20 +3145,20 @@ "type": "tidelift" } ], - "time": "2025-04-07T19:09:28+00:00" + "time": "2025-05-24T10:34:04+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -3105,7 +3171,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3130,7 +3196,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.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3146,20 +3212,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", "shasum": "" }, "require": { @@ -3210,7 +3276,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.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" }, "funding": [ { @@ -3226,20 +3292,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-04-22T09:11:45+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -3253,7 +3319,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3286,7 +3352,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -3302,11 +3368,11 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -3352,7 +3418,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + "source": "https://github.com/symfony/filesystem/tree/v7.3.0" }, "funding": [ { @@ -3372,16 +3438,16 @@ }, { "name": "symfony/finder", - "version": "v7.2.2", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", "shasum": "" }, "require": { @@ -3416,7 +3482,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.2" + "source": "https://github.com/symfony/finder/tree/v7.3.0" }, "funding": [ { @@ -3432,20 +3498,20 @@ "type": "tidelift" } ], - "time": "2024-12-30T19:00:17+00:00" + "time": "2024-12-30T19:00:26+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca", + "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca", "shasum": "" }, "require": { @@ -3483,7 +3549,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.0" }, "funding": [ { @@ -3499,7 +3565,7 @@ "type": "tidelift" } ], - "time": "2024-11-20T11:17:29+00:00" + "time": "2025-04-04T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3978,16 +4044,16 @@ }, { "name": "symfony/process", - "version": "v7.2.5", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", "shasum": "" }, "require": { @@ -4019,7 +4085,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.5" + "source": "https://github.com/symfony/process/tree/v7.3.0" }, "funding": [ { @@ -4035,20 +4101,20 @@ "type": "tidelift" } ], - "time": "2025-03-13T12:21:46+00:00" + "time": "2025-04-17T09:11:12+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -4066,7 +4132,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -4102,7 +4168,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -4118,11 +4184,11 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/stopwatch", - "version": "v7.2.4", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -4164,7 +4230,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.2.4" + "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" }, "funding": [ { @@ -4184,16 +4250,16 @@ }, { "name": "symfony/string", - "version": "v7.2.6", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931" + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/a214fe7d62bd4df2a76447c67c6b26e1d5e74931", - "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", "shasum": "" }, "require": { @@ -4251,7 +4317,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.6" + "source": "https://github.com/symfony/string/tree/v7.3.0" }, "funding": [ { @@ -4267,7 +4333,7 @@ "type": "tidelift" } ], - "time": "2025-04-20T20:18:16+00:00" + "time": "2025-04-20T20:19:01+00:00" }, { "name": "theseer/tokenizer", @@ -4359,58 +4425,6 @@ "source": "https://github.com/wiz-develop/php-cs-fixer-config/tree/v8.3.2" }, "time": "2024-05-24T08:02:53+00:00" - }, - { - "name": "wiz-develop/php-monad", - "version": "v2.6.1", - "source": { - "type": "git", - "url": "https://github.com/wiz-develop/php-monad.git", - "reference": "476d2ff6b51f788c5c175068e2bac7982c9469ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wiz-develop/php-monad/zipball/476d2ff6b51f788c5c175068e2bac7982c9469ea", - "reference": "476d2ff6b51f788c5c175068e2bac7982c9469ea", - "shasum": "" - }, - "require": { - "php": ">=8.3" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/php-code-coverage": "^11.0", - "phpunit/phpunit": "^11.3", - "wiz-develop/php-cs-fixer-config": "^8.3", - "wiz-develop/php-value-object": "^0.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ], - "psr-4": { - "WizDevelop\\PhpMonad\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "kakiuchi-shigenao", - "email": "87973394+endou-mame@users.noreply.github.com" - } - ], - "description": "📦 Implemented the concept of monads in functional programming in PHP.", - "support": { - "issues": "https://github.com/wiz-develop/php-monad/issues", - "source": "https://github.com/wiz-develop/php-monad/tree/v2.6.1" - }, - "time": "2025-05-10T08:02:27+00:00" } ], "aliases": [], diff --git a/examples/DateTime/TestLocalDateRange.php b/examples/DateTime/TestLocalDateRange.php new file mode 100644 index 0000000..0ab6917 --- /dev/null +++ b/examples/DateTime/TestLocalDateRange.php @@ -0,0 +1,124 @@ +toISOString()}\n"; +echo "日数: {$january->days()} 日\n\n"; + +// 2. 開区間と閉区間の違い +echo "=== 開区間と閉区間の違い ===\n"; +$closedWeek = LocalDateRange::closed( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 1, 7) +); +$openWeek = LocalDateRange::open( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 1, 7) +); + +echo "閉区間(両端含む): {$closedWeek->toISOString()} = {$closedWeek->days()} 日\n"; +echo "開区間(両端含まない): {$openWeek->toISOString()} = {$openWeek->days()} 日\n\n"; + +// 3. 半開区間の使用例(一般的な日付範囲の表現) +echo "=== 半開区間の使用例 ===\n"; +// 月初から月末まで(月末を含まない一般的なパターン) +$month = LocalDateRange::halfOpenRight( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 2, 1) +); + +echo "1月(右半開区間): {$month->toISOString()}\n"; +echo '1月31日を含む: ' . ($month->contains(LocalDate::of(2024, 1, 31)) ? 'はい' : 'いいえ') . "\n"; +echo '2月1日を含む: ' . ($month->contains(LocalDate::of(2024, 2, 1)) ? 'はい' : 'いいえ') . "\n\n"; + +// 4. 日付の反復処理 +echo "=== 日付の反復処理 ===\n"; +$weekRange = LocalDateRange::closed( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 1, 7) +); + +echo "1週間の日付:\n"; +foreach ($weekRange->iterate() as $date) { + echo " - {$date->toISOString()}\n"; +} +echo "\n"; + +// 5. 期間の重なり判定 +echo "=== 期間の重なり判定 ===\n"; +$q1 = LocalDateRange::closed( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 3, 31) +); +$q2 = LocalDateRange::closed( + LocalDate::of(2024, 4, 1), + LocalDate::of(2024, 6, 30) +); +$marchToMay = LocalDateRange::closed( + LocalDate::of(2024, 3, 1), + LocalDate::of(2024, 5, 31) +); + +echo "第1四半期: {$q1->toISOString()}\n"; +echo "第2四半期: {$q2->toISOString()}\n"; +echo "3月〜5月: {$marchToMay->toISOString()}\n"; +echo '第1四半期と第2四半期が重なる: ' . ($q1->overlaps($q2) ? 'はい' : 'いいえ') . "\n"; +echo '第1四半期と3月〜5月が重なる: ' . ($q1->overlaps($marchToMay) ? 'はい' : 'いいえ') . "\n"; +echo '第2四半期と3月〜5月が重なる: ' . ($q2->overlaps($marchToMay) ? 'はい' : 'いいえ') . "\n\n"; + +// 6. 特定の日付が期間内かチェック +echo "=== 期間内チェック ===\n"; +$vacation = LocalDateRange::closed( + LocalDate::of(2024, 8, 10), + LocalDate::of(2024, 8, 20) +); +$checkDate = LocalDate::of(2024, 8, 15); + +echo "休暇期間: {$vacation->toISOString()}\n"; +echo "{$checkDate->toISOString()} は休暇中: " . ($vacation->contains($checkDate) ? 'はい' : 'いいえ') . "\n\n"; + +// 7. エラーハンドリング +echo "=== エラーハンドリング ===\n"; +$invalidResult = LocalDateRange::tryFrom( + LocalDate::of(2024, 12, 31), + LocalDate::of(2024, 1, 1) +); + +if ($invalidResult->isErr()) { + $error = $invalidResult->unwrapErr(); + echo "エラー: {$error->getMessage()}\n"; + echo "エラーコード: {$error->getCode()}\n"; +} + +// 8. nullable対応 +echo "\n=== Nullable対応 ===\n"; +$startDate = LocalDate::of(2024, 1, 1); +$endDate = null; + +$optionRange = LocalDateRange::fromNullable($startDate, $endDate); +if ($optionRange->isNone()) { + echo "範囲を作成できませんでした(いずれかの値がnullです)\n"; +} + +// 9. 年間カレンダーの例 +echo "\n=== 年間カレンダーの例 ===\n"; +$year2024 = LocalDateRange::closed( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 12, 31) +); + +echo "2024年: {$year2024->toISOString()}\n"; +echo "日数: {$year2024->days()} 日(うるう年)\n"; diff --git a/examples/DateTime/TestLocalDateTimeRange.php b/examples/DateTime/TestLocalDateTimeRange.php new file mode 100644 index 0000000..919e440 --- /dev/null +++ b/examples/DateTime/TestLocalDateTimeRange.php @@ -0,0 +1,97 @@ +toISOString()}\n"; +echo "期間(時間): {$workingHours->durationInHours()} 時間\n\n"; + +// 2. 開区間と閉区間の違い +echo "=== 開区間と閉区間の違い ===\n"; +$closedRange = LocalDateTimeRange::closed($start, $end); +$openRange = LocalDateTimeRange::open($start, $end); + +$testTime = $start; // 開始時刻でテスト +echo "テスト時刻: {$testTime->toISOString()}\n"; +echo '閉区間に含まれる: ' . ($closedRange->contains($testTime) ? 'はい' : 'いいえ') . "\n"; +echo '開区間に含まれる: ' . ($openRange->contains($testTime) ? 'はい' : 'いいえ') . "\n\n"; + +// 3. 半開区間の使用例 +echo "=== 半開区間の使用例 ===\n"; +// イベントのスケジュール(開始時刻を含み、終了時刻を含まない) +$event1 = LocalDateTimeRange::halfOpenRight( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-01 12:00:00')) +); +$event2 = LocalDateTimeRange::halfOpenRight( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 12:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-01 14:00:00')) +); + +echo "イベント1: {$event1->toISOString()}\n"; +echo "イベント2: {$event2->toISOString()}\n"; +echo 'イベントが重なる: ' . ($event1->overlaps($event2) ? 'はい' : 'いいえ') . "\n\n"; + +// 4. 現在時刻が営業時間内かチェック +echo "=== 営業時間チェック ===\n"; +$now = LocalDateTime::now(new DateTimeZone('Asia/Tokyo')); +$businessHours = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('today 09:00:00')), + LocalDateTime::from(new DateTimeImmutable('today 18:00:00')) +); + +echo "現在時刻: {$now->toISOString()}\n"; +echo "営業時間: {$businessHours->toISOString()}\n"; +echo '営業中: ' . ($businessHours->contains($now) ? 'はい' : 'いいえ') . "\n\n"; + +// 5. 期間の重なり判定 +echo "=== 期間の重なり判定 ===\n"; +$meeting1 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 14:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-01 15:30:00')) +); +$meeting2 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 15:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-01 16:00:00')) +); + +echo "会議1: {$meeting1->toISOString()}\n"; +echo "会議2: {$meeting2->toISOString()}\n"; +echo 'スケジュールが衝突: ' . ($meeting1->overlaps($meeting2) ? 'はい' : 'いいえ') . "\n\n"; + +// 6. エラーハンドリング +echo "=== エラーハンドリング ===\n"; +$invalidResult = LocalDateTimeRange::tryFrom( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 18:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-01 09:00:00')) +); + +if ($invalidResult->isErr()) { + $error = $invalidResult->unwrapErr(); + echo "エラー: {$error->getMessage()}\n"; + echo "エラーコード: {$error->getCode()}\n"; +} + +// 7. nullable対応 +echo "\n=== Nullable対応 ===\n"; +$fromTime = LocalDateTime::from(new DateTimeImmutable('2024-01-01 09:00:00')); +$toTime = null; + +$optionRange = LocalDateTimeRange::fromNullable($fromTime, $toTime); +if ($optionRange->isNone()) { + echo "範囲を作成できませんでした(いずれかの値がnullです)\n"; +} diff --git a/src/DateTime/LocalDateRange.php b/src/DateTime/LocalDateRange.php new file mode 100644 index 0000000..0d98be5 --- /dev/null +++ b/src/DateTime/LocalDateRange.php @@ -0,0 +1,319 @@ +isOk()); + } + + // ------------------------------------------------------------------------- + // MARK: implement IValueObject + // ------------------------------------------------------------------------- + #[Override] + final public function equals(IValueObject $other): bool + { + + return $this->from->equals($other->from) + && $this->to->equals($other->to) + && $this->rangeType === $other->rangeType; + } + + #[Override] + final public function __toString(): string + { + return $this->toISOString(); + } + + #[Override] + final public function jsonSerialize(): string + { + return (string)$this; + } + + // ------------------------------------------------------------------------- + // MARK: factory methods + // ------------------------------------------------------------------------- + /** + * 指定された開始日付、終了日付、範囲タイプでインスタンスを生成 + */ + final public static function from( + LocalDate $from, + ?LocalDate $to = null, + RangeType $rangeType = RangeType::CLOSED + ): static { + $to ??= LocalDate::of(self::MAX_DATE_YEAR, self::MAX_DATE_MONTH, self::MAX_DATE_DAY); + + return new static($from, $to, $rangeType); + } + + /** + * 閉区間でインスタンスを生成 + */ + final public static function closed(LocalDate $from, ?LocalDate $to = null): static + { + return static::from($from, $to, RangeType::CLOSED); + } + + /** + * 開区間でインスタンスを生成 + */ + final public static function open(LocalDate $from, ?LocalDate $to = null): static + { + return static::from($from, $to, RangeType::OPEN); + } + + /** + * 左開区間でインスタンスを生成 + */ + final public static function halfOpenLeft(LocalDate $from, ?LocalDate $to = null): static + { + return static::from($from, $to, RangeType::HALF_OPEN_LEFT); + } + + /** + * 右開区間でインスタンスを生成 + */ + final public static function halfOpenRight(LocalDate $from, ?LocalDate $to = null): static + { + return static::from($from, $to, RangeType::HALF_OPEN_RIGHT); + } + + /** + * @return Result + */ + final public static function tryFrom( + LocalDate $from, + ?LocalDate $to = null, + RangeType $rangeType = RangeType::CLOSED + ): Result { + $to ??= LocalDate::of(self::MAX_DATE_YEAR, self::MAX_DATE_MONTH, self::MAX_DATE_DAY); + + return static::isValid($from, $to) + ->andThen(static fn () => Result\ok(static::from($from, $to, $rangeType))); + } + + /** + * @return Option + */ + final public static function fromNullable( + ?LocalDate $from, + ?LocalDate $to = null, + RangeType $rangeType = RangeType::CLOSED + ): Option { + if ($from === null) { + return Option\none(); + } + + return Option\some(static::from($from, $to, $rangeType)); + } + + /** + * @return Result,ValueObjectError> + */ + final public static function tryFromNullable( + ?LocalDate $from, + ?LocalDate $to = null, + RangeType $rangeType = RangeType::CLOSED + ): Result { + if ($from === null) { + // @phpstan-ignore return.type + return Result\ok(Option\none()); + } + + // @phpstan-ignore return.type + return static::tryFrom($from, $to, $rangeType)->map(static fn ($result) => Option\some($result)); + } + + // ------------------------------------------------------------------------- + // MARK: validation methods + // ------------------------------------------------------------------------- + /** + * 有効な値かどうか + * @return Result + */ + protected static function isValid(LocalDate $from, LocalDate $to): Result + { + if ($from->isAfter($to)) { + return Result\err(ValueObjectError::of( + code: 'value_object.date_range.invalid_range', + message: '開始日付は終了日付以前である必要があります' + )); + } + + return Result\ok(true); + } + + // ------------------------------------------------------------------------- + // MARK: public methods + // ------------------------------------------------------------------------- + /** + * ISO 8601形式の文字列表現を返す + */ + final public function toISOString(): string + { + $leftBracket = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_RIGHT => '[', + RangeType::OPEN, RangeType::HALF_OPEN_LEFT => '(', + }; + + $rightBracket = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_LEFT => ']', + RangeType::OPEN, RangeType::HALF_OPEN_RIGHT => ')', + }; + + return sprintf('%s%s, %s%s', $leftBracket, $this->from->toISOString(), $this->to->toISOString(), $rightBracket); + } + + final public function getFrom(): LocalDate + { + return $this->from; + } + + final public function getTo(): LocalDate + { + return $this->to; + } + + final public function getRangeType(): RangeType + { + return $this->rangeType; + } + + /** + * 指定された日付が範囲内に含まれるかを判定 + */ + final public function contains(LocalDate $date): bool + { + $afterFrom = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_RIGHT => $date->isAfterOrEqualTo($this->from), + RangeType::OPEN, RangeType::HALF_OPEN_LEFT => $date->isAfter($this->from), + }; + + $beforeTo = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_LEFT => $date->isBeforeOrEqualTo($this->to), + RangeType::OPEN, RangeType::HALF_OPEN_RIGHT => $date->isBefore($this->to), + }; + + return $afterFrom && $beforeTo; + } + + /** + * 他の範囲と重なりがあるかを判定 + */ + final public function overlaps(self $other): bool + { + // 一方の範囲の終了が他方の開始より前の場合、重なりなし + if ($this->strictlyBefore($other) || $other->strictlyBefore($this)) { + return false; + } + + // 境界での接触を考慮 + return $this->hasOverlapAt($other); + } + + /** + * この範囲が他の範囲より完全に前にあるかを判定 + */ + private function strictlyBefore(self $other): bool + { + return $this->to->isBefore($other->from) || ( + $this->to->equals($other->from) && ( + $this->rangeType === RangeType::OPEN + || $this->rangeType === RangeType::HALF_OPEN_RIGHT + || $other->rangeType === RangeType::OPEN + || $other->rangeType === RangeType::HALF_OPEN_LEFT + ) + ); + } + + /** + * 境界での重なりを考慮した判定 + */ + private function hasOverlapAt(self $other): bool + { + // 開始点での重なり判定 + $startOverlap = $this->contains($other->from) || $other->contains($this->from); + + // 終了点での重なり判定 + $endOverlap = $this->contains($other->to) || $other->contains($this->to); + + // 一方が他方を完全に含む場合 + $containment = ($this->from->isBeforeOrEqualTo($other->from) && $this->to->isAfterOrEqualTo($other->to)) + || ($other->from->isBeforeOrEqualTo($this->from) && $other->to->isAfterOrEqualTo($this->to)); + + return $startOverlap || $endOverlap || $containment; + } + + /** + * 範囲の日数を返す + * 注意: 開区間の場合、実際の日数は計算結果より1日または2日少なくなる可能性があります + */ + final public function days(): int + { + $fromTimestamp = $this->from->toDateTimeImmutable()->getTimestamp(); + $toTimestamp = $this->to->toDateTimeImmutable()->getTimestamp(); + + $days = (int)(($toTimestamp - $fromTimestamp) / 86400); + + // 区間タイプによる調整 + return match ($this->rangeType) { + RangeType::CLOSED => $days + 1, // 両端を含む + RangeType::OPEN => max(0, $days - 1), // 両端を含まない + RangeType::HALF_OPEN_LEFT, RangeType::HALF_OPEN_RIGHT => $days, // 片方の端を含む + }; + } + + /** + * 範囲に含まれる各日付を順に返すイテレータを取得 + * @return Generator + */ + final public function iterate(): Generator + { + $current = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_RIGHT => $this->from, + RangeType::OPEN, RangeType::HALF_OPEN_LEFT => $this->from->addDays(1), + }; + + $endCondition = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_LEFT => fn (LocalDate $date) => $date->isBeforeOrEqualTo($this->to), + RangeType::OPEN, RangeType::HALF_OPEN_RIGHT => fn (LocalDate $date) => $date->isBefore($this->to), + }; + + while ($endCondition($current)) { + yield $current; + $current = $current->addDays(1); + } + } +} diff --git a/src/DateTime/LocalDateTimeRange.php b/src/DateTime/LocalDateTimeRange.php new file mode 100644 index 0000000..4fa084d --- /dev/null +++ b/src/DateTime/LocalDateTimeRange.php @@ -0,0 +1,320 @@ +isOk()); + } + + // ------------------------------------------------------------------------- + // MARK: implement IValueObject + // ------------------------------------------------------------------------- + #[Override] + final public function equals(IValueObject $other): bool + { + return $this->from->equals($other->from) + && $this->to->equals($other->to) + && $this->rangeType === $other->rangeType; + } + + #[Override] + final public function __toString(): string + { + return $this->toISOString(); + } + + #[Override] + final public function jsonSerialize(): string + { + return (string)$this; + } + + // ------------------------------------------------------------------------- + // MARK: factory methods + // ------------------------------------------------------------------------- + /** + * 指定された開始日時、終了日時、範囲タイプでインスタンスを生成 + */ + final public static function from( + LocalDateTime $from, + ?LocalDateTime $to = null, + RangeType $rangeType = RangeType::CLOSED + ): static { + $to ??= LocalDateTime::of( + LocalDate::of(self::MAX_DATE_YEAR, self::MAX_DATE_MONTH, self::MAX_DATE_DAY), + LocalTime::of(self::MAX_TIME_HOUR, self::MAX_TIME_MINUTE, self::MAX_TIME_SECOND) + ); + + return new static($from, $to, $rangeType); + } + + /** + * 閉区間でインスタンスを生成 + */ + final public static function closed(LocalDateTime $from, ?LocalDateTime $to = null): static + { + return static::from($from, $to, RangeType::CLOSED); + } + + /** + * 開区間でインスタンスを生成 + */ + final public static function open(LocalDateTime $from, ?LocalDateTime $to = null): static + { + return static::from($from, $to, RangeType::OPEN); + } + + /** + * 左開区間でインスタンスを生成 + */ + final public static function halfOpenLeft(LocalDateTime $from, ?LocalDateTime $to = null): static + { + return static::from($from, $to, RangeType::HALF_OPEN_LEFT); + } + + /** + * 右開区間でインスタンスを生成 + */ + final public static function halfOpenRight(LocalDateTime $from, ?LocalDateTime $to = null): static + { + return static::from($from, $to, RangeType::HALF_OPEN_RIGHT); + } + + /** + * @return Result + */ + final public static function tryFrom( + LocalDateTime $from, + ?LocalDateTime $to = null, + RangeType $rangeType = RangeType::CLOSED + ): Result { + $to ??= LocalDateTime::of( + date: LocalDate::of(self::MAX_DATE_YEAR, self::MAX_DATE_MONTH, self::MAX_DATE_DAY), + time: LocalTime::of(self::MAX_TIME_HOUR, self::MAX_TIME_MINUTE, self::MAX_TIME_SECOND) + ); + + return static::isValid($from, $to) + ->andThen(static fn () => Result\ok(static::from($from, $to, $rangeType))); + } + + /** + * @return Option + */ + final public static function fromNullable( + ?LocalDateTime $from, + ?LocalDateTime $to = null, + RangeType $rangeType = RangeType::CLOSED + ): Option { + if ($from === null) { + return Option\none(); + } + + return Option\some(static::from($from, $to, $rangeType)); + } + + /** + * @return Result,ValueObjectError> + */ + final public static function tryFromNullable( + ?LocalDateTime $from, + ?LocalDateTime $to = null, + RangeType $rangeType = RangeType::CLOSED + ): Result { + if ($from === null) { + // @phpstan-ignore return.type + return Result\ok(Option\none()); + } + + // @phpstan-ignore return.type + return static::tryFrom($from, $to, $rangeType)->map(static fn ($result) => Option\some($result)); + } + + // ------------------------------------------------------------------------- + // MARK: validation methods + // ------------------------------------------------------------------------- + /** + * 有効な値かどうか + * @return Result + */ + protected static function isValid(LocalDateTime $from, LocalDateTime $to): Result + { + if ($from->isAfter($to)) { + return Result\err(ValueObjectError::of( + code: 'value_object.datetime_range.invalid_range', + message: '開始日時は終了日時以前である必要があります' + )); + } + + return Result\ok(true); + } + + // ------------------------------------------------------------------------- + // MARK: public methods + // ------------------------------------------------------------------------- + /** + * ISO 8601形式の文字列表現を返す + */ + final public function toISOString(): string + { + $leftBracket = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_RIGHT => '[', + RangeType::OPEN, RangeType::HALF_OPEN_LEFT => '(', + }; + + $rightBracket = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_LEFT => ']', + RangeType::OPEN, RangeType::HALF_OPEN_RIGHT => ')', + }; + + return sprintf('%s%s, %s%s', $leftBracket, $this->from->toISOString(), $this->to->toISOString(), $rightBracket); + } + + final public function getFrom(): LocalDateTime + { + return $this->from; + } + + final public function getTo(): LocalDateTime + { + return $this->to; + } + + final public function getRangeType(): RangeType + { + return $this->rangeType; + } + + /** + * 指定された日時が範囲内に含まれるかを判定 + */ + final public function contains(LocalDateTime $dateTime): bool + { + $afterFrom = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_RIGHT => $dateTime->isAfterOrEqualTo($this->from), + RangeType::OPEN, RangeType::HALF_OPEN_LEFT => $dateTime->isAfter($this->from), + }; + + $beforeTo = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_LEFT => $dateTime->isBeforeOrEqualTo($this->to), + RangeType::OPEN, RangeType::HALF_OPEN_RIGHT => $dateTime->isBefore($this->to), + }; + + return $afterFrom && $beforeTo; + } + + /** + * 他の範囲と重なりがあるかを判定 + */ + final public function overlaps(self $other): bool + { + // 一方の範囲の終了が他方の開始より前の場合、重なりなし + if ($this->strictlyBefore($other) || $other->strictlyBefore($this)) { + return false; + } + + // 境界での接触を考慮 + return $this->hasOverlapAt($other); + } + + /** + * この範囲が他の範囲より完全に前にあるかを判定 + */ + private function strictlyBefore(self $other): bool + { + return $this->to->isBefore($other->from) || ( + $this->to->equals($other->from) && ( + $this->rangeType === RangeType::OPEN + || $this->rangeType === RangeType::HALF_OPEN_RIGHT + || $other->rangeType === RangeType::OPEN + || $other->rangeType === RangeType::HALF_OPEN_LEFT + ) + ); + } + + /** + * 境界での重なりを考慮した判定 + */ + private function hasOverlapAt(self $other): bool + { + // 開始点での重なり判定 + $startOverlap = $this->contains($other->from) || $other->contains($this->from); + + // 終了点での重なり判定 + $endOverlap = $this->contains($other->to) || $other->contains($this->to); + + // 一方が他方を完全に含む場合 + $containment = ($this->from->isBeforeOrEqualTo($other->from) && $this->to->isAfterOrEqualTo($other->to)) + || ($other->from->isBeforeOrEqualTo($this->from) && $other->to->isAfterOrEqualTo($this->to)); + + return $startOverlap || $endOverlap || $containment; + } + + /** + * 範囲の期間を秒単位で返す + */ + final public function durationInSeconds(): int + { + $fromTimestamp = $this->from->toDateTimeImmutable()->getTimestamp(); + $toTimestamp = $this->to->toDateTimeImmutable()->getTimestamp(); + + return $toTimestamp - $fromTimestamp; + } + + /** + * 範囲の期間を分単位で返す + */ + final public function durationInMinutes(): float + { + return $this->durationInSeconds() / 60; + } + + /** + * 範囲の期間を時間単位で返す + */ + final public function durationInHours(): float + { + return $this->durationInSeconds() / 3600; + } + + /** + * 範囲の期間を日単位で返す + */ + final public function durationInDays(): float + { + return $this->durationInSeconds() / 86400; + } +} diff --git a/src/DateTime/RangeType.php b/src/DateTime/RangeType.php new file mode 100644 index 0000000..0b1549c --- /dev/null +++ b/src/DateTime/RangeType.php @@ -0,0 +1,16 @@ +assertSame($from, $range->getFrom()); + $this->assertSame($to, $range->getTo()); + $this->assertSame(RangeType::CLOSED, $range->getRangeType()); + $this->assertSame('[2024-01-01, 2024-01-31]', $range->toISOString()); + } + + public function test_開区間で有効な範囲を作成できる(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + + // Act + $range = LocalDateRange::open($from, $to); + + // Assert + $this->assertSame(RangeType::OPEN, $range->getRangeType()); + $this->assertSame('(2024-01-01, 2024-01-31)', $range->toISOString()); + } + + public function test_半開区間で有効な範囲を作成できる(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + + // Act + $rangeLeft = LocalDateRange::halfOpenLeft($from, $to); + $rangeRight = LocalDateRange::halfOpenRight($from, $to); + + // Assert + $this->assertSame(RangeType::HALF_OPEN_LEFT, $rangeLeft->getRangeType()); + $this->assertSame('(2024-01-01, 2024-01-31]', $rangeLeft->toISOString()); + $this->assertSame(RangeType::HALF_OPEN_RIGHT, $rangeRight->getRangeType()); + $this->assertSame('[2024-01-01, 2024-01-31)', $rangeRight->toISOString()); + } + + public function test_開始日付が終了日付より後の場合エラーになる(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 31); + $to = LocalDate::of(2024, 1, 30); + + // Act + $result = LocalDateRange::tryFrom($from, $to); + + // Assert + $this->assertTrue($result->isErr()); + $error = $result->unwrapErr(); + $this->assertInstanceOf(ValueObjectError::class, $error); + $this->assertSame('value_object.date_range.invalid_range', $error->getCode()); + $this->assertSame('開始日付は終了日付以前である必要があります', $error->getMessage()); + } + + public function test_contains_閉区間の境界値を含む(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + $range = LocalDateRange::closed($from, $to); + + // Act & Assert + $this->assertTrue($range->contains($from)); // 開始境界 + $this->assertTrue($range->contains($to)); // 終了境界 + $this->assertTrue($range->contains(LocalDate::of(2024, 1, 15))); // 中間 + $this->assertFalse($range->contains(LocalDate::of(2023, 12, 31))); // 範囲前 + $this->assertFalse($range->contains(LocalDate::of(2024, 2, 1))); // 範囲後 + } + + public function test_contains_開区間の境界値を含まない(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + $range = LocalDateRange::open($from, $to); + + // Act & Assert + $this->assertFalse($range->contains($from)); // 開始境界 + $this->assertFalse($range->contains($to)); // 終了境界 + $this->assertTrue($range->contains(LocalDate::of(2024, 1, 15))); // 中間 + } + + public function test_contains_半開区間の境界値(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + + // Act & Assert + // 左開区間 + $rangeLeft = LocalDateRange::halfOpenLeft($from, $to); + $this->assertFalse($rangeLeft->contains($from)); // 開始境界(含まない) + $this->assertTrue($rangeLeft->contains($to)); // 終了境界(含む) + + // 右開区間 + $rangeRight = LocalDateRange::halfOpenRight($from, $to); + $this->assertTrue($rangeRight->contains($from)); // 開始境界(含む) + $this->assertFalse($rangeRight->contains($to)); // 終了境界(含まない) + } + + public function test_overlaps_重なりがある範囲(): void + { + // Arrange + $range1 = LocalDateRange::closed( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 1, 15) + ); + $range2 = LocalDateRange::closed( + LocalDate::of(2024, 1, 10), + LocalDate::of(2024, 1, 20) + ); + + // Act & Assert + $this->assertTrue($range1->overlaps($range2)); + $this->assertTrue($range2->overlaps($range1)); + } + + public function test_overlaps_重なりがない範囲(): void + { + // Arrange + $range1 = LocalDateRange::closed( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 1, 10) + ); + $range2 = LocalDateRange::closed( + LocalDate::of(2024, 1, 20), + LocalDate::of(2024, 1, 31) + ); + + // Act & Assert + $this->assertFalse($range1->overlaps($range2)); + $this->assertFalse($range2->overlaps($range1)); + } + + public function test_overlaps_境界で接する範囲_閉区間(): void + { + // Arrange + $range1 = LocalDateRange::closed( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 1, 15) + ); + $range2 = LocalDateRange::closed( + LocalDate::of(2024, 1, 15), + LocalDate::of(2024, 1, 31) + ); + + // Act & Assert + $this->assertTrue($range1->overlaps($range2)); // 境界で接触 + $this->assertTrue($range2->overlaps($range1)); + } + + public function test_overlaps_境界で接する範囲_開区間(): void + { + // Arrange + $range1 = LocalDateRange::open( + LocalDate::of(2024, 1, 1), + LocalDate::of(2024, 1, 15) + ); + $range2 = LocalDateRange::open( + LocalDate::of(2024, 1, 15), + LocalDate::of(2024, 1, 31) + ); + + // Act & Assert + $this->assertFalse($range1->overlaps($range2)); // 開区間では境界での接触は重なりとみなさない + $this->assertFalse($range2->overlaps($range1)); + } + + public function test_days_閉区間の日数計算(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 5); + $range = LocalDateRange::closed($from, $to); + + // Act + $days = $range->days(); + + // Assert + $this->assertSame(5, $days); // 1日から5日まで(両端含む)= 5日間 + } + + public function test_days_開区間の日数計算(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 5); + $range = LocalDateRange::open($from, $to); + + // Act + $days = $range->days(); + + // Assert + $this->assertSame(3, $days); // 1日と5日を含まない = 3日間(2日、3日、4日) + } + + public function test_days_半開区間の日数計算(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 5); + + // Act + $daysLeft = LocalDateRange::halfOpenLeft($from, $to)->days(); + $daysRight = LocalDateRange::halfOpenRight($from, $to)->days(); + + // Assert + $this->assertSame(4, $daysLeft); // 1日を含まず、5日を含む = 4日間 + $this->assertSame(4, $daysRight); // 1日を含み、5日を含まない = 4日間 + } + + public function test_iterate_閉区間での日付の反復(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 3); + $range = LocalDateRange::closed($from, $to); + + // Act + $dates = []; + foreach ($range->iterate() as $date) { + $dates[] = $date->toISOString(); + } + + // Assert + $this->assertSame(['2024-01-01', '2024-01-02', '2024-01-03'], $dates); + } + + public function test_iterate_開区間での日付の反復(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 5); + $range = LocalDateRange::open($from, $to); + + // Act + $dates = []; + foreach ($range->iterate() as $date) { + $dates[] = $date->toISOString(); + } + + // Assert + $this->assertSame(['2024-01-02', '2024-01-03', '2024-01-04'], $dates); + } + + public function test_equals_同じ範囲(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + $range1 = LocalDateRange::closed($from, $to); + $range2 = LocalDateRange::closed($from, $to); + + // Act & Assert + $this->assertTrue($range1->equals($range2)); + } + + public function test_equals_異なる範囲(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + $range1 = LocalDateRange::closed($from, $to); + $range2 = LocalDateRange::open($from, $to); + + // Act & Assert + $this->assertFalse($range1->equals($range2)); // 範囲タイプが異なる + } + + public function test_fromNullable_両方の値がnullでない場合(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + + // Act + $option = LocalDateRange::fromNullable($from, $to); + + // Assert + $this->assertTrue($option->isSome()); + $range = $option->unwrap(); + $this->assertTrue($range->getFrom()->equals($from)); + $this->assertTrue($range->getTo()->equals($to)); + } + + public function test_fromNullable_いずれかの値がnullの場合(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + + // Act + $option1 = LocalDateRange::fromNullable(null, $from); + $option2 = LocalDateRange::fromNullable($from, null); + $option3 = LocalDateRange::fromNullable(null, null); + + // Assert + $this->assertTrue($option1->isNone()); + $this->assertTrue($option2->isSome()); // fromがnullでなければ、toは自動的に最大日付になる + $this->assertTrue($option3->isNone()); + } + + public function test_jsonSerialize(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + $range = LocalDateRange::closed($from, $to); + + // Act + $json = $range->jsonSerialize(); + + // Assert + $this->assertSame('[2024-01-01, 2024-01-31]', $json); + } + + public function test_from_デフォルトは開区間(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + $to = LocalDate::of(2024, 1, 31); + + // Act + $range = LocalDateRange::from($from, $to); + + // Assert + $this->assertSame(RangeType::CLOSED, $range->getRangeType()); + $this->assertSame('[2024-01-01, 2024-01-31]', $range->toISOString()); + } + + public function test_from_to引数省略時は最大日付になる(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + + // Act + $range = LocalDateRange::from($from); + + // Assert + $this->assertSame($from, $range->getFrom()); + $this->assertSame('9999-12-31', $range->getTo()->toISOString()); + $this->assertSame(RangeType::CLOSED, $range->getRangeType()); + } + + public function test_tryFrom_to引数省略時も正常に動作(): void + { + // Arrange + $from = LocalDate::of(2024, 1, 1); + + // Act + $result = LocalDateRange::tryFrom($from); + + // Assert + $this->assertTrue($result->isOk()); + $range = $result->unwrap(); + $this->assertSame($from, $range->getFrom()); + $this->assertSame('9999-12-31', $range->getTo()->toISOString()); + } +} diff --git a/tests/Unit/DateTime/LocalDateTimeRangeTest.php b/tests/Unit/DateTime/LocalDateTimeRangeTest.php new file mode 100644 index 0000000..3eafded --- /dev/null +++ b/tests/Unit/DateTime/LocalDateTimeRangeTest.php @@ -0,0 +1,320 @@ +assertSame($from, $range->getFrom()); + $this->assertSame($to, $range->getTo()); + $this->assertSame(RangeType::CLOSED, $range->getRangeType()); + $this->assertSame('[2024-01-01T10:00, 2024-01-31T18:00]', $range->toISOString()); + } + + public function test_開区間で有効な範囲を作成できる(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + + // Act + $range = LocalDateTimeRange::open($from, $to); + + // Assert + $this->assertSame(RangeType::OPEN, $range->getRangeType()); + $this->assertSame('(2024-01-01T10:00, 2024-01-31T18:00)', $range->toISOString()); + } + + public function test_半開区間で有効な範囲を作成できる(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + + // Act + $rangeLeft = LocalDateTimeRange::halfOpenLeft($from, $to); + $rangeRight = LocalDateTimeRange::halfOpenRight($from, $to); + + // Assert + $this->assertSame(RangeType::HALF_OPEN_LEFT, $rangeLeft->getRangeType()); + $this->assertSame('(2024-01-01T10:00, 2024-01-31T18:00]', $rangeLeft->toISOString()); + $this->assertSame(RangeType::HALF_OPEN_RIGHT, $rangeRight->getRangeType()); + $this->assertSame('[2024-01-01T10:00, 2024-01-31T18:00)', $rangeRight->toISOString()); + } + + public function test_開始日時が終了日時より後の場合エラーになる(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + + // Act + $result = LocalDateTimeRange::tryFrom($from, $to); + + // Assert + $this->assertTrue($result->isErr()); + $error = $result->unwrapErr(); + $this->assertInstanceOf(ValueObjectError::class, $error); + $this->assertSame('value_object.datetime_range.invalid_range', $error->getCode()); + $this->assertSame('開始日時は終了日時以前である必要があります', $error->getMessage()); + } + + public function test_contains_閉区間の境界値を含む(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + $range = LocalDateTimeRange::closed($from, $to); + + // Act & Assert + $this->assertTrue($range->contains($from)); // 開始境界 + $this->assertTrue($range->contains($to)); // 終了境界 + $this->assertTrue($range->contains(LocalDateTime::from(new DateTimeImmutable('2024-01-15 12:00:00')))); // 中間 + $this->assertFalse($range->contains(LocalDateTime::from(new DateTimeImmutable('2023-12-31 23:59:59')))); // 範囲前 + $this->assertFalse($range->contains(LocalDateTime::from(new DateTimeImmutable('2024-02-01 00:00:00')))); // 範囲後 + } + + public function test_contains_開区間の境界値を含まない(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + $range = LocalDateTimeRange::open($from, $to); + + // Act & Assert + $this->assertFalse($range->contains($from)); // 開始境界 + $this->assertFalse($range->contains($to)); // 終了境界 + $this->assertTrue($range->contains(LocalDateTime::from(new DateTimeImmutable('2024-01-15 12:00:00')))); // 中間 + } + + public function test_contains_半開区間の境界値(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + + // Act & Assert + // 左開区間 + $rangeLeft = LocalDateTimeRange::halfOpenLeft($from, $to); + $this->assertFalse($rangeLeft->contains($from)); // 開始境界(含まない) + $this->assertTrue($rangeLeft->contains($to)); // 終了境界(含む) + + // 右開区間 + $rangeRight = LocalDateTimeRange::halfOpenRight($from, $to); + $this->assertTrue($rangeRight->contains($from)); // 開始境界(含む) + $this->assertFalse($rangeRight->contains($to)); // 終了境界(含まない) + } + + public function test_overlaps_重なりがある範囲(): void + { + // Arrange + $range1 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-15 18:00:00')) + ); + $range2 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-10 10:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-20 18:00:00')) + ); + + // Act & Assert + $this->assertTrue($range1->overlaps($range2)); + $this->assertTrue($range2->overlaps($range1)); + } + + public function test_overlaps_重なりがない範囲(): void + { + // Arrange + $range1 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-10 18:00:00')) + ); + $range2 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-20 10:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')) + ); + + // Act & Assert + $this->assertFalse($range1->overlaps($range2)); + $this->assertFalse($range2->overlaps($range1)); + } + + public function test_overlaps_境界で接する範囲_閉区間(): void + { + // Arrange + $range1 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-15 18:00:00')) + ); + $range2 = LocalDateTimeRange::closed( + LocalDateTime::from(new DateTimeImmutable('2024-01-15 18:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-31 22:00:00')) + ); + + // Act & Assert + $this->assertTrue($range1->overlaps($range2)); // 境界で接触 + $this->assertTrue($range2->overlaps($range1)); + } + + public function test_overlaps_境界で接する範囲_開区間(): void + { + // Arrange + $range1 = LocalDateTimeRange::open( + LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-15 18:00:00')) + ); + $range2 = LocalDateTimeRange::open( + LocalDateTime::from(new DateTimeImmutable('2024-01-15 18:00:00')), + LocalDateTime::from(new DateTimeImmutable('2024-01-31 22:00:00')) + ); + + // Act & Assert + $this->assertFalse($range1->overlaps($range2)); // 開区間では境界での接触は重なりとみなさない + $this->assertFalse($range2->overlaps($range1)); + } + + public function test_duration_計算(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-02 10:00:00')); + $range = LocalDateTimeRange::closed($from, $to); + + // Act & Assert + $this->assertSame(86400, $range->durationInSeconds()); // 24時間 + $this->assertSame(1440.0, $range->durationInMinutes()); // 24時間 * 60分 + $this->assertSame(24.0, $range->durationInHours()); // 24時間 + $this->assertSame(1.0, $range->durationInDays()); // 1日 + } + + public function test_equals_同じ範囲(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + $range1 = LocalDateTimeRange::closed($from, $to); + $range2 = LocalDateTimeRange::closed($from, $to); + + // Act & Assert + $this->assertTrue($range1->equals($range2)); + } + + public function test_equals_異なる範囲(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + $range1 = LocalDateTimeRange::closed($from, $to); + $range2 = LocalDateTimeRange::open($from, $to); + + // Act & Assert + $this->assertFalse($range1->equals($range2)); // 範囲タイプが異なる + } + + public function test_fromNullable_両方の値がnullでない場合(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + + // Act + $option = LocalDateTimeRange::fromNullable($from, $to); + + // Assert + $this->assertTrue($option->isSome()); + $range = $option->unwrap(); + $this->assertTrue($range->getFrom()->equals($from)); + $this->assertTrue($range->getTo()->equals($to)); + } + + public function test_fromNullable_いずれかの値がnullの場合(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + + // Act + $option1 = LocalDateTimeRange::fromNullable(null, $from); + $option2 = LocalDateTimeRange::fromNullable($from, null); + $option3 = LocalDateTimeRange::fromNullable(null, null); + + // Assert + $this->assertTrue($option1->isNone()); + $this->assertTrue($option2->isSome()); // fromがnullでなければ、toは自動的に最大日時になる + $this->assertTrue($option3->isNone()); + } + + public function test_jsonSerialize(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + $range = LocalDateTimeRange::closed($from, $to); + + // Act + $json = $range->jsonSerialize(); + + // Assert + $this->assertSame('[2024-01-01T10:00, 2024-01-31T18:00]', $json); + } + + public function test_from_デフォルトは閉区間(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + $to = LocalDateTime::from(new DateTimeImmutable('2024-01-31 18:00:00')); + + // Act + $range = LocalDateTimeRange::from($from, $to); + + // Assert + $this->assertSame(RangeType::CLOSED, $range->getRangeType()); + $this->assertSame('[2024-01-01T10:00, 2024-01-31T18:00]', $range->toISOString()); + } + + public function test_from_to引数省略時は最大日時になる(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + + // Act + $range = LocalDateTimeRange::from($from); + + // Assert + $this->assertSame($from, $range->getFrom()); + $this->assertSame('9999-12-31T23:59:59', $range->getTo()->toISOString()); + $this->assertSame(RangeType::CLOSED, $range->getRangeType()); + } + + public function test_tryFrom_to引数省略時も正常に動作(): void + { + // Arrange + $from = LocalDateTime::from(new DateTimeImmutable('2024-01-01 10:00:00')); + + // Act + $result = LocalDateTimeRange::tryFrom($from); + + // Assert + $this->assertTrue($result->isOk()); + $range = $result->unwrap(); + $this->assertSame($from, $range->getFrom()); + $this->assertSame('9999-12-31T23:59:59', $range->getTo()->toISOString()); + } +} diff --git a/tests/Unit/Enum/EnumValueTest.php b/tests/Unit/Enum/EnumValueTest.php index a6b0ec3..0e9edba 100644 --- a/tests/Unit/Enum/EnumValueTest.php +++ b/tests/Unit/Enum/EnumValueTest.php @@ -66,6 +66,7 @@ public function tryFrom2メソッドで無効な値はエラーになる(): void $result = TestEnumValue::tryFrom2('InvalidValue'); $this->assertFalse($result->isOk()); + // @phpstan-ignore method.alreadyNarrowedType $this->assertTrue($result->isErr()); $error = $result->unwrapErr();