diff --git a/examples/Number/TestIntegerRange.php b/examples/Number/TestIntegerRange.php new file mode 100644 index 0000000..c9eaa0b --- /dev/null +++ b/examples/Number/TestIntegerRange.php @@ -0,0 +1,143 @@ +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\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 new file mode 100644 index 0000000..02682b9 --- /dev/null +++ b/src/Number/IntegerRange.php @@ -0,0 +1,363 @@ +isOk()); + } + + /** + * 指定された整数値から範囲を生成する + * + * @param int $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + */ + 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|null $to 終了値(nullの場合は最大値) + */ + 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|null $to 終了値(nullの場合は最大値) + */ + 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|null $to 終了値(nullの場合は最大値) + */ + 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|null $to 終了値(nullの場合は最大値) + */ + public static function halfOpenRight(int $from, ?int $to = null): self + { + $to ??= self::MAX_INT; + + return new self($from, $to, RangeType::HALF_OPEN_RIGHT); + } + + /** + * 指定された整数値から範囲を生成する(エラーハンドリング付き) + * + * @param int $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + * @return 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; + } + + return Result\ok(new self($from, $to, $rangeType)); + } + + /** + * 指定された整数値から範囲を生成する(null許容) + * + * @param int|null $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + * @return Option + */ + public static function fromNullable(?int $from, ?int $to = null, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Option + { + if ($from === null) { + return Option\none(); + } + + $to ??= self::MAX_INT; + + return Option\some(new self($from, $to, $rangeType)); + } + + /** + * 指定された整数値から範囲を生成する(null許容、エラーハンドリング付き) + * + * @param int|null $from 開始値 + * @param int|null $to 終了値(nullの場合は最大値) + * @param RangeType $rangeType 範囲タイプ(デフォルト:HALF_OPEN_RIGHT) + * @return Result, ValueObjectError> + */ + public static function tryFromNullable(?int $from, ?int $to = null, RangeType $rangeType = RangeType::HALF_OPEN_RIGHT): Result + { + 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) + ); + } + + /** + * 開始値と終了値の妥当性を検証する + * + * @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..f95280f --- /dev/null +++ b/tests/Unit/Number/IntegerRangeTest.php @@ -0,0 +1,479 @@ +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_有効な値が渡された場合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_無効な値が渡された場合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()); + } + + 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()); + } +}