diff --git a/src/Number/Decimal.php b/src/Number/Decimal.php new file mode 100644 index 0000000..e295101 --- /dev/null +++ b/src/Number/Decimal.php @@ -0,0 +1,84 @@ + + * @param int $rounding (1 : PHP_ROUND_HALF_UP (Default), 2 : PHP_ROUND_HALF_DOWN, + * 3 : PHP_ROUND_HALF_EVEN, 4 : PHP_ROUND_HALF_ODD) + */ + public function __construct(float $value, int $precision = 2, int $rounding = PHP_ROUND_HALF_UP) + { + if ($value == 0.0) { + $minimumPrecision = 0; + } else { + $minimumPrecision = -(floor(log10(abs($value))) + 1); + } + + if ($precision > 16 || $precision <= $minimumPrecision) { + throw new InvalidArgumentException( + "Precision must be greater than {$minimumPrecision} and less than or equal to 16." + ); + } + + if ($rounding < 1 || $rounding > 4) { + throw new InvalidArgumentException('Rounding must be between 1 and 4.'); + } + + $this->value = round($value, $precision, $rounding); + $this->precision = $precision; + $this->rounding = $rounding; + } + + final public function getValue(): float + { + return $this->value; + } + + final public function getPrecision(): int + { + return $this->precision; + } + + final public function getRounding(): int + { + return $this->rounding; + } + + /** + * @param int|float|string $value + * @param int $precision + * @param int $rounding (1 : PHP_ROUND_HALF_UP, 2 : PHP_ROUND_HALF_DOWN, + * 3 : PHP_ROUND_HALF_EVEN, 4 : PHP_ROUND_HALF_ODD) + * @return self + */ + public static function fromNumeric($value, int $precision = 2, int $rounding = PHP_ROUND_HALF_UP): self + { + if (!is_numeric($value)) { + throw new InvalidArgumentException('Value is not numeric.'); + } + + $value = (float)$value; + + return new self($value, $precision, $rounding); + } + + final public function toString(): string + { + return (string)$this->value; + } +} diff --git a/src/Number/NonNegativeDecimal.php b/src/Number/NonNegativeDecimal.php new file mode 100644 index 0000000..669c45d --- /dev/null +++ b/src/Number/NonNegativeDecimal.php @@ -0,0 +1,43 @@ + + * @param int $rounding (1 : PHP_ROUND_HALF_UP (Default), 2 : PHP_ROUND_HALF_DOWN, + * 3 : PHP_ROUND_HALF_EVEN, 4 : PHP_ROUND_HALF_ODD) + */ + public function __construct($value, int $precision = 2, int $rounding = PHP_ROUND_HALF_UP) + { + if ($value < 0) { + throw new InvalidArgumentException('Value must be a non-negative decimal number.'); + } + + parent::__construct($value, $precision, $rounding); + } + + /** + * @param int|float|string $value + * @param int $precision + * @param int $rounding (1 : PHP_ROUND_HALF_UP, 2 : PHP_ROUND_HALF_DOWN, + * 3 : PHP_ROUND_HALF_EVEN, 4 : PHP_ROUND_HALF_ODD) + * @return self + */ + public static function fromNumeric($value, int $precision = 2, int $rounding = PHP_ROUND_HALF_UP): self + { + if (!is_numeric($value)) { + throw new InvalidArgumentException('Value is not numeric.'); + } + + $value = (float)$value; + + return new self($value, $precision, $rounding); + } +} diff --git a/tests/Number/DecimalTest.php b/tests/Number/DecimalTest.php new file mode 100644 index 0000000..1ef5167 --- /dev/null +++ b/tests/Number/DecimalTest.php @@ -0,0 +1,232 @@ +getValue(); + + $this->assertEqualsWithDelta(12.35, $result, 0.00001); + } + + public function test_CanBeCreatedFromFloat(): void + { + $result = (new Decimal(12.345, 1))->getValue(); + + $this->assertEqualsWithDelta(12.3, $result, 0.00001); + } + + public function test_canBeCreatedFromFloatString(): void + { + $result = Decimal::fromNumeric('12.345')->getValue(); + + $this->assertEqualsWithDelta(12.35, $result, 0.00001); + } + + public function test_canBeCreatedFromInteger(): void + { + $result = Decimal::fromNumeric(12.3)->getValue(); + + $this->assertEqualsWithDelta(12.3, $result, 0.00001); + } + + public function test_canNotBeCreatedFromNonNumeric(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Value is not numeric.'); + Decimal::fromNumeric('abc'); + } + + public function test_IsEqualToSameTypeWithSameValue(): void + { + $float1 = new Decimal(12.345); + $float2 = new Decimal(12.345); + + $this->assertTrue($float1->equals($float2)); + $this->assertTrue($float2->equals($float1)); + } + + public function test_IsNotEqualToSameTypeWithDifferentValue(): void + { + $float1 = new Decimal(12.345, 3); + $float2 = new Decimal(12.346, 3); + + $this->assertFalse($float1->equals($float2)); + $this->assertFalse($float2->equals($float1)); + } + + public function test_IsNotEqualToSameTypeWithDifferentType(): void + { + $float1 = new Decimal(12, 0); + $integer = new Integer(12); + + $this->assertFalse($float1->equals($integer)); + $this->assertFalse($integer->equals($float1)); + } + + public function test_GetMethods(): void + { + $decimal = new Decimal(12.345, 16, PHP_ROUND_HALF_UP); + + $this->assertEquals(12.345, $decimal->getValue()); + $this->assertEquals(16, $decimal->getPrecision()); + $this->assertEquals(1, $decimal->getRounding()); + } + + public function test_RoundingValues(): void + { + $decimal = new Decimal(12.345, 2, PHP_ROUND_HALF_UP); + $this->assertEquals(12.35, $decimal->getValue()); + $this->assertEquals(1, $decimal->getRounding()); + + $decimal = new Decimal(12.345, 2, PHP_ROUND_HALF_DOWN); + $this->assertEquals(12.34, $decimal->getValue()); + $this->assertEquals(2, $decimal->getRounding()); + + $decimal = new Decimal(12.345, 2, PHP_ROUND_HALF_EVEN); + $this->assertEquals(12.34, $decimal->getValue()); + $this->assertEquals(3, $decimal->getRounding()); + + $decimal = new Decimal(12.345, 2, PHP_ROUND_HALF_ODD); + $this->assertEquals(12.35, $decimal->getValue()); + $this->assertEquals(4, $decimal->getRounding()); + } + + public function test_RoundingThrowsExceptionOnInvalidHigherValue(): void + { + $this->expectException(InvalidArgumentException::class); + new Decimal(12.345, 2, 5); + } + + public function test_RoundingThrowsExceptionOnInvalidLowerValue(): void + { + $this->expectException(InvalidArgumentException::class); + new Decimal(12.345, 2, 0); + } + + public function test_PrecisionThrowsExceptionWhenTooHigh(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Precision must be greater than -1 and less than or equal to 16."); + new Decimal(2, 17); + } + + public function test_PrecisionThrowsExceptionWhenTooLow(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Precision must be greater than -1 and less than or equal to 16."); + new Decimal(2, -1); + } + + public function test_FromNumericPrecisionThrowsExceptionWhenTooLow(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Precision must be greater than -1 and less than or equal to 16."); + Decimal::fromNumeric(2, -1); + } + + public function test_FromNumericPrecisionThrowsExceptionWhenTooHigh(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Precision must be greater than -1 and less than or equal to 16."); + Decimal::fromNumeric(2, 17); + } + + public function test_FromNumericPrecisionThrowsExceptionWhenMinimumZeroAndValueZero(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Precision must be greater than 0 and less than or equal to 16."); + Decimal::fromNumeric(0, -12); + } + + public function test_MinimumPrecisionIsZeroWhenGivenZeroAsValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Precision must be greater than 0 and less than or equal to 16."); + new Decimal(0, -12); + } + + public function test_PrecisionFailsWhenBelowMinimumBasedOnFloor(): void + { + $value = 9.99; + $this->expectException(InvalidArgumentException::class); + new Decimal($value, -1); + } + + public function test_PrecisionPassesWhenJustAboveMinimumBasedOnFloor(): void + { + $value = 9.99; + $decimal = new Decimal($value, 0); + $this->assertEquals(round(9.99, 0), $decimal->getValue()); + } + + public function test_FromNumericMinimumPrecisionIsZeroWhenGivenZeroAsValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Precision must be greater than 0 and less than or equal to 16."); + Decimal::fromNumeric(0, -12); + } + + public function test_FromNumericPrecisionFailsWhenBelowMinimumBasedOnFloor(): void + { + $value = 9.99; + $this->expectException(InvalidArgumentException::class); + Decimal::fromNumeric($value, -1); + } + + public function test_FromNumericPrecisionPassesWhenJustAboveMinimumBasedOnFloor(): void + { + $value = 9.99; + $decimal = Decimal::fromNumeric($value, 0); + $this->assertEquals(round(9.99, 0), $decimal->getValue()); + } + + public function test_FromNumericRoundingValues(): void + { + $decimal = Decimal::fromNumeric(12.345, 2, PHP_ROUND_HALF_UP); + $this->assertEquals(12.35, $decimal->getValue()); + $this->assertEquals(1, $decimal->getRounding()); + + $decimal = Decimal::fromNumeric(12.345, 2, PHP_ROUND_HALF_DOWN); + $this->assertEquals(12.34, $decimal->getValue()); + $this->assertEquals(2, $decimal->getRounding()); + + $decimal = Decimal::fromNumeric(12.345, 2, PHP_ROUND_HALF_EVEN); + $this->assertEquals(12.34, $decimal->getValue()); + $this->assertEquals(3, $decimal->getRounding()); + + $decimal = Decimal::fromNumeric(12.345, 2, PHP_ROUND_HALF_ODD); + $this->assertEquals(12.35, $decimal->getValue()); + $this->assertEquals(4, $decimal->getRounding()); + } + + public function test_FromNumericRoundingThrowsExceptionOnInvalidHigherValue(): void + { + $this->expectException(InvalidArgumentException::class); + Decimal::fromNumeric(12.345, 2, 5); + } + + public function test_FromNumericRoundingThrowsExceptionOnInvalidLowerValue(): void + { + $this->expectException(InvalidArgumentException::class); + Decimal::fromNumeric(12.345, 2, 0); + } + public function test_toString(): void + { + $decimal = new Decimal(123.321, 15, 1); + $this->assertEquals('123.321', $decimal->toString()); + $this->assertSame('123.321', (string) $decimal->getValue()); + } +} diff --git a/tests/Number/NonNegativeDecimalTest.php b/tests/Number/NonNegativeDecimalTest.php new file mode 100644 index 0000000..f3d6a44 --- /dev/null +++ b/tests/Number/NonNegativeDecimalTest.php @@ -0,0 +1,108 @@ +getValue(); + + $this->assertEqualsWithDelta(12.35, $result, 0.00001); + } + + public function test_CanBeCreatedFromZero(): void + { + $result = (new NonNegativeDecimal(0))->getValue(); + + $this->assertEqualsWithDelta(0.0, $result, 0.00001); + } + + public function test_canBeCreatedFromNumericString(): void + { + $result = NonNegativeDecimal::fromNumeric('12.345')->getValue(); + + $this->assertEqualsWithDelta(12.35, $result, 0.00001); + } + + public function test_canBeCreatedFromFloat(): void + { + $result = NonNegativeDecimal::fromNumeric(12.34)->getValue(); + + $this->assertEqualsWithDelta(12.34, $result, 0.00001); + } + + public function test_canBeCreatedFromZeroFromNumeric(): void + { + $result = NonNegativeDecimal::fromNumeric(0)->getValue(); + + $this->assertEqualsWithDelta(0, $result, 0.00001); + } + + public function test_canNotBeCreatedFromNegativeUsingNumericFloat(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Value must be a non-negative decimal number.'); + + NonNegativeDecimal::fromNumeric(-12.34); + } + + public function test_canNotBeCreatedFromNonNumeric(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Value is not numeric.'); + + NonNegativeDecimal::fromNumeric('abc'); + } + + public function test_CannotBeCreatedFromNegativeFloat(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Value must be a non-negative decimal number.'); + + new NonNegativeDecimal(-12.34); + } + + public function test_IsEqualToSameTypeWithSameValue(): void + { + $float1 = new NonNegativeDecimal(12.34); + $float2 = new NonNegativeDecimal(12.34); + + $this->assertTrue($float1->equals($float2)); + $this->assertTrue($float2->equals($float1)); + } + + public function test_IsNotEqualToSameTypeWithDifferentValue(): void + { + $float1 = new NonNegativeDecimal(12.34); + $float2 = new NonNegativeDecimal(12.35); + + $this->assertFalse($float1->equals($float2)); + $this->assertFalse($float2->equals($float1)); + } + + public function test_isNotEqualToDifferentType(): void + { + $float1 = new NonNegativeDecimal(12.34); + $float2 = new Decimal(12.34); + + $this->assertFalse($float1->equals($float2)); + $this->assertFalse($float2->equals($float1)); + } + + public function test_toString(): void + { + $decimal = new NonNegativeDecimal(123.321, 3); + $this->assertEquals('123.321', $decimal->toString()); + } +}