assertSame(1500, FixedPoint::toScaled('1.5', 3)); $this->assertSame(-1500, FixedPoint::toScaled('-1.5', 3)); $this->assertSame(1000000, FixedPoint::toScaled('1', 6)); $this->assertSame(0, FixedPoint::toScaled('0', 4)); } public function test_rejects_malformed_decimal_strings(): void { $this->expectException(InvalidArgumentException::class); FixedPoint::toScaled('1.5.3', 3); } public function test_rejects_non_numeric_strings(): void { $this->expectException(InvalidArgumentException::class); FixedPoint::toScaled('abc', 3); } public function test_rejects_values_exceeding_bound(): void { $this->expectException(InvalidArgumentException::class); FixedPoint::toScaled('100000000', 6, maxAbs: 1_000_000); } public function test_rejects_negative_values_when_disallowed(): void { $this->expectException(InvalidArgumentException::class); FixedPoint::toScaled('-5', 3, allowNegative: false); } public function test_multiplies_two_scaled_values_and_rescales_with_round_half_up(): void { // 2.5 grams (scale 3) * 4.0 rate per kg (scale 6) => product scale 9, then /1000 for g->kg => rescale to scale 4. $grams = FixedPoint::toScaled('2.500', 3); $rate = FixedPoint::toScaled('4.000000', 6); $productScale9 = FixedPoint::multiply($grams, $rate); // raw cost = 2.5g * (4.0/1000 per g) = 0.01 -> scale 4 => 100 $result = FixedPoint::rescale($productScale9, 9 + 3, 4); $this->assertSame(100, $result); $this->assertSame('0.0100', FixedPoint::format($result, 4)); } public function test_rescale_rounds_half_up(): void { // 0.00005 at scale 5 rescaled to scale 4 should round up to 0.0001 (not banker's rounding). $this->assertSame(1, FixedPoint::rescale(5, 5, 4)); $this->assertSame(0, FixedPoint::rescale(4, 5, 4)); $this->assertSame(-1, FixedPoint::rescale(-5, 5, 4)); } public function test_add_and_subtract_scaled_values(): void { $a = FixedPoint::toScaled('10.1234', 4); $b = FixedPoint::toScaled('0.0001', 4); $this->assertSame(101235, FixedPoint::add($a, $b)); $this->assertSame(101233, FixedPoint::subtract($a, $b)); } public function test_add_detects_overflow(): void { $this->expectException(OverflowException::class); FixedPoint::add(PHP_INT_MAX, 1); } public function test_multiply_detects_overflow(): void { $this->expectException(OverflowException::class); FixedPoint::multiply(PHP_INT_MAX, 2); } public function test_divide_scaled_value_by_positive_integer_with_round_half_up(): void { $amount = FixedPoint::toScaled('10.0000', 4); $this->assertSame(FixedPoint::toScaled('3.3333', 4), FixedPoint::divideByInt($amount, 3)); } public function test_divide_by_zero_is_rejected(): void { $this->expectException(InvalidArgumentException::class); FixedPoint::divideByInt(FixedPoint::toScaled('1', 4), 0); } public function test_format_renders_fixed_decimal_places_without_floats(): void { $this->assertSame('1234.5000', FixedPoint::format(FixedPoint::toScaled('1234.5', 4), 4)); $this->assertSame('-0.0100', FixedPoint::format(FixedPoint::toScaled('-0.01', 4), 4)); $this->assertSame('0.0000', FixedPoint::format(0, 4)); } }