
Immutable monetary value object bundling amount, currency code and decimals; high-precision decimal arithmetic, fiat and crypto support, exact atomic conversions.
Moneta is an immutable monetary amount that carries its own Currency. It is built on
dev.voir:decimal, so amounts are exact decimals with no
floating-point artifacts, for fiat and crypto alike.
Targets: JVM, Android, iOS (arm64, x64, simulator arm64), and macOS arm64.
dependencies {
implementation("dev.voir:moneta:2.0.0")
}dev.voir:decimal is exposed as an api dependency, so Decimal and Rounding are available
to your code as well.
Every Moneta holds two invariants:
IllegalArgumentException.Currency.decimals. Factories and every
arithmetic operation round to that scale (Rounding.HALF_UP unless you pass another mode), so
an amount always converts exactly to atomic units such as cents, satoshis, or wei.Arithmetic and comparison require the same Currency on both sides; mixing currencies throws
IllegalArgumentException. Equality is numeric and includes the currency: 1.5 USD == 1.50 USD,
but 1 USD != 1 EUR.
val usd = Currency(decimals = 2, code = "USD", symbol = "$")
val jpy = Currency(decimals = 0, code = "JPY", symbol = "¥")
val btc = Currency(decimals = 8, code = "BTC", symbol = "₿")
val eth = Currency(decimals = 18, code = "ETH")decimals must not be negative. code and symbol are optional metadata; all three properties
take part in currency equality.
// Decimal text: preferred for user input, JSON, and CSV
Moneta.fromDecimalString("1.235", usd) // 1.24 USD
Moneta.fromDecimalString("1.239", usd, Rounding.DOWN) // 1.23 USD
"19.99".toMoneta(usd) // 19.99 USD
// Whole units
Moneta.fromInt(10, usd) // 10.00 USD
5L.toMoneta(btc) // 5.00000000 BTC
// Atomic units (cents, satoshis, wei)
Moneta.fromAtomicLong(150_000_000L, btc) // 1.5 BTC
Moneta.fromAtomicString("1000000000000000000", eth) // 1 ETH
150.toAtomicMoneta(usd) // 1.50 USD
// Existing Decimal values
Moneta.fromDecimal(Decimal.parse("0.333"), usd) // 0.33 USD
// Floating point: uses the printed digits, so 0.1 stays 0.1
Moneta.fromDouble(0.1, usd) // 0.10 USD
// Zero
Moneta.zero(usd) // 0.00 USDInvalid text throws IllegalArgumentException, as do NaN and infinite floating-point values.
val a = Moneta.fromDecimalString("10.00", usd)
val b = Moneta.fromDecimalString("2.50", usd)
a + b // 12.50 USD
a - b // 7.50 USD
b - a // throws: the result would be negative
b * 3 // 7.50 USD
a * Decimal.parse("0.075") // 0.75 USD (rounded to cents)
a / 3 // 3.33 USD (rounded to cents)
a.divide(Decimal.fromInt(3), Rounding.UP) // 3.34 USD
a > b // true
listOf(a, b).sorted() // [2.50 USD, 10.00 USD]
a.isZero() // falseRounded division does not add back up: 10.00 / 3 * 3 == 9.99. Use split to share an amount;
the parts always add up exactly, and leftover atomic units go to the first parts.
Moneta.fromInt(10, usd).split(3) // [3.34, 3.33, 3.33] USDval amount = Moneta.zero(usd)
.withDecimalString("2.40") // 2.40 USD
.withCurrency(btc) // 2.40000000 BTC (re-labels, no exchange rate)
amount.withValue(Decimal.parse("9.99"))
amount.withAtomicLong(1234L)withCurrency only re-labels an amount; use exchange-rate conversion to convert it.
Rates are expressed as target units per one source unit and must be positive.
val myr = Currency(decimals = 2, code = "MYR")
Moneta.fromInt(1, usd).convertByRate(Decimal.parse("4.6"), myr) // 4.60 MYR
val from = Moneta.fromDecimalString("2.00", usd)
val to = Moneta.fromDecimalString("9.20", myr)
calculateExchangeRate(from, to) // 4.6 (MYR per USD)
calculateReverseExchangeRate(Decimal.parse("4.6"), scale = 4) // 0.2174
val (direct, reverse) = calculateExchangeRatesPair(from, to, scale = 6) // 4.6, 0.217391calculateExchangeRatesPair computes both rates from the amounts, so the reverse rate never
inherits rounding from the direct one. Choose a scale large enough for your currencies; very
small rates round to zero when the scale is too low.
val m = Moneta.fromDecimalString("1234567.5", usd)
m.toDecimalString() // "1234567.5"
m.toDecimalString(scale = 2) // "1234567.50"
m.toFormattedString() // "1 234 567.5"
m.toFormattedString(decimals = 2, groupSeparator = '.', decimalSeparator = ',') // "1.234.567,50"
m.toFormattedString(appendSymbol = true) // "1 234 567.5$"
m.toAtomicString() // "123456750"
m.toAtomicLong() // 123456750, throws ArithmeticException on Long overflow
m.toAtomicLongOrNull() // 123456750, or null on Long overflow
m.toString() // "Moneta(1234567.50 USD)" — for logs, not for UIPersist amounts with toAtomicString() (or toAtomicLong()) together with the currency, and
restore them with fromAtomicString(...); the round trip is exact.
This project is licensed under the Apache License, Version 2.0. See LICENSE.
Moneta is an immutable monetary amount that carries its own Currency. It is built on
dev.voir:decimal, so amounts are exact decimals with no
floating-point artifacts, for fiat and crypto alike.
Targets: JVM, Android, iOS (arm64, x64, simulator arm64), and macOS arm64.
dependencies {
implementation("dev.voir:moneta:2.0.0")
}dev.voir:decimal is exposed as an api dependency, so Decimal and Rounding are available
to your code as well.
Every Moneta holds two invariants:
IllegalArgumentException.Currency.decimals. Factories and every
arithmetic operation round to that scale (Rounding.HALF_UP unless you pass another mode), so
an amount always converts exactly to atomic units such as cents, satoshis, or wei.Arithmetic and comparison require the same Currency on both sides; mixing currencies throws
IllegalArgumentException. Equality is numeric and includes the currency: 1.5 USD == 1.50 USD,
but 1 USD != 1 EUR.
val usd = Currency(decimals = 2, code = "USD", symbol = "$")
val jpy = Currency(decimals = 0, code = "JPY", symbol = "¥")
val btc = Currency(decimals = 8, code = "BTC", symbol = "₿")
val eth = Currency(decimals = 18, code = "ETH")decimals must not be negative. code and symbol are optional metadata; all three properties
take part in currency equality.
// Decimal text: preferred for user input, JSON, and CSV
Moneta.fromDecimalString("1.235", usd) // 1.24 USD
Moneta.fromDecimalString("1.239", usd, Rounding.DOWN) // 1.23 USD
"19.99".toMoneta(usd) // 19.99 USD
// Whole units
Moneta.fromInt(10, usd) // 10.00 USD
5L.toMoneta(btc) // 5.00000000 BTC
// Atomic units (cents, satoshis, wei)
Moneta.fromAtomicLong(150_000_000L, btc) // 1.5 BTC
Moneta.fromAtomicString("1000000000000000000", eth) // 1 ETH
150.toAtomicMoneta(usd) // 1.50 USD
// Existing Decimal values
Moneta.fromDecimal(Decimal.parse("0.333"), usd) // 0.33 USD
// Floating point: uses the printed digits, so 0.1 stays 0.1
Moneta.fromDouble(0.1, usd) // 0.10 USD
// Zero
Moneta.zero(usd) // 0.00 USDInvalid text throws IllegalArgumentException, as do NaN and infinite floating-point values.
val a = Moneta.fromDecimalString("10.00", usd)
val b = Moneta.fromDecimalString("2.50", usd)
a + b // 12.50 USD
a - b // 7.50 USD
b - a // throws: the result would be negative
b * 3 // 7.50 USD
a * Decimal.parse("0.075") // 0.75 USD (rounded to cents)
a / 3 // 3.33 USD (rounded to cents)
a.divide(Decimal.fromInt(3), Rounding.UP) // 3.34 USD
a > b // true
listOf(a, b).sorted() // [2.50 USD, 10.00 USD]
a.isZero() // falseRounded division does not add back up: 10.00 / 3 * 3 == 9.99. Use split to share an amount;
the parts always add up exactly, and leftover atomic units go to the first parts.
Moneta.fromInt(10, usd).split(3) // [3.34, 3.33, 3.33] USDval amount = Moneta.zero(usd)
.withDecimalString("2.40") // 2.40 USD
.withCurrency(btc) // 2.40000000 BTC (re-labels, no exchange rate)
amount.withValue(Decimal.parse("9.99"))
amount.withAtomicLong(1234L)withCurrency only re-labels an amount; use exchange-rate conversion to convert it.
Rates are expressed as target units per one source unit and must be positive.
val myr = Currency(decimals = 2, code = "MYR")
Moneta.fromInt(1, usd).convertByRate(Decimal.parse("4.6"), myr) // 4.60 MYR
val from = Moneta.fromDecimalString("2.00", usd)
val to = Moneta.fromDecimalString("9.20", myr)
calculateExchangeRate(from, to) // 4.6 (MYR per USD)
calculateReverseExchangeRate(Decimal.parse("4.6"), scale = 4) // 0.2174
val (direct, reverse) = calculateExchangeRatesPair(from, to, scale = 6) // 4.6, 0.217391calculateExchangeRatesPair computes both rates from the amounts, so the reverse rate never
inherits rounding from the direct one. Choose a scale large enough for your currencies; very
small rates round to zero when the scale is too low.
val m = Moneta.fromDecimalString("1234567.5", usd)
m.toDecimalString() // "1234567.5"
m.toDecimalString(scale = 2) // "1234567.50"
m.toFormattedString() // "1 234 567.5"
m.toFormattedString(decimals = 2, groupSeparator = '.', decimalSeparator = ',') // "1.234.567,50"
m.toFormattedString(appendSymbol = true) // "1 234 567.5$"
m.toAtomicString() // "123456750"
m.toAtomicLong() // 123456750, throws ArithmeticException on Long overflow
m.toAtomicLongOrNull() // 123456750, or null on Long overflow
m.toString() // "Moneta(1234567.50 USD)" — for logs, not for UIPersist amounts with toAtomicString() (or toAtomicLong()) together with the currency, and
restore them with fromAtomicString(...); the round trip is exact.
This project is licensed under the Apache License, Version 2.0. See LICENSE.