Skip to content

Commit 9882788

Browse files
endou-mameclaude
andcommitted
feat: Add isPositive, isNegative, and zero methods to Number Value Objects
- Add isPositive() method to DecimalValueBase and IntegerValueBase - Add isNegative() method to DecimalValueBase and IntegerValueBase - Add zero() method to DecimalValueFactory and IntegerValueFactory - Comprehensive test coverage for all new methods including edge cases - Update existing tests to accommodate new functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3448046 commit 9882788

File tree

8 files changed

+637
-2
lines changed

8 files changed

+637
-2
lines changed

src/Number/Decimal/DecimalValueBase.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ final public function isZero(): bool
129129
return $this->value->compare(0) === 0;
130130
}
131131

132+
/**
133+
* 正の値かどうか
134+
*/
135+
final public function isPositive(): bool
136+
{
137+
return $this->value->compare(0) > 0;
138+
}
139+
140+
/**
141+
* 負の値かどうか
142+
*/
143+
final public function isNegative(): bool
144+
{
145+
return $this->value->compare(0) < 0;
146+
}
147+
132148
/**
133149
* 小数点以下の桁数を指定してフォーマットする
134150
* @param positive-int|0|null $decimals 小数点以下の桁数

src/Number/Decimal/DecimalValueFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,10 @@ final public static function tryFromNullable(?Number $value): Result
4343
// @phpstan-ignore return.type
4444
return static::tryFrom($value)->map(static fn ($result) => Option\some($result));
4545
}
46+
47+
#[Override]
48+
public static function zero(): static
49+
{
50+
return static::from(new Number('0'));
51+
}
4652
}

src/Number/Decimal/IDecimalValueFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ public static function tryFrom(Number $value): Result;
3737
* @return Result<Option<static>,ValueObjectError>
3838
*/
3939
public static function tryFromNullable(?Number $value): Result;
40+
41+
/**
42+
* ゼロの値オブジェクトを生成する
43+
*/
44+
public static function zero(): static;
4045
}

src/Number/Integer/IIntegerValueFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ public static function tryFrom(int $value): Result;
3636
* @return Result<Option<static>,ValueObjectError>
3737
*/
3838
public static function tryFromNullable(?int $value): Result;
39+
40+
/**
41+
* ゼロの値オブジェクトを生成する
42+
*/
43+
public static function zero(): static;
3944
}

src/Number/Integer/IntegerValueBase.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,20 @@ final public function isZero(): bool
8282
{
8383
return $this->value === 0;
8484
}
85+
86+
/**
87+
* 正の値かどうか
88+
*/
89+
final public function isPositive(): bool
90+
{
91+
return $this->value > 0;
92+
}
93+
94+
/**
95+
* 負の値かどうか
96+
*/
97+
final public function isNegative(): bool
98+
{
99+
return $this->value < 0;
100+
}
85101
}

src/Number/Integer/IntegerValueFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@ final public static function tryFromNullable(?int $value): Result
4242
// @phpstan-ignore return.type
4343
return static::tryFrom($value)->map(static fn ($result) => Option\some($result));
4444
}
45+
46+
#[Override]
47+
public static function zero(): static
48+
{
49+
return static::from(0);
50+
}
4551
}

tests/Unit/Number/Decimal/DecimalValueTest.php

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,30 @@ public function isZero関数でゼロかどうかを判定できる(): void
7171
$this->assertFalse($nonZeroValue->isZero());
7272
}
7373

74+
#[Test]
75+
public function isPositive関数で正の値かどうかを判定できる(): void
76+
{
77+
$positiveValue = TestDecimalValue::from(new Number('123.45'));
78+
$negativeValue = TestDecimalValue::from(new Number('-123.45'));
79+
$zeroValue = TestDecimalValue::from(new Number('0'));
80+
81+
$this->assertTrue($positiveValue->isPositive());
82+
$this->assertFalse($negativeValue->isPositive());
83+
$this->assertFalse($zeroValue->isPositive());
84+
}
85+
86+
#[Test]
87+
public function isNegative関数で負の値かどうかを判定できる(): void
88+
{
89+
$positiveValue = TestDecimalValue::from(new Number('123.45'));
90+
$negativeValue = TestDecimalValue::from(new Number('-123.45'));
91+
$zeroValue = TestDecimalValue::from(new Number('0'));
92+
93+
$this->assertFalse($positiveValue->isNegative());
94+
$this->assertTrue($negativeValue->isNegative());
95+
$this->assertFalse($zeroValue->isNegative());
96+
}
97+
7498
/**
7599
* @return array<string, array{string}>
76100
*/
@@ -621,6 +645,77 @@ public function 有効桁数を超える値はエラーになることを確認(
621645
$this->assertStringContainsString('30', $errorMessage, 'エラーメッセージには実際の桁数(30)が含まれるべき');
622646
}
623647

