Arithmetic & comparison
Arithmetic and comparisons work within one currency — mixing currencies throws CurrencyMismatch, and conversion is always an explicit step. Lossless operations never round; lossy ones round exactly once, defaulting to RoundingMode::HalfAwayFromZero:
$price = Money::ofMajor('19.99', 'EUR');
$fee = Money::ofMinor(250, 'EUR');
$total = $price->add($fee)->multiply(3); // 67.47 EUR, exact
$net = $total->subtract($fee); // 64.97 EUR
$vat = $total->percentage('20'); // 13.49 EUR — one rounding, HalfAwayFromZero
$net->multiply('1.19'); // 77.31 EUR — a decimal factor rounds once
$total->divide(4); // 16.87 EUR
Money::ofMajor('10.03', 'CHF')->roundTo(5); // 10.05 CHF — 5-Rappen cash rounding
$total->negate(); // -67.47 EUR
$total->negate()->abs(); // 67.47 EUR
$total->add(Money::ofMajor('1', 'USD')); // throws CurrencyMismatch- add() / subtract() — variadic, exact, never round.
- multiply($factor, $rounding) — an integer factor is exact; a decimal string ('1.19') or a Ratio rounds once.
- divide($divisor, $rounding) — rounds once; zero throws DivisionByZero.
- percentage($percent, $rounding) — this × p / 100, rounded once.
- roundTo($minorIncrement, $rounding) — cash rounding to a multiple of the increment, e.g. roundTo(5) for Swiss 5-Rappen steps.
- negate() / abs() — never round.
Factors and divisors must be plain decimals or integers — '1e3', '1,5' or ' 2' throw InvalidAmount. Results are capped at 65 digits.
Comparison
$a = Money::ofMajor('50', 'EUR');
$b = Money::ofMajor('67.47', 'EUR');
$b->isGreaterThan($a); // true
$b->isGreaterThanOrEqualTo($a); // true
$a->isLessThan($b); // true
$a->isLessThanOrEqualTo($b); // true
$a->compareTo($b); // -1
$a->equals(Money::ofMinor(5000, 'EUR')); // true
$a->equals(Money::ofMajor('50', 'USD')); // false — equals() never throws
$a->isSameCurrency($b); // true
$a->isZero(); $a->isPositive(); $a->isNegative();
$a->isGreaterThan(Money::ofMajor('1', 'USD')); // throws CurrencyMismatchequals() compares the amount, the currency code and the exponent, and never throws. Every ordering method throws CurrencyMismatch across currencies. Comparisons run on the canonical strings — no bcmath, no float.
Aggregates
$prices = [
Money::ofMajor('10', 'EUR'),
Money::ofMajor('25.50', 'EUR'),
Money::ofMajor('4.99', 'EUR'),
];
Money::sum($prices); // 40.49 EUR
Money::sum([], currencyIfEmpty: 'EUR'); // 0.00 EUR
Money::min($prices); // 4.99 EUR
Money::max($prices); // 25.50 EUR
Money::average($prices); // 13.50 EUR — rounded oncesum() of an empty list needs currencyIfEmpty, otherwise it throws EmptyMoneyCollection — as do min(), max() and average() of nothing. Non-Money items throw InvalidMoneyValue; mixed currencies throw CurrencyMismatch.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.