Currency Conversion
Exchange-rate math — conversions, cross-rates, multi-currency totals — without floating-point drift.
Currency Conversion
Exchange rates are decimals, and money is precision-sensitive — exactly where native floating-point bites. Pass amounts and rates as strings and let @wzo/calc keep every cent exact.
Basic Conversion
// USD → CNY at 7.2345
const usd = 250.75
const rate = 7.2345
calc(`${usd} * ${rate}`, { _fmt: { decimals: 2, thousands: true, rounding: 'round' } })
// 250.75 * 7.2345 = 1814.050875 → "1,814.05"
Why It Matters
A single conversion can already drift in native JS:
19.99 * 1.1 // 21.989000000000004 ← native float
fmt('19.99 * 1.1', { decimals: 2, rounding: 'round' }) // "21.99" ✅ exact
Cross-Rate via a Base Currency
No direct EUR→JPY rate? Compose through USD by chaining the two rates:
// EUR → JPY via USD: ×1.08 (EUR→USD), ×150 (USD→JPY)
const eur = 500
chainMul(eur, 1.08, 150)({ decimals: 0, thousands: true })
// 500 * 1.08 * 150 = "81,000"
Multi-Currency Portfolio
Convert each holding to a common currency, then total with calcSum (high-precision, no large-number drift):
const holdings = [
{ ccy: 'EUR', amount: 1200, toUsd: 1.08 },
{ ccy: 'GBP', amount: 800, toUsd: 1.27 },
{ ccy: 'JPY', amount: 150000, toUsd: 0.0067 },
]
// Value each holding in USD (string-returning, exact)
const inUsd = holdings.map(h => ({
...h,
usd: mulStr(String(h.amount), String(h.toUsd)),
}))
// EUR 1296 · GBP 1016 · JPY 1005
fmt(calcSum('usd', inUsd), { decimals: 2, thousands: true }) // "3,317.00"
Inverse Rate
The reverse rate is a repeating decimal — control how many places you keep:
// USD→CNY is 7.2345, so CNY→USD = 1 / 7.2345
rawDiv('1', '7.2345', 6) // "0.138227"
Always pass rates and amounts as strings (
'7.2345', not 7.2345 after arithmetic). A rate written as a number literal is fine, but a rate produced by an earlier floating-point calculation is already corrupted before it reaches the library — see Caveats.