From ea5bae8bfeda3259b405555e61289c528e5eceee Mon Sep 17 00:00:00 2001
From: kakiuchi-shigenao <kakiuchi-shigenao@wiznet.co.jp>
Date: Wed, 11 Jun 2025 17:24:58 +0900
Subject: [PATCH 1/2] =?UTF-8?q?feat:=20LocalDateRange=E3=81=A8LocalDateTim?=
 =?UTF-8?q?eRange=E3=82=AF=E3=83=A9=E3=82=B9=E3=82=92=E5=AE=9F=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- LocalDateRange: 日付の範囲を表す値オブジェクト
- LocalDateTimeRange: 日時の範囲を表す値オブジェクト
- RangeType: 範囲の境界タイプを表す列挙型(閉区間、開区間、半開区間)
- 範囲の有効性検証、包含判定、重なり判定などの機能を実装
- php-monadを本番依存に移動(Result/Option型の利用のため)

Fixes #29

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
---
 composer.json                                 |   4 +-
 composer.lock                                 | 296 +++++++-------
 examples/DateTime/TestLocalDateRange.php      | 124 ++++++
 examples/DateTime/TestLocalDateTimeRange.php  |  97 +++++
 src/DateTime/LocalDateRange.php               | 319 +++++++++++++++
 src/DateTime/LocalDateTimeRange.php           | 320 +++++++++++++++
 src/DateTime/RangeType.php                    |  16 +
 tests/Unit/DateTime/LocalDateRangeTest.php    | 382 ++++++++++++++++++
 .../Unit/DateTime/LocalDateTimeRangeTest.php  | 320 +++++++++++++++
 9 files changed, 1735 insertions(+), 143 deletions(-)
 create mode 100644 examples/DateTime/TestLocalDateRange.php
 create mode 100644 examples/DateTime/TestLocalDateTimeRange.php
 create mode 100644 src/DateTime/LocalDateRange.php
 create mode 100644 src/DateTime/LocalDateTimeRange.php
 create mode 100644 src/DateTime/RangeType.php
 create mode 100644 tests/Unit/DateTime/LocalDateRangeTest.php
 create mode 100644 tests/Unit/DateTime/LocalDateTimeRangeTest.php

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 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WizDevelop\PhpValueObject\Examples\DateTime;
+
+use WizDevelop\PhpValueObject\DateTime\LocalDate;
+use WizDevelop\PhpValueObject\DateTime\LocalDateRange;
+
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+// 1. 基本的な使用例:月間の範囲
+echo "=== 基本的な使用例 ===\n";
+$startOfMonth = LocalDate::of(2024, 1, 1);
+$endOfMonth = LocalDate::of(2024, 1, 31);
+$january = LocalDateRange::closed($startOfMonth, $endOfMonth);
+
+echo "1月の期間: {$january->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 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WizDevelop\PhpValueObject\Examples\DateTime;
+
+use DateTimeImmutable;
+use DateTimeZone;
+use WizDevelop\PhpValueObject\DateTime\LocalDateTime;
+use WizDevelop\PhpValueObject\DateTime\LocalDateTimeRange;
+
+require_once __DIR__ . '/../../vendor/autoload.php';
+
+// 1. 基本的な使用例:閉区間での範囲作成
+echo "=== 基本的な使用例 ===\n";
+$start = LocalDateTime::from(new DateTimeImmutable('2024-01-01 09:00:00'));
+$end = LocalDateTime::from(new DateTimeImmutable('2024-01-01 18:00:00'));
+$workingHours = LocalDateTimeRange::closed($start, $end);
+
+echo "営業時間: {$workingHours->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 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WizDevelop\PhpValueObject\DateTime;
+
+use Generator;
+use Override;
+use Stringable;
+use WizDevelop\PhpMonad\Option;
+use WizDevelop\PhpMonad\Result;
+use WizDevelop\PhpValueObject\Error\ValueObjectError;
+use WizDevelop\PhpValueObject\IValueObject;
+use WizDevelop\PhpValueObject\ValueObjectMeta;
+
+/**
+ * ローカル日付範囲を表す値オブジェクト
+ */
+#[ValueObjectMeta(name: 'ローカル日付範囲')]
+readonly class LocalDateRange implements IValueObject, Stringable
+{
+    /**
+     * 最大日付(9999-12-31)
+     */
+    private const MAX_DATE_YEAR = 9999;
+    private const MAX_DATE_MONTH = 12;
+    private const MAX_DATE_DAY = 31;
+
+    /**
+     * Avoid new() operator.
+     */
+    final private function __construct(
+        private LocalDate $from,
+        private LocalDate $to,
+        private RangeType $rangeType
+    ) {
+        // NOTE: 不変条件(invariant)
+        assert(static::isValid($from, $to)->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<static,ValueObjectError>
+     */
+    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<static>
+     */
+    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<Option<static>,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<bool,ValueObjectError>
+     */
+    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<LocalDate>
+     */
+    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 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WizDevelop\PhpValueObject\DateTime;
+
+use Override;
+use Stringable;
+use WizDevelop\PhpMonad\Option;
+use WizDevelop\PhpMonad\Result;
+use WizDevelop\PhpValueObject\Error\ValueObjectError;
+use WizDevelop\PhpValueObject\IValueObject;
+use WizDevelop\PhpValueObject\ValueObjectMeta;
+
+/**
+ * ローカル日時範囲を表す値オブジェクト
+ */
+#[ValueObjectMeta(name: 'ローカル日時範囲')]
+readonly class LocalDateTimeRange implements IValueObject, Stringable
+{
+    /**
+     * 最大日時(9999-12-31 23:59:59)
+     */
+    private const MAX_DATE_YEAR = 9999;
+    private const MAX_DATE_MONTH = 12;
+    private const MAX_DATE_DAY = 31;
+    private const MAX_TIME_HOUR = 23;
+    private const MAX_TIME_MINUTE = 59;
+    private const MAX_TIME_SECOND = 59;
+
+    /**
+     * Avoid new() operator.
+     */
+    final private function __construct(
+        private LocalDateTime $from,
+        private LocalDateTime $to,
+        private RangeType $rangeType
+    ) {
+        // NOTE: 不変条件(invariant)
+        assert(static::isValid($from, $to)->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<static,ValueObjectError>
+     */
+    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<static>
+     */
+    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<Option<static>,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<bool,ValueObjectError>
+     */
+    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 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WizDevelop\PhpValueObject\DateTime;
+
+/**
+ * 期間の境界タイプを表す列挙型
+ */
+enum RangeType: string
+{
+    case CLOSED = 'closed';              // [from, to] 閉区間
+    case OPEN = 'open';                  // (from, to) 開区間
+    case HALF_OPEN_LEFT = 'half_open_left';   // (from, to] 左開区間
+    case HALF_OPEN_RIGHT = 'half_open_right'; // [from, to) 右開区間
+}
\ No newline at end of file
diff --git a/tests/Unit/DateTime/LocalDateRangeTest.php b/tests/Unit/DateTime/LocalDateRangeTest.php
new file mode 100644
index 0000000..b28a2e1
--- /dev/null
+++ b/tests/Unit/DateTime/LocalDateRangeTest.php
@@ -0,0 +1,382 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WizDevelop\PhpValueObject\Tests\Unit\DateTime;
+
+use PHPUnit\Framework\TestCase;
+use WizDevelop\PhpValueObject\DateTime\LocalDate;
+use WizDevelop\PhpValueObject\DateTime\LocalDateRange;
+use WizDevelop\PhpValueObject\DateTime\RangeType;
+use WizDevelop\PhpValueObject\Error\ValueObjectError;
+
+final class LocalDateRangeTest extends TestCase
+{
+    public function test_閉区間で有効な範囲を作成できる(): void
+    {
+        // Arrange
+        $from = LocalDate::of(2024, 1, 1);
+        $to = LocalDate::of(2024, 1, 31);
+
+        // Act
+        $range = LocalDateRange::closed($from, $to);
+
+        // Assert
+        $this->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 @@
+<?php
+
+declare(strict_types=1);
+
+namespace WizDevelop\PhpValueObject\Tests\Unit\DateTime;
+
+use DateTimeImmutable;
+use PHPUnit\Framework\TestCase;
+use WizDevelop\PhpValueObject\DateTime\LocalDateTime;
+use WizDevelop\PhpValueObject\DateTime\LocalDateTimeRange;
+use WizDevelop\PhpValueObject\DateTime\RangeType;
+use WizDevelop\PhpValueObject\Error\ValueObjectError;
+
+final class LocalDateTimeRangeTest extends TestCase
+{
+    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::closed($from, $to);
+
+        // Assert
+        $this->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());
+    }
+}

From 0f7678c2ce9d90b2b984c517c015088c42aceb68 Mon Sep 17 00:00:00 2001
From: kakiuchi-shigenao <kakiuchi-shigenao@wiznet.co.jp>
Date: Wed, 11 Jun 2025 17:33:11 +0900
Subject: [PATCH 2/2] =?UTF-8?q?test:=20tryFrom2=E3=83=A1=E3=82=BD=E3=83=83?=
 =?UTF-8?q?=E3=83=89=E3=81=AE=E7=84=A1=E5=8A=B9=E3=81=AA=E5=80=A4=E3=81=AB?=
 =?UTF-8?q?=E5=AF=BE=E3=81=99=E3=82=8B=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=81?=
 =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 tests/Unit/Enum/EnumValueTest.php | 1 +
 1 file changed, 1 insertion(+)

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();