648+
// ------------------------------------------
649+
// DecimalValueFactoryのtryFromメソッドのテスト
650+
// ------------------------------------------
651+
652+
#[Test]
653+
public function tryFromメソッドは未実装のため常にResult_okを返す(): void
654+
{
655+
// DecimalValueFactoryのtryFromメソッドは抽象メソッドなので
656+
// 実装クラスで実装される必要がある
657+
$value = new Number('100.50');
658+
$result = TestDecimalValue::tryFrom($value);
659+
660+
$this->assertTrue($result->isOk());
661+
$this->assertEquals('100.50', (string)$result->unwrap()->value);
662+
}
663+
664+
#[Test]
665+
public function tryFromメソッドは範囲外の値に対してエラーを返す(): void
666+
{
667+
// 範囲外の値をテスト
668+
$result = TestDecimalValue::tryFrom(new Number('1001'));
669+
$this->assertFalse($result->isOk());
670+
$this->assertInstanceOf(ValueObjectError::class, $result->unwrapErr());
671+
}
672+
673+
#[Test]
674+
public function tryFromメソッドは有効桁数を超える値に対してエラーを返す(): void
675+
{
676+
// 範囲内で有効桁数を超える値をテスト
677+
$largeDigitValue = '1.' . str_repeat('9', 29); // 30桁の値(範囲内)
678+
$result = TestDecimalValue::tryFrom(new Number($largeDigitValue));
679+
680+
$this->assertFalse($result->isOk());
681+
$this->assertInstanceOf(ValueObjectError::class, $result->unwrapErr());
682+
683+
$errorMessage = $result->unwrapErr()->getMessage();
684+
$this->assertStringContainsString('桁数', $errorMessage);
685+
}
686+
687+
/**
688+
* @return array<string, array{string, bool}>
689+
*/
690+
public static function tryFrom用のテストデータを提供(): array
691+
{
692+
return [
693+
'有効な正の値' => ['100.50', true],
694+
'有効な負の値' => ['-100.50', true],
695+
'有効なゼロ値' => ['0', true],
696+
'有効な最小値' => ['-1000', true],
697+
'有効な最大値' => ['1000', true],
698+
'無効な最小値未満' => ['-1000.01', false],
699+
'無効な最大値超過' => ['1000.01', false],
700+
'有効桁数内の値' => ['123.456789', true],
701+
];
702+
}
703+
704+
#[Test]
705+
#[DataProvider('tryFrom用のテストデータを提供')]
706+
public function tryFromメソッドのデータ駆動テスト(string $value, bool $shouldSucceed): void
707+
{
708+
$result = TestDecimalValue::tryFrom(new Number($value));
709+
710+
if ($shouldSucceed) {
711+
$this->assertTrue($result->isOk(), "{$value} は有効であるべき");
712+
$this->assertEquals($value, (string)$result->unwrap()->value);
713+
} else {
714+
$this->assertFalse($result->isOk(), "{$value} は無効であるべき");
715+
$this->assertInstanceOf(ValueObjectError::class, $result->unwrapErr());
716+
}
717+
}
718+
624719
#[Test]
625720
public function formatToNumberメソッドの動作確認(): void
626721
{
@@ -764,4 +859,193 @@ public function 極端な桁数でのフォーマットテスト(): void
764859
$this->assertEquals('123', $value->format(0));
765860
$this->assertEquals('123', (string)$value->formatToNumber(0));
766861
}
862+
863+
// ------------------------------------------
864+
// DecimalValueBaseのisValidDigitsメソッドのテスト
865+
// ------------------------------------------
866+
867+
#[Test]
868+
public function isValidDigitsメソッドは有効な桁数に対してResult_okを返す(): void
869+
{
870+
// 有効な桁数(29桁以下)
871+
$validValue = new Number('123.456');
872+
$result = TestDecimalValue::tryFrom($validValue);
873+
$this->assertTrue($result->isOk());
874+
}
875+
876+
#[Test]
877+
public function isValidDigitsメソッドは無効な桁数に対してResult_errを返す(): void
878+
{
879+
// 無効な桁数(30桁)で範囲内の値
880+
$invalidValue = new Number('1.' . str_repeat('9', 29));
881+
$result = TestDecimalValue::tryFrom($invalidValue);
882+
$this->assertFalse($result->isOk());
883+
884+
$errorMessage = $result->unwrapErr()->getMessage();
885+
$this->assertStringContainsString('桁数', $errorMessage);
886+
$this->assertStringContainsString('29', $errorMessage); // 許容桁数
887+
$this->assertStringContainsString('30', $errorMessage); // 実際の桁数
888+
}
889+
890+
/**
891+
* @return array<string, array{string, bool}>
892+
*/
893+
public static function isValidDigits用のテストデータを提供(): array
894+
{
895+
return [
896+
'有効な桁数_1桁' => ['1', true],
897+
'有効な桁数_2桁' => ['12', true],
898+
'有効な桁数_小数点含む' => ['123.456', true],
899+
'有効な桁数_負数' => ['-123.456', true],
900+
'有効な桁数_ゼロ' => ['0', true],
901+
'有効な桁数_小数点のみ' => ['1.0', true],
902+
'無効な桁数_大きな小数' => ['1.' . str_repeat('9', 29), false],
903+
'無効な桁数_大きな負数' => ['-1.' . str_repeat('9', 29), false],
904+
];
905+
}
906+
907+
#[Test]
908+
#[DataProvider('isValidDigits用のテストデータを提供')]
909+
public function isValidDigitsメソッドのデータ駆動テスト(string $value, bool $shouldSucceed): void
910+
{
911+
$result = TestDecimalValue::tryFrom(new Number($value));
912+
913+
if ($shouldSucceed) {
914+
$this->assertTrue($result->isOk(), "{$value} は有効な桁数であるべき");
915+
} else {
916+
$this->assertFalse($result->isOk(), "{$value} は無効な桁数であるべき");
917+
$this->assertInstanceOf(ValueObjectError::class, $result->unwrapErr());
918+
919+
$errorMessage = $result->unwrapErr()->getMessage();
920+
$this->assertStringContainsString('桁数', $errorMessage);
921+
}
922+
}
923+
924+
#[Test]
925+
public function isValidDigitsメソッドは小数点とマイナス記号を除外して桁数を計算する(): void
926+
{
927+
// 小数点とマイナス記号を除外した桁数のテスト
928+
$testCases = [
929+
'123.456' => 6, // 1,2,3,4,5,6
930+
'-123.456' => 6, // 1,2,3,4,5,6 (マイナス記号は除外)
931+
'0.123' => 4, // 0,1,2,3
932+
'-0.123' => 4, // 0,1,2,3 (マイナス記号は除外)
933+
'100.00' => 5, // 1,0,0,0,0
934+
];
935+
936+
foreach ($testCases as $value => $expectedDigits) {
937+
$result = TestDecimalValue::tryFrom(new Number($value));
938+
$this->assertTrue($result->isOk(), "{$value} は有効であるべき(桁数: {$expectedDigits}");
939+
}
940+
}
941+
942+
#[Test]
943+
public function isValidDigitsメソッドは29桁ちょうどの値を受け入れる(): void
944+
{
945+
// 29桁ちょうどの値で範囲内
946+
$exactValue = '1.' . str_repeat('1', 27); // 1.111...(29桁)
947+
$result = TestDecimalValue::tryFrom(new Number($exactValue));
948+
$this->assertTrue($result->isOk());
949+
}
950+
951+
#[Test]
952+
public function isValidDigitsメソッドは30桁以上の値を拒否する(): void
953+
{
954+
// 30桁の値で範囲内
955+
$overLimitValue = '1.' . str_repeat('9', 29); // 1.999...(31桁)
956+
$result = TestDecimalValue::tryFrom(new Number($overLimitValue));
957+
$this->assertFalse($result->isOk());
958+
959+
$errorMessage = $result->unwrapErr()->getMessage();
960+
$this->assertStringContainsString('桁数', $errorMessage);
961+
$this->assertStringContainsString('29', $errorMessage);
962+
$this->assertStringContainsString('30', $errorMessage);
963+
}
964+
965+
// ------------------------------------------
966+
// zero()メソッドのテスト
967+
// ------------------------------------------
968+
969+
#[Test]
970+
public function zero関数でゼロの値オブジェクトを生成できる(): void
971+
{
972+
$zeroValue = TestDecimalValue::zero();
973+
974+
$this->assertInstanceOf(TestDecimalValue::class, $zeroValue);
975+
$this->assertEquals('0', (string)$zeroValue->value);
976+
$this->assertTrue($zeroValue->isZero());
977+
$this->assertFalse($zeroValue->isPositive());
978+
$this->assertFalse($zeroValue->isNegative());
979+
}
980+
981+
#[Test]
982+
public function zero関数で生成したインスタンスは他のゼロ値と等価である(): void
983+
{
984+
$zeroValue1 = TestDecimalValue::zero();
985+
$zeroValue2 = TestDecimalValue::from(new Number('0'));
986+
$zeroValue3 = TestDecimalValue::from(new Number('0.0'));
987+
988+
$this->assertTrue($zeroValue1->equals($zeroValue2));
989+
$this->assertTrue($zeroValue1->equals($zeroValue3));
990+
$this->assertTrue($zeroValue2->equals($zeroValue3));
991+
}
992+
993+
#[Test]
994+
public function zero関数で生成したインスタンスは算術演算で期待通りに動作する(): void
995+
{
996+
$zeroValue = TestDecimalValue::zero();
997+
$someValue = TestDecimalValue::from(new Number('123.45'));
998+
999+
// ゼロとの加算
1000+
$addResult = $someValue->add($zeroValue);
1001+
$this->assertTrue($addResult->equals($someValue));
1002+
1003+
// ゼロとの減算
1004+
$subResult = $someValue->sub($zeroValue);
1005+
$this->assertTrue($subResult->equals($someValue));
1006+
1007+
// ゼロとの乗算
1008+
$mulResult = $someValue->mul($zeroValue);
1009+
$this->assertTrue($mulResult->equals($zeroValue));
1010+
}
1011+
1012+
/**
1013+
* @return array<string, array{string, bool, bool, bool}>
1014+
*/
1015+
public static function 正負判定用のテストデータを提供(): array
1016+
{
1017+
return [
1018+
'正の整数' => ['100', false, true, false],
1019+
'正の小数' => ['123.45', false, true, false],
1020+
'負の整数' => ['-100', false, false, true],
1021+
'負の小数' => ['-123.45', false, false, true],
1022+
'ゼロ' => ['0', true, false, false],
1023+
'ゼロ小数' => ['0.0', true, false, false],
1024+
'正の小さい値' => ['0.01', false, true, false],
1025+
'負の小さい値' => ['-0.01', false, false, true],
1026+
'大きな正の値' => ['999.99', false, true, false],
1027+
'大きな負の値' => ['-999.99', false, false, true],
1028+
];
1029+
}
1030+
1031+
/**
1032+
* @param string $value テスト対象の値
1033+
* @param bool $expectZero isZeroの期待値
1034+
* @param bool $expectPos isPositiveの期待値
1035+
* @param bool $expectNeg isNegativeの期待値
1036+
*/
1037+
#[Test]
1038+
#[DataProvider('正負判定用のテストデータを提供')]
1039+
public function 正負判定メソッドのデータ駆動テスト(
1040+
string $value,
1041+
bool $expectZero,
1042+
bool $expectPos,
1043+
bool $expectNeg
1044+
): void {
1045+
$decimalValue = TestDecimalValue::from(new Number($value));
1046+
1047+
$this->assertSame($expectZero, $decimalValue->isZero(), "{$value} のisZero()結果が期待値と異なる");
1048+
$this->assertSame($expectPos, $decimalValue->isPositive(), "{$value} のisPositive()結果が期待値と異なる");
1049+
$this->assertSame($expectNeg, $decimalValue->isNegative(), "{$value} のisNegative()結果が期待値と異なる");
1050+
}
7671051
}

0 commit comments

Comments
 (0)