caffeine / money
Money as integer nanos, with locale-aware formatting via ext-intl.
Requires
- php: ^8.1
- ext-intl: *
Requires (Dev)
- carthage-software/mago: *
- infection/infection: ^0.29
- phpunit/phpunit: ^9.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Nano-precision currency conversion and locale-aware formatting for PHP.
Money as integer nanos (10⁹ per major unit), so your arithmetic stays in ints. Formatting goes through ext-intl's NumberFormatter. You hold the ints, Nano converts and formats them.
Use
brick/moneyormoneyphp/moneyif you can. This is for when you can't, yet.
Requirements
- PHP 8.1+
ext-intl
Installation
composer require caffeine/money
Usage
Creating an instance
use Caffeine\Money\Nano; // Reader's locale, invoice's currency $nano = new Nano('de_DE', 'USD'); // Locale's default currency, store it $currency = Nano::currencyForLocale('de_DE'); // "EUR"
Locale controls formatting, currency controls digits and symbol. Throws InvalidArgumentException if the currency isn't 3 letters.
currencyForLocale uses whatever the installed ICU says, and that changes (bg_BG went BGN to EUR in 2026), so store the result. Throws for locales with no currency, like de or es_419.
If ICU has no data for a language it falls back to the server's locale. getLocale() shows what it used.
Converting to nanos
$nano = new Nano('en_US', 'USD'); $nano->fromMajor(19.99); // 19_990_000_000 (dollars → nanos) $nano->fromMajor('19.99'); // 19_990_000_000 (e.g. a DECIMAL column) $nano->fromMinor(1999); // 19_990_000_000 (cents → nanos) $nano->toNano(1.5); // 1_500_000_000 (raw, no currency snapping)
Strings must be numeric, . as decimal point in any locale. '$19.99' throws.
Converting from nanos
$nano->toMajor(19_990_000_000); // 19.99 (float) $nano->toMinor(19_990_000_000); // 1999 (int)
Formatting
(new Nano('en_US', 'USD'))->formatCurrency(19_990_000_000); // "$19.99" (new Nano('en_US', 'USD'))->formatDecimal(19_990_000_000); // "19.99" (new Nano('ja_JP', 'JPY'))->formatCurrency(2_000_000_000_000); // "¥2,000" (new Nano('de_DE', 'USD'))->formatCurrency(19_990_000_000); // "19,99 $"
Rounding
Default is PHP_ROUND_HALF_UP, pass a mode to override:
$nano = new Nano('en_US', 'USD'); $nano->fromMajor(19.995, PHP_ROUND_HALF_EVEN); $nano->snapToMinor(19_995_000_000, PHP_ROUND_HALF_DOWN);
Snapping
snapToMinor rounds nanos to the nearest minor unit (cent for USD, fil for KWD):
$nano = new Nano('en_US', 'USD'); $nano->snapToMinor(19_995_000_000); // 20_000_000_000 ($19.995 → $20.00) $nano = new Nano('en_US', 'KWD'); // 3 decimal places $nano->snapToMinor(1_500_500_000); // 1_501_000_000 (1.5005 → 1.501 KWD)
snapToNano rounds float nanos to an int, e.g. after multiplying by a tax rate:
$nano->snapToNano(1_000_000_000.6); // 1_000_000_001
Getters
$nano->getLocale(); // "en_US" $nano->getCurrency(); // "USD" $nano->getFractionDigits(); // 2
Fraction digits come from ICU and don't always match ISO 4217 (HUF 0 vs 2, IQD 0 vs 3). Check before sending toMinor() to a payment API.
Safe operating range
Nanos are PHP ints, so 64-bit only. Safe range is ±9,000,000,000 major units (±9×10¹⁸ nanos). Outside it you get InvalidArgumentException.
toMinor, snapToMinor, snapToNano and toMajor use integer math and are exact across the whole range. Anything that starts as a float, including numeric strings in fromMajor, is only nano-exact up to ±9,007,199 major units (2⁵³ nanos). Past that, amounts within ~0.000001 of a half cent can round either way.
License
0BSD - do whatever you want, no attribution required.