// A runnable tour of MoonMoney.
//
// moon run examples/demo
//
// Each section prints what the library computes, including the parts that are
// easy to get wrong with floating point: percentages, tax, splitting a bill and
// converting between currencies.
///|
fn usd() -> @moonmoney.Currency {
@moonmoney.Currency::find("USD").unwrap()
}
///|
fn amount(minor : Int64) -> @moonmoney.Money {
@moonmoney.Money::of_minor(minor, usd())
}
///|
fn heading(title : String) -> Unit {
println("")
println("== \{title} ==")
}
///|
/// Renders an amount, failing loudly if the library reports an error, which in
/// the demo would mean a bug rather than bad input.
fn show(money : @moonmoney.Money, style : @format.MoneyStyle) -> String {
@format.format(money, style~) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
}
///|
fn percent(text : String) -> @moonmoney.Percent {
@moonmoney.Percent::parse(text) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
}
///|
fn exchange_rate(text : String) -> @moonmoney.Rate {
@moonmoney.Rate::parse(text) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
}
///|
fn read(text : String) -> @moonmoney.Money? {
Some(@parse.amount(text)) catch {
_ => None
}
}
///|
fn read_result(text : String) -> String {
match read(text) {
Some(money) => show(money, @format.MoneyStyle::code_first())
None => "refused"
}
}
///|
/// Reads an amount in a stated currency; used for the inputs that carry no
/// currency of their own.
fn read_in_result(text : String, code : String) -> String {
let currency = @moonmoney.Currency::find(code).unwrap()
let money = @parse.amount_in(text, currency) catch { _ => return "refused" }
show(money, @format.MoneyStyle::code_first())
}
///|
fn main {
println("MoonMoney: exact money arithmetic on ISO 4217 currencies")
heading("the registry")
println(
"\{@moonmoney.Currency::count()} currencies with a defined minor unit",
)
for code in ["USD", "JPY", "BHD", "CLF"] {
let currency = @moonmoney.Currency::find(code).unwrap()
println(
" \{currency.label()}: \{currency.exponent} decimal place(s), symbol \{currency.display_symbol()}",
)
}
println("cash rounding:")
for code in ["CHF", "DKK", "SEK", "USD"] {
let currency = @moonmoney.Currency::find(code).unwrap()
let step = @moonmoney.Money::of_minor(currency.cash_increment(), currency)
let note = if currency.has_cash_rounding() {
""
} else {
" (no special step)"
}
println(
" \{code} settles to \{show(step, @format.MoneyStyle::code_first())}\{note}",
)
}
heading("reading amounts from text")
for text in ["USD 1,234.56", "CA$12.34", "1.234,56 €", "¥1,234"] {
println(" \{text} -> \{read_result(text)}")
}
println(" and when the text names no currency, the caller declares one:")
for text in ["(5.00)", "5.00-", "1.234", "1.2345"] {
println(" \{text} as USD -> \{read_in_result(text, "USD")}")
}
heading("why not a float")
println(" 0.1 + 0.2 as a Double is \{0.1 + 0.2}")
let mut cents = amount(0L)
for _ in 0..<100 {
cents = cents.add(amount(1L)) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
}
println(
" 100 additions of 0.01 USD are \{show(cents, @format.MoneyStyle::standard())}",
)
heading("rounding, per mode")
let odd = @moonmoney.Money::of_minor(1001L, usd())
for
mode in [
@moonmoney.HalfUp,
@moonmoney.HalfEven,
@moonmoney.HalfDown,
@moonmoney.Up,
@moonmoney.Down,
@moonmoney.Ceiling,
@moonmoney.Floor,
] {
let half = @moonmoney.Money::of_minor(-1001L, usd())
let positive = odd.div_int64(2L, mode~) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
let negative = half.div_int64(2L, mode~) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
let positive_text = show(positive, @format.MoneyStyle::plain())
let negative_text = show(negative, @format.MoneyStyle::plain())
println(
" \{mode.label()}: 10.01 -> \{positive_text}, -10.01 -> \{negative_text}",
)
}
heading("splitting a bill without losing a cent")
let bill = amount(10000L)
let thirds = bill.allocate(3) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
println(" 100.00 three ways: \{share_text(thirds)}")
let weighted = bill.allocate_ratios([3L, 2L, 1L]) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
println(" 100.00 as 3:2:1: \{share_text(weighted)}")
let quartered = amount(9999L).allocate_percents([
@moonmoney.Percent::of_bp(2500L),
@moonmoney.Percent::of_bp(2500L),
@moonmoney.Percent::of_bp(5000L),
]) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
println(" 99.99 as 25/25/50: \{share_text(quartered)}")
heading("tax")
let net = amount(10000L)
let tax_rate = percent("19%")
let gross = net.with_tax(tax_rate) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
println(
" net \{show(net, plan_style())} at 19% grosses to \{show(gross, plan_style())}",
)
let (back_net, tax) = gross.net_from_gross(tax_rate) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
println(
" and back: net \{show(back_net, plan_style())}, tax \{show(tax, plan_style())}",
)
let lines = net.tax_lines([percent("7%"), percent("19%")]) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
for line in lines {
println(
" at \{line.rate.text()}: tax \{show(line.tax, plan_style())}, gross \{show(line.gross, plan_style())}",
)
}
heading("exchange")
let eur = @moonmoney.Currency::find("EUR").unwrap()
let rate_eur = exchange_rate("0.85")
let converted = amount(10000L).convert(eur, rate_eur) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
println(" 100.00 USD at 0.85 is \{show(converted, plan_style())}")
let jpy = @moonmoney.Currency::find("JPY").unwrap()
let rate_jpy = exchange_rate("150")
let yen = amount(10000L).convert(jpy, rate_jpy) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
println(" 100.00 USD at 150 is \{show(yen, plan_style())}")
heading("the same amount, written the local way")
let money = amount(123456L)
for locale in ["en-US", "de-DE", "fr-FR", "en-IN", "sv-SE"] {
let style = @format.MoneyStyle::for_locale(locale).unwrap()
let inr_style = if locale == "en-IN" {
@format.format(
@moonmoney.Money::of_minor(
12345678L,
@moonmoney.Currency::find("INR").unwrap(),
),
style~,
) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
} else {
show(money, style)
}
println(" \{locale}: \{inr_style}")
}
}
///|
/// `$12.34` with the symbol in front, which reads better in a list than the
/// code-first form.
fn plan_style() -> @format.MoneyStyle {
@format.MoneyStyle::standard()
}
///|
/// The shares of a split, with their total, so that the invariant is visible.
fn share_text(shares : Array[@moonmoney.Money]) -> String {
let parts : Array[String] = []
for share in shares {
parts.push(show(share, @format.MoneyStyle::plain()))
}
let total = @moonmoney.Money::sum(shares, usd()) catch {
error => abort(@moonmoney.MoneyError::message(error))
}
parts.join(" + ") + " = " + show(total, @format.MoneyStyle::plain())
}