diff --git a/composer.json b/composer.json index 4c0b29f..817fd8a 100644 --- a/composer.json +++ b/composer.json @@ -3,15 +3,22 @@ "description": "Monetary extension for Axiom - provides schema types, parsers, and operators for monetary values with strong type safety and currency validation", "type": "library", "license": "MIT", - "keywords": ["money", "currency", "schema", "validation", "monetary", "interval"], + "keywords": [ + "money", + "currency", + "schema", + "validation", + "monetary", + "interval" + ], "homepage": "https://github.com/gosuperscript/axiom-money", "require": { "php": "^8.4", "ext-intl": "*", - "brick/money": "^0.10.1", - "gosuperscript/axiom": "^0.6.0", - "superscript/interval": "^1.0", - "superscript/monetary-interval": "^0.1.2" + "brick/money": "^0.14.1", + "gosuperscript/axiom": "^0.6.9", + "superscript/interval": "^2.0", + "superscript/monetary-interval": "^0.2.0" }, "require-dev": { "infection/infection": "^0.29.14", diff --git a/src/MoneyExtension.php b/src/MoneyExtension.php index 460b2b0..38c4fff 100644 --- a/src/MoneyExtension.php +++ b/src/MoneyExtension.php @@ -9,6 +9,7 @@ use Brick\Money\Currency; use Brick\Money\Money; use Superscript\Axiom\Extension; +use Superscript\Axiom\Money\MoneyParser; use Superscript\Axiom\Money\Types\MonetaryIntervalType; use Superscript\Axiom\Money\Types\MonetaryType; use Superscript\Axiom\Operators\Operator; @@ -51,7 +52,7 @@ final class MoneyExtension extends Extension */ public function __construct( private readonly array $currencies, - private readonly RoundingMode $roundingMode = RoundingMode::HALF_UP, + private readonly RoundingMode $roundingMode = RoundingMode::HalfUp, ) {} public function operators(): array @@ -78,7 +79,7 @@ public function operators(): array ->evaluatesWith(fn(int|float $left, Money $right) => $this->scale($right, $left)); $rules[] = Operator::infix('/')->takes($money, $number)->returns($money) ->evaluatesWith(fn(Money $left, int|float $right) => attempt( - fn() => $left->toRational()->dividedBy($right)->to(new DefaultContext(), $this->roundingMode), + fn() => $left->toRational()->dividedBy(MoneyParser::exact($right))->toContext(new DefaultContext(), $this->roundingMode), )); // Ordering: same currency. @@ -94,11 +95,11 @@ public function operators(): array // Equality: Brick's amount-and-currency comparison, not identity. foreach (['=', '==', '==='] as $operator) { $rules[] = Operator::infix($operator)->takes($money, $money)->returns($boolean) - ->evaluatesWith(fn(Money $left, Money $right) => $left->isAmountAndCurrencyEqualTo($right)); + ->evaluatesWith(fn(Money $left, Money $right) => $left->isSameValueAs($right)); } foreach (['!=', '!=='] as $operator) { $rules[] = Operator::infix($operator)->takes($money, $money)->returns($boolean) - ->evaluatesWith(fn(Money $left, Money $right) => !$left->isAmountAndCurrencyEqualTo($right)); + ->evaluatesWith(fn(Money $left, Money $right) => !$left->isSameValueAs($right)); } // A monetary interval compared against a money of its currency. @@ -138,6 +139,7 @@ public function literals(): array private function scale(Money $money, int|float $scalar): Money { - return $money->toRational()->multipliedBy($scalar)->to(new DefaultContext(), $this->roundingMode); + return $money->toRational()->multipliedBy(MoneyParser::exact($scalar))->toContext(new DefaultContext(), $this->roundingMode); } + } diff --git a/src/MoneyParser.php b/src/MoneyParser.php index c8aa1f9..f044a0c 100644 --- a/src/MoneyParser.php +++ b/src/MoneyParser.php @@ -47,7 +47,7 @@ public static function parse(mixed $money): Result if ($amount !== false) { try { - return Ok(Money::of($amount, non_empty_string()->assert($currency))); + return Ok(Money::of(self::exact($amount), non_empty_string()->assert($currency))); } catch (RoundingNecessaryException $exception) { return Err(new InvalidArgumentException(sprintf('Could not parse [%s] as money', new Exporter()->shortenedExport($money)), previous: $exception)); } @@ -58,4 +58,35 @@ public static function parse(mixed $money): Result message: sprintf('Could not parse [%s] as money', new Exporter()->shortenedExport($money)), )); } + + /** + * Renders a raw scalar amount in the form brick accepts. + * + * brick rejects floats throughout its monetary and arithmetic APIs, because + * a binary float carries no exact decimal value to compute with: the literal + * 0.1 is not one tenth. A float that reaches here is therefore spelled as + * the shortest decimal that round-trips back to it, so brick receives the + * number the caller actually holds. + * + * That spelling is `json_encode`'s, because it honours `serialize_precision` + * (-1). A `(string)` cast honours `precision` instead — 14 significant + * digits by default — so it truncates and can emit exponent notation: + * `(string) 123456789012345.67` gives `1.2345678901235E+14`, a different + * amount. Ints and numeric strings are already exact and pass through + * untouched. + * + * @throws InvalidArgumentException if the float is not finite, and so names no decimal at all + */ + public static function exact(string|float|int $amount): string|int + { + if (! is_float($amount)) { + return $amount; + } + + if (! is_finite($amount)) { + throw new InvalidArgumentException(sprintf('[%s] is not a monetary amount.', $amount)); + } + + return json_encode($amount, JSON_THROW_ON_ERROR); + } } diff --git a/src/Types/DynamicMonetaryType.php b/src/Types/DynamicMonetaryType.php index 54bd771..eda12ef 100644 --- a/src/Types/DynamicMonetaryType.php +++ b/src/Types/DynamicMonetaryType.php @@ -16,6 +16,7 @@ use Superscript\Axiom\Types\Shapes\Shape; use Superscript\Axiom\Types\Type; +use function Psl\Type\instance_of; use function Psl\Type\non_empty_string; use function Superscript\Monads\Option\Some; use function Superscript\Monads\Result\Err; @@ -26,7 +27,7 @@ */ final readonly class DynamicMonetaryType implements Type { - public function __construct(public RoundingMode $roundingMode = RoundingMode::HALF_UP) {} + public function __construct(public RoundingMode $roundingMode = RoundingMode::HalfUp) {} /** * @return Result, TransformValueException> @@ -46,7 +47,7 @@ public function assert(mixed $value): Result public function coerce(mixed $value): Result { if ($value instanceof RationalMoney) { - return Ok(Some($value->to(new DefaultContext(), $this->roundingMode))); + return Ok(Some($value->toContext(new DefaultContext(), $this->roundingMode))); } return MoneyParser::parse($value)->map(fn(Money $money) => Some($money)) @@ -55,8 +56,9 @@ public function coerce(mixed $value): Result public function format(mixed $value): string { + $money = instance_of(Money::class)->assert($value); $formatter = new \NumberFormatter('en_GB', \NumberFormatter::CURRENCY); - $result = $formatter->formatCurrency($value->getAmount()->toFloat(), $value->getCurrency()->getCurrencyCode()); + $result = $formatter->formatCurrency($money->getAmount()->toFloat(), $money->getCurrency()->getCurrencyCode()); return non_empty_string()->assert($result); } diff --git a/src/Types/MinorMonetaryType.php b/src/Types/MinorMonetaryType.php index 092acbf..ed4ec5d 100644 --- a/src/Types/MinorMonetaryType.php +++ b/src/Types/MinorMonetaryType.php @@ -13,6 +13,7 @@ use Superscript\Monads\Option\Option; use Superscript\Monads\Result\Result; use Superscript\Axiom\Exceptions\TransformValueException; +use Superscript\Axiom\Money\MoneyParser; use Superscript\Axiom\Types\Shapes\LiteralShape; use Superscript\Axiom\Types\Shapes\OpaqueShape; use Superscript\Axiom\Types\Shapes\Shape; @@ -20,6 +21,7 @@ use function Psl\Type\float; use function Psl\Type\int; +use function Psl\Type\instance_of; use function Psl\Type\non_empty_string; use function Psl\Type\string; use function Psl\Type\union; @@ -33,7 +35,7 @@ */ final readonly class MinorMonetaryType implements Type { - public function __construct(public Currency $currency, public RoundingMode $roundingMode = RoundingMode::HALF_UP) {} + public function __construct(public Currency $currency, public RoundingMode $roundingMode = RoundingMode::HalfUp) {} /** * @return Result, TransformValueException> @@ -44,7 +46,7 @@ public function assert(mixed $value): Result return Err(new TransformValueException(type: 'money', value: $value)); } - if (!$value->getCurrency()->is($this->currency)) { + if (!$value->getCurrency()->isEqualTo($this->currency)) { return Err(new TransformValueException(type: 'money', value: $value)); } @@ -57,16 +59,16 @@ public function assert(mixed $value): Result public function coerce(mixed $value): Result { $candidate = $value instanceof RationalMoney - ? $value->to(new DefaultContext(), $this->roundingMode) + ? $value->toContext(new DefaultContext(), $this->roundingMode) : $value; return (match (true) { - $candidate instanceof Money => $candidate->getCurrency()->is($this->currency) + $candidate instanceof Money => $candidate->getCurrency()->isEqualTo($this->currency) ? Ok($candidate) : Err(new InvalidArgumentException(sprintf("Mismatching currencies: expected %s, got %s", $this->currency->getCurrencyCode(), $candidate->getCurrency()->getCurrencyCode()))), default => attempt(function () use ($candidate) { - union(string(), float(), int())->assert($candidate); - return Money::ofMinor($candidate, $this->currency); + $amount = union(string(), float(), int())->assert($candidate); + return Money::ofMinor(MoneyParser::exact($amount), $this->currency); }), }) ->map(fn(Money $money) => Some($money)) @@ -75,8 +77,9 @@ public function coerce(mixed $value): Result public function format(mixed $value): string { + $money = instance_of(Money::class)->assert($value); $formatter = new \NumberFormatter('en_GB', \NumberFormatter::CURRENCY); - $result = $formatter->formatCurrency($value->getAmount()->toFloat(), $value->getCurrency()->getCurrencyCode()); + $result = $formatter->formatCurrency($money->getAmount()->toFloat(), $money->getCurrency()->getCurrencyCode()); return non_empty_string()->assert($result); } diff --git a/src/Types/MonetaryIntervalType.php b/src/Types/MonetaryIntervalType.php index f9901f8..ffe68b4 100644 --- a/src/Types/MonetaryIntervalType.php +++ b/src/Types/MonetaryIntervalType.php @@ -18,6 +18,7 @@ use Superscript\Axiom\Types\Shapes\Shape; use Superscript\Axiom\Types\Type; +use function Psl\Type\instance_of; use function Superscript\Monads\Option\Some; use function Superscript\Monads\Result\attempt; use function Superscript\Monads\Result\Err; @@ -39,7 +40,7 @@ public function assert(mixed $value): Result return Err(new TransformValueException(type: 'monetary-interval', value: $value)); } - if (!$value->left->getCurrency()->is($this->currency)) { + if (!$value->left->getCurrency()->isEqualTo($this->currency)) { return Err(new TransformValueException(type: 'monetary-interval', value: $value)); } @@ -52,7 +53,7 @@ public function assert(mixed $value): Result public function coerce(mixed $value): Result { return (match (true) { - $value instanceof MonetaryInterval => $value->left->getCurrency()->is($this->currency) + $value instanceof MonetaryInterval => $value->left->getCurrency()->isEqualTo($this->currency) ? Ok($value) : Err(new InvalidArgumentException(sprintf("Mismatching currencies: expected %s, got %s", $this->currency->getCurrencyCode(), $value->left->getCurrency()->getCurrencyCode()))), is_string($value) => attempt(fn() => Interval::fromString($value)) @@ -69,7 +70,7 @@ public function coerce(mixed $value): Result public function format(mixed $value): string { - return (string) $value; + return (string) instance_of(MonetaryInterval::class)->assert($value); } /** diff --git a/src/Types/MonetaryType.php b/src/Types/MonetaryType.php index 1873aa3..f4ba853 100644 --- a/src/Types/MonetaryType.php +++ b/src/Types/MonetaryType.php @@ -13,6 +13,7 @@ use Superscript\Monads\Option\Option; use Superscript\Monads\Result\Result; use Superscript\Axiom\Exceptions\TransformValueException; +use Superscript\Axiom\Money\MoneyParser; use Superscript\Axiom\Types\Shapes\LiteralShape; use Superscript\Axiom\Types\Shapes\OpaqueShape; use Superscript\Axiom\Types\Shapes\Shape; @@ -20,6 +21,7 @@ use function Psl\Type\float; use function Psl\Type\int; +use function Psl\Type\instance_of; use function Psl\Type\non_empty_string; use function Psl\Type\string; use function Psl\Type\union; @@ -33,7 +35,7 @@ */ final readonly class MonetaryType implements Type { - public function __construct(public Currency $currency, public RoundingMode $roundingMode = RoundingMode::HALF_UP) {} + public function __construct(public Currency $currency, public RoundingMode $roundingMode = RoundingMode::HalfUp) {} /** * @return Result, TransformValueException> @@ -44,7 +46,7 @@ public function assert(mixed $value): Result return Err(new TransformValueException(type: 'money', value: $value)); } - if (!$value->getCurrency()->is($this->currency)) { + if (!$value->getCurrency()->isEqualTo($this->currency)) { return Err(new TransformValueException(type: 'money', value: $value)); } @@ -57,16 +59,16 @@ public function assert(mixed $value): Result public function coerce(mixed $value): Result { $candidate = $value instanceof RationalMoney - ? $value->to(new DefaultContext(), $this->roundingMode) + ? $value->toContext(new DefaultContext(), $this->roundingMode) : $value; return (match (true) { - $candidate instanceof Money => $candidate->getCurrency()->is($this->currency) + $candidate instanceof Money => $candidate->getCurrency()->isEqualTo($this->currency) ? Ok($candidate) : Err(new InvalidArgumentException(sprintf("Mismatching currencies: expected %s, got %s", $this->currency->getCurrencyCode(), $candidate->getCurrency()->getCurrencyCode()))), default => attempt(function () use ($candidate) { - union(string(), float(), int())->assert($candidate); - return Money::of($candidate, $this->currency); + $amount = union(string(), float(), int())->assert($candidate); + return Money::of(MoneyParser::exact($amount), $this->currency); }), }) ->map(fn(Money $money) => Some($money)) @@ -75,8 +77,9 @@ public function coerce(mixed $value): Result public function format(mixed $value): string { + $money = instance_of(Money::class)->assert($value); $formatter = new \NumberFormatter('en_GB', \NumberFormatter::CURRENCY); - $result = $formatter->formatCurrency($value->getAmount()->toFloat(), $value->getCurrency()->getCurrencyCode()); + $result = $formatter->formatCurrency($money->getAmount()->toFloat(), $money->getCurrency()->getCurrencyCode()); return non_empty_string()->assert($result); } diff --git a/tests/MoneyExtensionTest.php b/tests/MoneyExtensionTest.php index a2abaac..cc108dc 100644 --- a/tests/MoneyExtensionTest.php +++ b/tests/MoneyExtensionTest.php @@ -16,6 +16,7 @@ use Superscript\Axiom\Dialect; use Superscript\Axiom\Expression; use Superscript\Axiom\Money\MoneyExtension; +use Superscript\Axiom\Money\MoneyParser; use Superscript\Axiom\Money\Types\MonetaryIntervalType; use Superscript\Axiom\Money\Types\MonetaryType; use Superscript\Axiom\Source; @@ -29,9 +30,10 @@ #[CoversClass(MoneyExtension::class)] #[UsesClass(MonetaryType::class)] #[UsesClass(MonetaryIntervalType::class)] +#[UsesClass(MoneyParser::class)] class MoneyExtensionTest extends TestCase { - private function dialect(RoundingMode $rounding = RoundingMode::HALF_UP): Dialect + private function dialect(RoundingMode $rounding = RoundingMode::HalfUp): Dialect { return Dialect::core()->with(new MoneyExtension(['GBP', 'USD', 'EUR'], $rounding)); } @@ -40,7 +42,7 @@ private function dialect(RoundingMode $rounding = RoundingMode::HALF_UP): Dialec * @param array $declarations * @param array $bindings */ - private function evaluate(Source $source, array $declarations, array $bindings, RoundingMode $rounding = RoundingMode::HALF_UP): mixed + private function evaluate(Source $source, array $declarations, array $bindings, RoundingMode $rounding = RoundingMode::HalfUp): mixed { return (new Expression($source, dialect: $this->dialect($rounding), declarations: $declarations)) ->compile()->unwrap()($bindings)->unwrap()->unwrap(); @@ -62,7 +64,7 @@ public function it_adds_two_monies_of_the_same_currency(): void ['a' => Money::of(1, 'GBP'), 'b' => Money::of(2, 'GBP')], ); - $this->assertTrue($result->isAmountAndCurrencyEqualTo(Money::of(3, 'GBP'))); + $this->assertTrue($result->isSameValueAs(Money::of(3, 'GBP'))); } #[Test] @@ -74,7 +76,7 @@ public function it_subtracts_two_monies_of_the_same_currency(): void ['a' => Money::of(5, 'GBP'), 'b' => Money::of(2, 'GBP')], ); - $this->assertTrue($result->isAmountAndCurrencyEqualTo(Money::of(3, 'GBP'))); + $this->assertTrue($result->isSameValueAs(Money::of(3, 'GBP'))); } #[Test] @@ -86,7 +88,7 @@ public function it_multiplies_money_by_a_scalar_on_the_right(): void ['m' => Money::of(10, 'GBP')], ); - $this->assertTrue($result->isAmountAndCurrencyEqualTo(Money::of(20, 'GBP'))); + $this->assertTrue($result->isSameValueAs(Money::of(20, 'GBP'))); } #[Test] @@ -98,7 +100,7 @@ public function it_multiplies_a_scalar_by_money_on_the_left(): void ['m' => Money::of(10, 'GBP')], ); - $this->assertTrue($result->isAmountAndCurrencyEqualTo(Money::of(20, 'GBP'))); + $this->assertTrue($result->isSameValueAs(Money::of(20, 'GBP'))); } #[Test] @@ -110,7 +112,7 @@ public function it_divides_money_by_a_scalar(): void ['m' => Money::of(20, 'GBP')], ); - $this->assertTrue($result->isAmountAndCurrencyEqualTo(Money::of(10, 'GBP'))); + $this->assertTrue($result->isSameValueAs(Money::of(10, 'GBP'))); } #[Test] @@ -125,7 +127,7 @@ public function it_rounds_scalar_arithmetic_back_to_the_currency_scale(RoundingM ['m' => Money::of(10, 'GBP')], $rounding, ); - $this->assertTrue($quotient->isAmountAndCurrencyEqualTo(Money::of($expectedQuotient, 'GBP'))); + $this->assertTrue($quotient->isSameValueAs(Money::of($expectedQuotient, 'GBP'))); $product = $this->evaluate( new InfixExpression(new SymbolSource('m'), '*', new StaticSource(0.6667)), @@ -133,13 +135,13 @@ public function it_rounds_scalar_arithmetic_back_to_the_currency_scale(RoundingM ['m' => Money::of(10, 'GBP')], $rounding, ); - $this->assertTrue($product->isAmountAndCurrencyEqualTo(Money::of($expectedProduct, 'GBP'))); + $this->assertTrue($product->isSameValueAs(Money::of($expectedProduct, 'GBP'))); } public static function roundingCases(): Generator { - yield 'half up' => [RoundingMode::HALF_UP, '1.43', '6.67']; - yield 'down' => [RoundingMode::DOWN, '1.42', '6.66']; + yield 'half up' => [RoundingMode::HalfUp, '1.43', '6.67']; + yield 'down' => [RoundingMode::Down, '1.42', '6.66']; } #[Test] diff --git a/tests/MoneyParserTest.php b/tests/MoneyParserTest.php index c4db6f7..bc5b3eb 100644 --- a/tests/MoneyParserTest.php +++ b/tests/MoneyParserTest.php @@ -5,6 +5,7 @@ namespace Superscript\Axiom\Money\Tests; use Brick\Money\Money; +use InvalidArgumentException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -26,9 +27,9 @@ public static function transformProvider(): array { return [ ['EUR 1', Money::of(1, 'EUR')], - ['£1.23', Money::of(1.23, 'GBP')], - ['USD 100.50', Money::of(100.50, 'USD')], - [Money::of(100.50, 'EUR'), Money::of(100.50, 'EUR')], + ['£1.23', Money::of('1.23', 'GBP')], + ['USD 100.50', Money::of('100.50', 'USD')], + [Money::of('100.50', 'EUR'), Money::of('100.50', 'EUR')], ]; } @@ -55,4 +56,33 @@ public static function errors(): array ['GBP'], ]; } + + #[DataProvider('exactAmounts')] + #[Test] + public function it_renders_a_raw_amount_in_the_form_brick_accepts(string|float|int $amount, string|int $expected) + { + $this->assertSame($expected, MoneyParser::exact($amount)); + } + + public static function exactAmounts(): array + { + return [ + 'a float becomes its shortest round-tripping decimal' => [1.23, '1.23'], + 'a float with a trailing zero drops it' => [100.50, '100.5'], + 'a whole float keeps no decimal point' => [2.0, '2'], + // Past 14 significant digits a (string) cast truncates to 1.2345678901235E+14. + 'a float past the default precision keeps every digit' => [123456789012345.67, '123456789012345.67'], + 'a float that is all residue keeps it' => [0.30000000000000004, '0.30000000000000004'], + 'an int passes through as an int' => [1234, 1234], + 'a numeric string passes through untouched' => ['1234.5678', '1234.5678'], + ]; + } + + #[Test] + public function it_refuses_a_float_that_names_no_amount(): void + { + $this->expectException(InvalidArgumentException::class); + + MoneyParser::exact(INF); + } } diff --git a/tests/Types/DynamicMonetaryTypeTest.php b/tests/Types/DynamicMonetaryTypeTest.php index 3c22638..d00e9f0 100644 --- a/tests/Types/DynamicMonetaryTypeTest.php +++ b/tests/Types/DynamicMonetaryTypeTest.php @@ -33,8 +33,8 @@ public static function transformProvider(): array { return [ ['EUR 1', Money::of(1, 'EUR')], - ['£1.23', Money::of(1.23, 'GBP')], - ['USD 100.50', Money::of(100.50, 'USD')], + ['£1.23', Money::of('1.23', 'GBP')], + ['USD 100.50', Money::of('100.50', 'USD')], [Money::of(100, 'EUR'), Money::of(100, 'EUR')], ]; } @@ -63,7 +63,7 @@ public function it_honors_a_custom_rounding_mode_when_coercing_rational_money(): { $twoThirds = RationalMoney::of(2, 'EUR')->dividedBy(3); // 0.666... - $this->assertTrue((new DynamicMonetaryType(RoundingMode::DOWN))->coerce($twoThirds)->unwrap()->unwrap()->isEqualTo(Money::of('0.66', 'EUR'))); + $this->assertTrue((new DynamicMonetaryType(RoundingMode::Down))->coerce($twoThirds)->unwrap()->unwrap()->isEqualTo(Money::of('0.66', 'EUR'))); } #[Test] diff --git a/tests/Types/MinorMonetaryTypeTest.php b/tests/Types/MinorMonetaryTypeTest.php index fe2c560..38f62c4 100644 --- a/tests/Types/MinorMonetaryTypeTest.php +++ b/tests/Types/MinorMonetaryTypeTest.php @@ -83,7 +83,7 @@ public function it_rounds_a_rational_money_to_the_currency_scale_on_coerce(): vo #[Test] public function it_honors_a_custom_rounding_mode_when_coercing_rational_money(): void { - $down = new MinorMonetaryType(Currency::of('EUR'), RoundingMode::DOWN); + $down = new MinorMonetaryType(Currency::of('EUR'), RoundingMode::Down); $twoThirds = RationalMoney::of(2, 'EUR')->dividedBy(3); // 0.666... $this->assertTrue($down->coerce($twoThirds)->unwrap()->unwrap()->isEqualTo(Money::of('0.66', 'EUR'))); diff --git a/tests/Types/MonetaryTypeTest.php b/tests/Types/MonetaryTypeTest.php index 1461ddf..b41323c 100644 --- a/tests/Types/MonetaryTypeTest.php +++ b/tests/Types/MonetaryTypeTest.php @@ -37,11 +37,11 @@ public static function transformProvider(): array [150, 'EUR', Money::of(150, 'EUR')], ['150', 'EUR', Money::of(150, 'EUR')], [150, 'GBP', Money::of(150, 'GBP')], - [1234.56, 'EUR', Money::of(1234.56, 'EUR')], - ['1234.56', 'EUR', Money::of(1234.56, 'EUR')], + [1234.56, 'EUR', Money::of('1234.56', 'EUR')], + ['1234.56', 'EUR', Money::of('1234.56', 'EUR')], ['100.000', 'EUR', Money::of(100, 'EUR')], - ['1234.567', 'IQD', Money::of(1234.567, 'IQD')], - ['1234.5678', 'UYW', Money::of(1234.5678, 'UYW')], + ['1234.567', 'IQD', Money::of('1234.567', 'IQD')], + ['1234.5678', 'UYW', Money::of('1234.5678', 'UYW')], [Money::of(100, 'EUR'), 'EUR', Money::of(100, 'EUR')], // An exactly-representable RationalMoney coerces without rounding. [RationalMoney::of(100, 'EUR'), 'EUR', Money::of(100, 'EUR')], @@ -63,10 +63,10 @@ public function it_honors_a_custom_rounding_mode_when_coercing_rational_money(): { $rational = RationalMoney::of(2, 'EUR')->dividedBy(3); // 0.666... - $halfUp = new MonetaryType(Currency::of('EUR'), RoundingMode::HALF_UP); + $halfUp = new MonetaryType(Currency::of('EUR'), RoundingMode::HalfUp); $this->assertTrue($halfUp->coerce($rational)->unwrap()->unwrap()->isEqualTo(Money::of('0.67', 'EUR'))); - $down = new MonetaryType(Currency::of('EUR'), RoundingMode::DOWN); + $down = new MonetaryType(Currency::of('EUR'), RoundingMode::Down); $this->assertTrue($down->coerce($rational)->unwrap()->unwrap()->isEqualTo(Money::of('0.66', 'EUR'))); } @@ -128,8 +128,8 @@ public function it_can_format_value(Money $value, string $expected): void public static function formatProvider(): array { return [ - [Money::of(1234.56, 'EUR'), '€1,234.56'], - [Money::of(1234.56, 'GBP'), '£1,234.56'], + [Money::of('1234.56', 'EUR'), '€1,234.56'], + [Money::of('1234.56', 'GBP'), '£1,234.56'], [Money::of(1000000, 'EUR'), '€1,000,000.00'], ]; }