From d90b10ec76b402bf4f62a0c1e319e94b2143c209 Mon Sep 17 00:00:00 2001 From: kakiuchi-shigenao Date: Tue, 17 Jun 2025 12:58:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E3=81=93=E3=81=AE=E3=83=AA=E3=83=9D?= =?UTF-8?q?=E3=82=B8=E3=83=88=E3=83=AA=E3=81=AELocalDateRange=E3=82=92?= =?UTF-8?q?=E5=8F=82=E8=80=83=E3=81=AB=E3=81=97=E3=81=A6=E3=80=81IntegerRa?= =?UTF-8?q?nge=E3=82=92=E5=AE=9F=E8=A3=85=E3=81=97=E3=81=A6=E3=81=8F?= =?UTF-8?q?=E3=81=A0=E3=81=95=E3=81=84=E3=80=82=20=20=20=E4=B8=80=E8=88=AC?= =?UTF-8?q?=E7=9A=84=E3=81=AAIntegerRange=E3=81=AE=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=81=A7=E3=81=8A=E9=A1=98=E3=81=84=E3=81=84=E3=81=9F=E3=81=97?= =?UTF-8?q?=E3=81=BE=E3=81=99=E3=80=82=EF=BC=88=E3=81=82=E3=81=BE=E3=82=8A?= =?UTF-8?q?=E3=83=AA=E3=83=83=E3=83=81=E3=81=AA=E5=85=AC=E9=96=8B=E3=83=A1?= =?UTF-8?q?=E3=82=BD=E3=83=83=E3=83=89=E3=81=AF=E3=81=84=E3=82=89=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=81=A7=E3=81=99=E3=80=82=E6=9C=80=E4=BD=8E=E9=99=90?= =?UTF-8?q?=E3=81=AE=E5=85=AC=E9=96=8B=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89?= =?UTF-8?q?=E3=81=A7=20=20=20=E3=81=8B=E3=81=BE=E3=81=84=E3=81=BE=E3=81=9B?= =?UTF-8?q?=E3=82=93=EF=BC=89=20=20=20=E5=AE=9F=E8=A3=85=E3=81=AE=E4=BB=95?= =?UTF-8?q?=E6=96=B9=E3=81=AB=E3=81=A4=E3=81=84=E3=81=A6=E3=81=AF=E6=97=A2?= =?UTF-8?q?=E5=AD=98=E3=81=AE=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E5=85=A5?= =?UTF-8?q?=E5=BF=B5=E3=81=AB=E8=AA=BF=E6=9F=BB=E3=81=97=E3=81=A6=E3=80=81?= =?UTF-8?q?=E5=90=8C=E3=81=98=E3=82=88=E3=81=86=E3=81=AB=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=81=8F=E3=81=A0=E3=81=95=E3=81=84=E3=80=82?= =?UTF-8?q?=20=20=20=E9=81=A0=E6=85=AE=E3=81=9B=E3=81=9A=E5=85=A8=E5=8A=9B?= =?UTF-8?q?=E3=82=92=E5=B0=BD=E3=81=8F=E3=81=97=E3=81=A6=E3=81=8F=E3=81=A0?= =?UTF-8?q?=E3=81=95=E3=81=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ● IntegerRangeクラスの実装が完了しました。LocalDateRangeを参考にして、以下の機能を持つIntegerRangeクラスを実装しました : 1. 基本機能: - 閉区間、開区間、半開区間(左開・右開)の4種類の範囲タイプをサポート - 範囲の妥当性検証(開始値≤終了値) - 文字列表現(例:[1, 10]、(1, 10)) 2. 主な公開メソッド: - contains(): 指定した整数が範囲内に含まれるかチェック - overlaps(): 他の範囲との重なりチェック - count(): 範囲内の整数の個数を取得 - iterate(): 範囲内の整数を順に返すジェネレータ - equals(): 等価性チェック 3. ファクトリメソッド: - from(), closed(), open(), halfOpenLeft(), halfOpenRight() - tryFrom(): エラーハンドリング対応 - fromNullable(), tryFromNullable(): null対応 4. その他: - 負の整数範囲もサポート - JSON変換機能 - PHPUnit によるテストコード(31のテストケース) - サンプルコード付き 実装は既存のLocalDateRangeクラスと同じスタイルで行い、テストも全て通過しています。 --- examples/Number/TestIntegerRange.php | 128 ++++++++ src/Number/IntegerRange.php | 346 ++++++++++++++++++++ tests/Unit/Number/IntegerRangeTest.php | 437 +++++++++++++++++++++++++ 3 files changed, 911 insertions(+) create mode 100644 examples/Number/TestIntegerRange.php create mode 100644 src/Number/IntegerRange.php create mode 100644 tests/Unit/Number/IntegerRangeTest.php diff --git a/examples/Number/TestIntegerRange.php b/examples/Number/TestIntegerRange.php new file mode 100644 index 0000000..e28179d --- /dev/null +++ b/examples/Number/TestIntegerRange.php @@ -0,0 +1,128 @@ +count()} 個\n\n"; + +// 2. 開区間と閉区間の違い +echo "=== 開区間と閉区間の違い ===\n"; +$closedRange = IntegerRange::closed(1, 5); +$openRange = IntegerRange::open(1, 5); + +echo "閉区間(両端含む): {$closedRange} = {$closedRange->count()} 個\n"; +echo "開区間(両端含まない): {$openRange} = {$openRange->count()} 個\n\n"; + +// 3. 半開区間の使用例(配列のインデックス範囲など) +echo "=== 半開区間の使用例 ===\n"; +// 配列のインデックス範囲(0から始まり、長さを終端とする) +$arrayIndexRange = IntegerRange::halfOpenRight(0, 10); + +echo "配列インデックス範囲: {$arrayIndexRange}\n"; +echo '0を含む: ' . ($arrayIndexRange->contains(0) ? 'はい' : 'いいえ') . "\n"; +echo '9を含む: ' . ($arrayIndexRange->contains(9) ? 'はい' : 'いいえ') . "\n"; +echo '10を含む: ' . ($arrayIndexRange->contains(10) ? 'はい' : 'いいえ') . "\n\n"; + +// 4. 整数の反復処理 +echo "=== 整数の反復処理 ===\n"; +$smallRange = IntegerRange::closed(1, 5); + +echo "1から5の整数:\n"; +foreach ($smallRange->iterate() as $value) { + echo " - {$value}\n"; +} +echo "\n"; + +// 5. 範囲の重なり判定 +echo "=== 範囲の重なり判定 ===\n"; +$range1to5 = IntegerRange::closed(1, 5); +$range3to8 = IntegerRange::closed(3, 8); +$range10to15 = IntegerRange::closed(10, 15); + +echo "範囲1: {$range1to5}\n"; +echo "範囲2: {$range3to8}\n"; +echo "範囲3: {$range10to15}\n"; +echo '範囲1と範囲2が重なる: ' . ($range1to5->overlaps($range3to8) ? 'はい' : 'いいえ') . "\n"; +echo '範囲1と範囲3が重なる: ' . ($range1to5->overlaps($range10to15) ? 'はい' : 'いいえ') . "\n\n"; + +// 6. 特定の整数が範囲内かチェック +echo "=== 範囲内チェック ===\n"; +$scoreRange = IntegerRange::closed(0, 100); +$testScores = [50, 100, 101, -1]; + +echo "有効なスコア範囲: {$scoreRange}\n"; +foreach ($testScores as $score) { + echo "{$score} は有効なスコア: " . ($scoreRange->contains($score) ? 'はい' : 'いいえ') . "\n"; +} +echo "\n"; + +// 7. エラーハンドリング +echo "=== エラーハンドリング ===\n"; +$invalidResult = IntegerRange::tryFrom(10, 5); + +if ($invalidResult->isErr()) { + $error = $invalidResult->unwrapErr(); + echo "エラー: {$error->getMessage()}\n"; + echo "エラーコード: {$error->getCode()}\n"; +} + +// 8. nullable対応 +echo "\n=== Nullable対応 ===\n"; +$start = 1; +$end = null; + +$optionRange = IntegerRange::fromNullable($start, $end); +if ($optionRange->isNone()) { + echo "範囲を作成できませんでした(いずれかの値がnullです)\n"; +} + +// 9. 負の整数範囲 +echo "\n=== 負の整数範囲 ===\n"; +$negativeRange = IntegerRange::closed(-10, -1); + +echo "負の整数範囲: {$negativeRange}\n"; +echo "要素数: {$negativeRange->count()} 個\n"; +echo '-5を含む: ' . ($negativeRange->contains(-5) ? 'はい' : 'いいえ') . "\n\n"; + +// 10. ゼロを含む範囲 +echo "=== ゼロを含む範囲 ===\n"; +$zeroIncludedRange = IntegerRange::closed(-5, 5); + +echo "範囲: {$zeroIncludedRange}\n"; +echo '0を含む: ' . ($zeroIncludedRange->contains(0) ? 'はい' : 'いいえ') . "\n"; +echo "要素数: {$zeroIncludedRange->count()} 個\n\n"; + +// 11. ページネーションの例 +echo "=== ページネーションの例 ===\n"; +$totalItems = 95; +$itemsPerPage = 10; +$totalPages = (int)ceil($totalItems / $itemsPerPage); + +$pageRange = IntegerRange::closed(1, $totalPages); +echo "ページ範囲: {$pageRange}\n"; + +$currentPage = 5; +$itemStartIndex = ($currentPage - 1) * $itemsPerPage; +$itemEndIndex = min($itemStartIndex + $itemsPerPage - 1, $totalItems - 1); +$itemRange = IntegerRange::closed($itemStartIndex, $itemEndIndex); + +echo "ページ{$currentPage}のアイテムインデックス範囲: {$itemRange}\n"; +echo "表示アイテム数: {$itemRange->count()} 個\n\n"; + +// 12. JSON変換 +echo "=== JSON変換 ===\n"; +$jsonRange = IntegerRange::halfOpenRight(0, 100); +$json = json_encode($jsonRange); + +echo "範囲: {$jsonRange}\n"; +echo "JSON: {$json}\n"; diff --git a/src/Number/IntegerRange.php b/src/Number/IntegerRange.php new file mode 100644 index 0000000..eb2b797 --- /dev/null +++ b/src/Number/IntegerRange.php @@ -0,0 +1,346 @@ +isOk()); + } + + /** + * 指定された整数値から範囲を生成する + * + * @param int $from 開始値 + * @param int $to 終了値 + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + */ + public static function from(int $from, int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): self + { + return new self($from, $to, $rangeType); + } + + /** + * 閉区間 [from, to] を生成する + * + * @param int $from 開始値 + * @param int $to 終了値 + */ + public static function closed(int $from, int $to): self + { + return new self($from, $to, RangeType::CLOSED); + } + + /** + * 開区間 (from, to) を生成する + * + * @param int $from 開始値 + * @param int $to 終了値 + */ + public static function open(int $from, int $to): self + { + return new self($from, $to, RangeType::OPEN); + } + + /** + * 左開区間 (from, to] を生成する + * + * @param int $from 開始値 + * @param int $to 終了値 + */ + public static function halfOpenLeft(int $from, int $to): self + { + return new self($from, $to, RangeType::HALF_OPEN_LEFT); + } + + /** + * 右開区間 [from, to) を生成する + * + * @param int $from 開始値 + * @param int $to 終了値 + */ + public static function halfOpenRight(int $from, int $to): self + { + return new self($from, $to, RangeType::HALF_OPEN_RIGHT); + } + + /** + * 指定された整数値から範囲を生成する(エラーハンドリング付き) + * + * @param int $from 開始値 + * @param int $to 終了値 + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + * @return Result + */ + public static function tryFrom(int $from, int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Result + { + $validationResult = self::isValid($from, $to); + if ($validationResult->isErr()) { + return $validationResult; + } + + return Result\ok(new self($from, $to, $rangeType)); + } + + /** + * 指定された整数値から範囲を生成する(null許容) + * + * @param int|null $from 開始値 + * @param int|null $to 終了値 + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + * @return Option + */ + public static function fromNullable(?int $from, ?int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Option + { + if ($from === null || $to === null) { + return Option\none(); + } + + return Option\some(new self($from, $to, $rangeType)); + } + + /** + * 指定された整数値から範囲を生成する(null許容、エラーハンドリング付き) + * + * @param int|null $from 開始値 + * @param int|null $to 終了値 + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + * @return Result, ValueObjectError> + */ + public static function tryFromNullable(?int $from, ?int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Result + { + if ($from === null || $to === null) { + // @phpstan-ignore-next-line + return Result\ok(Option\none()); + } + + // @phpstan-ignore-next-line + return self::tryFrom($from, $to, $rangeType)->map( + static fn (self $range) => Option\some($range) + ); + } + + /** + * 開始値と終了値の妥当性を検証する + * + * @param int $from 開始値 + * @param int $to 終了値 + * @return Result + */ + private static function isValid(int $from, int $to): Result + { + if ($from > $to) { + return Result\err(ValueObjectError::of( + code: 'value_object.integer_range.invalid_range', + message: '開始値は終了値以下である必要があります' + )); + } + + return Result\ok(true); + } + + /** + * 開始値を取得する + */ + public function getFrom(): int + { + return $this->from; + } + + /** + * 終了値を取得する + */ + public function getTo(): int + { + return $this->to; + } + + /** + * 範囲タイプを取得する + */ + public function getRangeType(): RangeType + { + return $this->rangeType; + } + + /** + * 指定された整数が範囲内に含まれるかを判定する + * + * @param int $value 判定する整数 + */ + public function contains(int $value): bool + { + return match ($this->rangeType) { + RangeType::CLOSED => $this->from <= $value && $value <= $this->to, + RangeType::OPEN => $this->from < $value && $value < $this->to, + RangeType::HALF_OPEN_LEFT => $this->from < $value && $value <= $this->to, + RangeType::HALF_OPEN_RIGHT => $this->from <= $value && $value < $this->to, + }; + } + + /** + * 他の範囲と重なりがあるかを判定する + * + * @param IntegerRange $other 比較対象の範囲 + */ + public function overlaps(self $other): bool + { + // 明らかに重ならない場合 + if ($this->strictlyBefore($other) || $other->strictlyBefore($this)) { + return false; + } + + // 境界での重なりを判定 + return $this->hasOverlapAt($other); + } + + /** + * この範囲が他の範囲よりも完全に前にあるかを判定する + * + * @param IntegerRange $other 比較対象の範囲 + */ + private function strictlyBefore(self $other): bool + { + return $this->to < $other->from || ($this->to === $other->from && !$this->hasOverlapAt($other)); + } + + /** + * 境界での重なりがあるかを判定する + * + * @param IntegerRange $other 比較対象の範囲 + */ + private function hasOverlapAt(self $other): bool + { + // 共通の値が存在するかチェック + $maxFrom = max($this->from, $other->from); + $minTo = min($this->to, $other->to); + + if ($maxFrom > $minTo) { + return false; + } + + // 境界値での包含を確認 + for ($value = $maxFrom; $value <= $minTo; ++$value) { + if ($this->contains($value) && $other->contains($value)) { + return true; + } + } + + return false; + } + + /** + * 範囲内の整数の個数を取得する + */ + public function count(): int + { + if ($this->from > $this->to) { + return 0; + } + + $baseCount = $this->to - $this->from + 1; + + return match ($this->rangeType) { + RangeType::CLOSED => $baseCount, + RangeType::OPEN => max(0, $baseCount - 2), + RangeType::HALF_OPEN_LEFT => max(0, $baseCount - 1), + RangeType::HALF_OPEN_RIGHT => max(0, $baseCount - 1), + }; + } + + /** + * 範囲内の整数を順に返すジェネレータを取得する + * + * @return Generator + */ + public function iterate(): Generator + { + $start = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_RIGHT => $this->from, + RangeType::OPEN, RangeType::HALF_OPEN_LEFT => $this->from + 1, + }; + + $end = match ($this->rangeType) { + RangeType::CLOSED, RangeType::HALF_OPEN_LEFT => $this->to, + RangeType::OPEN, RangeType::HALF_OPEN_RIGHT => $this->to - 1, + }; + + for ($i = $start; $i <= $end; ++$i) { + yield $i; + } + } + + /** + * 文字列表現を取得する + */ + #[Override] + public function __toString(): 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 "{$leftBracket}{$this->from}, {$this->to}{$rightBracket}"; + } + + /** + * JSON表現用のデータを取得する + * + * @return array{from: int, to: int, rangeType: string} + */ + #[Override] + public function jsonSerialize(): array + { + return [ + 'from' => $this->from, + 'to' => $this->to, + 'rangeType' => $this->rangeType->value, + ]; + } + + /** + * 他の値オブジェクトと等価かを判定する + * + * @param IValueObject $other 比較対象 + */ + public function equals(IValueObject $other): bool + { + return $other instanceof self + && $this->from === $other->from + && $this->to === $other->to + && $this->rangeType === $other->rangeType; + } +} diff --git a/tests/Unit/Number/IntegerRangeTest.php b/tests/Unit/Number/IntegerRangeTest.php new file mode 100644 index 0000000..b53eb05 --- /dev/null +++ b/tests/Unit/Number/IntegerRangeTest.php @@ -0,0 +1,437 @@ +assertSame($from, $range->getFrom()); + $this->assertSame($to, $range->getTo()); + $this->assertSame(RangeType::CLOSED, $range->getRangeType()); + $this->assertSame('[1, 10]', (string)$range); + } + + public function test_開区間で有効な範囲を作成できる(): void + { + // Arrange + $from = 1; + $to = 10; + + // Act + $range = IntegerRange::open($from, $to); + + // Assert + $this->assertSame(RangeType::OPEN, $range->getRangeType()); + $this->assertSame('(1, 10)', (string)$range); + } + + public function test_半開区間で有効な範囲を作成できる(): void + { + // Arrange + $from = 1; + $to = 10; + + // Act + $rangeLeft = IntegerRange::halfOpenLeft($from, $to); + $rangeRight = IntegerRange::halfOpenRight($from, $to); + + // Assert + $this->assertSame(RangeType::HALF_OPEN_LEFT, $rangeLeft->getRangeType()); + $this->assertSame('(1, 10]', (string)$rangeLeft); + $this->assertSame(RangeType::HALF_OPEN_RIGHT, $rangeRight->getRangeType()); + $this->assertSame('[1, 10)', (string)$rangeRight); + } + + public function test_開始値が終了値より大きい場合エラーになる(): void + { + // Arrange + $from = 10; + $to = 5; + + // Act + $result = IntegerRange::tryFrom($from, $to); + + // Assert + $this->assertTrue($result->isErr()); + $error = $result->unwrapErr(); + $this->assertInstanceOf(ValueObjectError::class, $error); + $this->assertSame('value_object.integer_range.invalid_range', $error->getCode()); + $this->assertSame('開始値は終了値以下である必要があります', $error->getMessage()); + } + + public function test_contains_閉区間の境界値を含む(): void + { + // Arrange + $from = 1; + $to = 10; + $range = IntegerRange::closed($from, $to); + + // Act & Assert + $this->assertTrue($range->contains($from)); // 開始境界 + $this->assertTrue($range->contains($to)); // 終了境界 + $this->assertTrue($range->contains(5)); // 中間 + $this->assertFalse($range->contains(0)); // 範囲前 + $this->assertFalse($range->contains(11)); // 範囲後 + } + + public function test_contains_開区間の境界値を含まない(): void + { + // Arrange + $from = 1; + $to = 10; + $range = IntegerRange::open($from, $to); + + // Act & Assert + $this->assertFalse($range->contains($from)); // 開始境界 + $this->assertFalse($range->contains($to)); // 終了境界 + $this->assertTrue($range->contains(5)); // 中間 + $this->assertFalse($range->contains(0)); // 範囲前 + $this->assertFalse($range->contains(11)); // 範囲後 + } + + public function test_contains_半開区間の境界値(): void + { + // Arrange + $from = 1; + $to = 10; + + // Act & Assert + // 左開区間 + $rangeLeft = IntegerRange::halfOpenLeft($from, $to); + $this->assertFalse($rangeLeft->contains($from)); // 開始境界(含まない) + $this->assertTrue($rangeLeft->contains($to)); // 終了境界(含む) + + // 右開区間 + $rangeRight = IntegerRange::halfOpenRight($from, $to); + $this->assertTrue($rangeRight->contains($from)); // 開始境界(含む) + $this->assertFalse($rangeRight->contains($to)); // 終了境界(含まない) + } + + public function test_overlaps_重なりがある範囲(): void + { + // Arrange + $range1 = IntegerRange::closed(1, 5); + $range2 = IntegerRange::closed(3, 8); + + // Act & Assert + $this->assertTrue($range1->overlaps($range2)); + $this->assertTrue($range2->overlaps($range1)); + } + + public function test_overlaps_重なりがない範囲(): void + { + // Arrange + $range1 = IntegerRange::closed(1, 5); + $range2 = IntegerRange::closed(10, 15); + + // Act & Assert + $this->assertFalse($range1->overlaps($range2)); + $this->assertFalse($range2->overlaps($range1)); + } + + public function test_overlaps_境界で接する範囲_閉区間(): void + { + // Arrange + $range1 = IntegerRange::closed(1, 5); + $range2 = IntegerRange::closed(5, 10); + + // Act & Assert + $this->assertTrue($range1->overlaps($range2)); // 境界で接触 + $this->assertTrue($range2->overlaps($range1)); + } + + public function test_overlaps_境界で接する範囲_開区間(): void + { + // Arrange + $range1 = IntegerRange::open(1, 5); + $range2 = IntegerRange::open(5, 10); + + // Act & Assert + $this->assertFalse($range1->overlaps($range2)); // 開区間では境界での接触は重なりとみなさない + $this->assertFalse($range2->overlaps($range1)); + } + + public function test_overlaps_境界で接する範囲_半開区間(): void + { + // Arrange + $range1 = IntegerRange::halfOpenRight(1, 5); // [1, 5) + $range2 = IntegerRange::halfOpenLeft(5, 10); // (5, 10] + + // Act & Assert + $this->assertFalse($range1->overlaps($range2)); // 5は片方にしか含まれない + $this->assertFalse($range2->overlaps($range1)); + } + + public function test_count_閉区間の要素数計算(): void + { + // Arrange + $from = 1; + $to = 5; + $range = IntegerRange::closed($from, $to); + + // Act + $count = $range->count(); + + // Assert + $this->assertSame(5, $count); // 1から5まで(両端含む)= 5個 + } + + public function test_count_開区間の要素数計算(): void + { + // Arrange + $from = 1; + $to = 5; + $range = IntegerRange::open($from, $to); + + // Act + $count = $range->count(); + + // Assert + $this->assertSame(3, $count); // 1と5を含まない = 3個(2、3、4) + } + + public function test_count_半開区間の要素数計算(): void + { + // Arrange + $from = 1; + $to = 5; + + // Act + $countLeft = IntegerRange::halfOpenLeft($from, $to)->count(); + $countRight = IntegerRange::halfOpenRight($from, $to)->count(); + + // Assert + $this->assertSame(4, $countLeft); // 1を含まず、5を含む = 4個 + $this->assertSame(4, $countRight); // 1を含み、5を含まない = 4個 + } + + public function test_count_境界値が同じ場合(): void + { + // Arrange & Act & Assert + $this->assertSame(1, IntegerRange::closed(5, 5)->count()); // [5, 5] = 1個 + $this->assertSame(0, IntegerRange::open(5, 5)->count()); // (5, 5) = 0個 + $this->assertSame(0, IntegerRange::halfOpenLeft(5, 5)->count()); // (5, 5] = 0個 + $this->assertSame(0, IntegerRange::halfOpenRight(5, 5)->count()); // [5, 5) = 0個 + } + + public function test_count_開区間で要素数が少ない場合(): void + { + // Arrange & Act & Assert + $this->assertSame(0, IntegerRange::open(1, 2)->count()); // (1, 2) = 0個 + $this->assertSame(1, IntegerRange::open(1, 3)->count()); // (1, 3) = 1個(2のみ) + } + + public function test_iterate_閉区間での整数の反復(): void + { + // Arrange + $from = 1; + $to = 3; + $range = IntegerRange::closed($from, $to); + + // Act + $values = []; + foreach ($range->iterate() as $value) { + $values[] = $value; + } + + // Assert + $this->assertSame([1, 2, 3], $values); + } + + public function test_iterate_開区間での整数の反復(): void + { + // Arrange + $from = 1; + $to = 5; + $range = IntegerRange::open($from, $to); + + // Act + $values = []; + foreach ($range->iterate() as $value) { + $values[] = $value; + } + + // Assert + $this->assertSame([2, 3, 4], $values); + } + + public function test_iterate_半開区間での整数の反復(): void + { + // Arrange + $from = 1; + $to = 5; + + // Act + $valuesLeft = []; + foreach (IntegerRange::halfOpenLeft($from, $to)->iterate() as $value) { + $valuesLeft[] = $value; + } + + $valuesRight = []; + foreach (IntegerRange::halfOpenRight($from, $to)->iterate() as $value) { + $valuesRight[] = $value; + } + + // Assert + $this->assertSame([2, 3, 4, 5], $valuesLeft); + $this->assertSame([1, 2, 3, 4], $valuesRight); + } + + public function test_iterate_要素がない場合(): void + { + // Arrange + $range = IntegerRange::open(1, 2); + + // Act + $values = []; + foreach ($range->iterate() as $value) { + $values[] = $value; + } + + // Assert + $this->assertSame([], $values); + } + + public function test_equals_同じ範囲の場合trueを返す(): void + { + // Arrange + $range1 = IntegerRange::closed(1, 10); + $range2 = IntegerRange::closed(1, 10); + + // Act & Assert + $this->assertTrue($range1->equals($range2)); + $this->assertTrue($range2->equals($range1)); + } + + public function test_equals_異なる範囲の場合falseを返す(): void + { + // Arrange + $range1 = IntegerRange::closed(1, 10); + $range2 = IntegerRange::closed(1, 11); + $range3 = IntegerRange::open(1, 10); + + // Act & Assert + $this->assertFalse($range1->equals($range2)); + $this->assertFalse($range1->equals($range3)); + } + + public function test_jsonSerialize_正しいJSON形式を返す(): void + { + // Arrange + $range = IntegerRange::halfOpenRight(1, 10); + + // Act + $json = $range->jsonSerialize(); + + // Assert + $this->assertSame([ + 'from' => 1, + 'to' => 10, + 'rangeType' => 'half_open_right', + ], $json); + } + + public function test_fromNullable_nullが渡された場合Noneを返す(): void + { + // Act + $result1 = IntegerRange::fromNullable(null, 10); + $result2 = IntegerRange::fromNullable(1, null); + $result3 = IntegerRange::fromNullable(null, null); + + // Assert + $this->assertTrue($result1->isNone()); + $this->assertTrue($result2->isNone()); + $this->assertTrue($result3->isNone()); + } + + public function test_fromNullable_有効な値が渡された場合Someを返す(): void + { + // Act + $result = IntegerRange::fromNullable(1, 10); + + // Assert + $this->assertTrue($result->isSome()); + $range = $result->unwrap(); + $this->assertSame(1, $range->getFrom()); + $this->assertSame(10, $range->getTo()); + } + + public function test_tryFromNullable_nullが渡された場合Ok_Noneを返す(): void + { + // Act + $result = IntegerRange::tryFromNullable(null, 10); + + // Assert + $this->assertTrue($result->isOk()); + $option = $result->unwrap(); + $this->assertTrue($option->isNone()); + } + + public function test_tryFromNullable_無効な値が渡された場合Errを返す(): void + { + // Act + $result = IntegerRange::tryFromNullable(10, 5); + + // Assert + $this->assertTrue($result->isErr()); + } + + public function test_tryFromNullable_有効な値が渡された場合Ok_Someを返す(): void + { + // Act + $result = IntegerRange::tryFromNullable(1, 10); + + // Assert + $this->assertTrue($result->isOk()); + $option = $result->unwrap(); + $this->assertTrue($option->isSome()); + $range = $option->unwrap(); + $this->assertSame(1, $range->getFrom()); + $this->assertSame(10, $range->getTo()); + } + + public function test_負の整数範囲も作成できる(): void + { + // Arrange + $from = -10; + $to = -1; + + // Act + $range = IntegerRange::closed($from, $to); + + // Assert + $this->assertSame($from, $range->getFrom()); + $this->assertSame($to, $range->getTo()); + $this->assertSame(10, $range->count()); + } + + public function test_ゼロを含む範囲も作成できる(): void + { + // Arrange + $from = -5; + $to = 5; + + // Act + $range = IntegerRange::closed($from, $to); + + // Assert + $this->assertTrue($range->contains(0)); + $this->assertSame(11, $range->count()); + } +} From 6143fd515b310c2d8a1513040fdfbc9a51bc65d3 Mon Sep 17 00:00:00 2001 From: kakiuchi-shigenao Date: Sun, 6 Jul 2025 17:55:09 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[feat]=20IntegerRange=20=E5=80=A4=E3=82=AA?= =?UTF-8?q?=E3=83=96=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20Fixes=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/Number/TestIntegerRange.php | 17 ++++- src/Number/IntegerRange.php | 61 +++++++++++------- tests/Unit/Number/IntegerRangeTest.php | 86 +++++++++++++++++++------- 3 files changed, 119 insertions(+), 45 deletions(-) diff --git a/examples/Number/TestIntegerRange.php b/examples/Number/TestIntegerRange.php index e28179d..c9eaa0b 100644 --- a/examples/Number/TestIntegerRange.php +++ b/examples/Number/TestIntegerRange.php @@ -125,4 +125,19 @@ $json = json_encode($jsonRange); echo "範囲: {$jsonRange}\n"; -echo "JSON: {$json}\n"; +echo "JSON: {$json}\n\n"; + +// 13. toがnullの場合(最大値まで) +echo "=== toがnullの場合(最大値まで) ===\n"; +$openEndRange = IntegerRange::closed(0, null); + +echo "0から最大値までの範囲: {$openEndRange}\n"; +echo "最大値: " . $openEndRange->getTo() . "\n"; +echo "1000を含む: " . ($openEndRange->contains(1000) ? 'はい' : 'いいえ') . "\n"; + +// fromNullableでもtoはnullを許容 +$optionalRange = IntegerRange::fromNullable(100, null); +if ($optionalRange->isSome()) { + $range = $optionalRange->unwrap(); + echo "100から最大値までの範囲(Optional): {$range}\n"; +} diff --git a/src/Number/IntegerRange.php b/src/Number/IntegerRange.php index eb2b797..02682b9 100644 --- a/src/Number/IntegerRange.php +++ b/src/Number/IntegerRange.php @@ -21,6 +21,8 @@ #[ValueObjectMeta(name: '整数範囲')] final readonly class IntegerRange implements IValueObject, Stringable, JsonSerializable { + private const int MAX_INT = PHP_INT_MAX; + /** * @param int $from 開始値 * @param int $to 終了値 @@ -39,55 +41,65 @@ private function __construct( * 指定された整数値から範囲を生成する * * @param int $from 開始値 - * @param int $to 終了値 + * @param int|null $to 終了値(nullの場合は最大値) * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) */ - public static function from(int $from, int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): self + public static function from(int $from, ?int $to = null, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): self { + $to ??= self::MAX_INT; + return new self($from, $to, $rangeType); } /** * 閉区間 [from, to] を生成する * - * @param int $from 開始値 - * @param int $to 終了値 + * @param int $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) */ - public static function closed(int $from, int $to): self + public static function closed(int $from, ?int $to = null): self { + $to ??= self::MAX_INT; + return new self($from, $to, RangeType::CLOSED); } /** * 開区間 (from, to) を生成する * - * @param int $from 開始値 - * @param int $to 終了値 + * @param int $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) */ - public static function open(int $from, int $to): self + public static function open(int $from, ?int $to = null): self { + $to ??= self::MAX_INT; + return new self($from, $to, RangeType::OPEN); } /** * 左開区間 (from, to] を生成する * - * @param int $from 開始値 - * @param int $to 終了値 + * @param int $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) */ - public static function halfOpenLeft(int $from, int $to): self + public static function halfOpenLeft(int $from, ?int $to = null): self { + $to ??= self::MAX_INT; + return new self($from, $to, RangeType::HALF_OPEN_LEFT); } /** * 右開区間 [from, to) を生成する * - * @param int $from 開始値 - * @param int $to 終了値 + * @param int $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) */ - public static function halfOpenRight(int $from, int $to): self + public static function halfOpenRight(int $from, ?int $to = null): self { + $to ??= self::MAX_INT; + return new self($from, $to, RangeType::HALF_OPEN_RIGHT); } @@ -95,12 +107,13 @@ public static function halfOpenRight(int $from, int $to): self * 指定された整数値から範囲を生成する(エラーハンドリング付き) * * @param int $from 開始値 - * @param int $to 終了値 + * @param int|null $to 終了値(nullの場合は最大値) * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) * @return Result */ - public static function tryFrom(int $from, int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Result + public static function tryFrom(int $from, ?int $to = null, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Result { + $to ??= self::MAX_INT; $validationResult = self::isValid($from, $to); if ($validationResult->isErr()) { return $validationResult; @@ -113,16 +126,18 @@ public static function tryFrom(int $from, int $to, RangeType $rangeType = RangeT * 指定された整数値から範囲を生成する(null許容) * * @param int|null $from 開始値 - * @param int|null $to 終了値 + * @param int|null $to 終了値(nullの場合は最大値) * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) * @return Option */ - public static function fromNullable(?int $from, ?int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Option + public static function fromNullable(?int $from, ?int $to = null, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Option { - if ($from === null || $to === null) { + if ($from === null) { return Option\none(); } + $to ??= self::MAX_INT; + return Option\some(new self($from, $to, $rangeType)); } @@ -130,17 +145,19 @@ public static function fromNullable(?int $from, ?int $to, RangeType $rangeType = * 指定された整数値から範囲を生成する(null許容、エラーハンドリング付き) * * @param int|null $from 開始値 - * @param int|null $to 終了値 + * @param int|null $to 終了値(nullの場合は最大値) * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) * @return Result, ValueObjectError> */ - public static function tryFromNullable(?int $from, ?int $to, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Result + public static function tryFromNullable(?int $from, ?int $to = null, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Result { - if ($from === null || $to === null) { + if ($from === null) { // @phpstan-ignore-next-line return Result\ok(Option\none()); } + $to ??= self::MAX_INT; + // @phpstan-ignore-next-line return self::tryFrom($from, $to, $rangeType)->map( static fn (self $range) => Option\some($range) diff --git a/tests/Unit/Number/IntegerRangeTest.php b/tests/Unit/Number/IntegerRangeTest.php index b53eb05..f95280f 100644 --- a/tests/Unit/Number/IntegerRangeTest.php +++ b/tests/Unit/Number/IntegerRangeTest.php @@ -347,18 +347,6 @@ public function test_jsonSerialize_正しいJSON形式を返す(): void ], $json); } - public function test_fromNullable_nullが渡された場合Noneを返す(): void - { - // Act - $result1 = IntegerRange::fromNullable(null, 10); - $result2 = IntegerRange::fromNullable(1, null); - $result3 = IntegerRange::fromNullable(null, null); - - // Assert - $this->assertTrue($result1->isNone()); - $this->assertTrue($result2->isNone()); - $this->assertTrue($result3->isNone()); - } public function test_fromNullable_有効な値が渡された場合Someを返す(): void { @@ -372,16 +360,6 @@ public function test_fromNullable_有効な値が渡された場合Someを返す $this->assertSame(10, $range->getTo()); } - public function test_tryFromNullable_nullが渡された場合Ok_Noneを返す(): void - { - // Act - $result = IntegerRange::tryFromNullable(null, 10); - - // Assert - $this->assertTrue($result->isOk()); - $option = $result->unwrap(); - $this->assertTrue($option->isNone()); - } public function test_tryFromNullable_無効な値が渡された場合Errを返す(): void { @@ -434,4 +412,68 @@ public function test_ゼロを含む範囲も作成できる(): void $this->assertTrue($range->contains(0)); $this->assertSame(11, $range->count()); } + + public function test_toがnullの場合は最大値が設定される(): void + { + // Act + $range = IntegerRange::closed(1, null); + + // Assert + $this->assertSame(1, $range->getFrom()); + $this->assertSame(PHP_INT_MAX, $range->getTo()); + } + + public function test_各ファクトリメソッドでtoがnullの場合は最大値が設定される(): void + { + // Act + $closed = IntegerRange::closed(1, null); + $open = IntegerRange::open(1, null); + $halfOpenLeft = IntegerRange::halfOpenLeft(1, null); + $halfOpenRight = IntegerRange::halfOpenRight(1, null); + + // Assert + $this->assertSame(PHP_INT_MAX, $closed->getTo()); + $this->assertSame(PHP_INT_MAX, $open->getTo()); + $this->assertSame(PHP_INT_MAX, $halfOpenLeft->getTo()); + $this->assertSame(PHP_INT_MAX, $halfOpenRight->getTo()); + } + + public function test_fromNullable_fromがnullの場合のみNoneを返す(): void + { + // Act + $result1 = IntegerRange::fromNullable(null, 10); + $result2 = IntegerRange::fromNullable(1, null); + $result3 = IntegerRange::fromNullable(null, null); + + // Assert + $this->assertTrue($result1->isNone()); + $this->assertTrue($result2->isSome()); + $this->assertTrue($result3->isNone()); + + // toがnullの場合は最大値が設定される + $range = $result2->unwrap(); + $this->assertSame(1, $range->getFrom()); + $this->assertSame(PHP_INT_MAX, $range->getTo()); + } + + public function test_tryFromNullable_fromがnullの場合のみNoneを返す(): void + { + // Act + $result1 = IntegerRange::tryFromNullable(null, 10); + $result2 = IntegerRange::tryFromNullable(1, null); + + // Assert + $this->assertTrue($result1->isOk()); + $option1 = $result1->unwrap(); + $this->assertTrue($option1->isNone()); + + $this->assertTrue($result2->isOk()); + $option2 = $result2->unwrap(); + $this->assertTrue($option2->isSome()); + + // toがnullの場合は最大値が設定される + $range = $option2->unwrap(); + $this->assertSame(1, $range->getFrom()); + $this->assertSame(PHP_INT_MAX, $range->getTo()); + } }