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