// The MoonMoney command line.
//
// moon run cmd/main -- parse "USD 1,234.56"
// moon run cmd/main -- format 123456 USD de-DE
// moon run cmd/main -- split 10000 USD 3
// moon run cmd/main -- tax 10000 USD 19%
// moon run cmd/main -- convert 10000 USD EUR 0.85
// moon run cmd/main -- currencies C
//
// Amounts on the command line are given in minor units, so 12.34 USD is 1234.
// Every argument is taken literally; there is no configuration file and no
// network access.
///|
const USAGE : String =
#|moonmoney - exact money arithmetic on ISO 4217 currencies
#|
#|usage:
#| moonmoney parse
#| moonmoney format [locale|plain|code|accounting]
#| moonmoney split
#| moonmoney split weights ...
#| moonmoney tax
#| moonmoney convert
#| moonmoney currencies [code-prefix]
#| moonmoney version
#|
#|notes:
#| is the amount in minor units: 12.34 USD is 1234, 500 yen is 500.
#| A rate is a decimal such as 0.85 or 150.
#|
#|examples:
#| moonmoney parse "CA$12.34"
#| moonmoney format 123456 USD en-IN
#| moonmoney split 10000 USD 3
#| moonmoney split 10000 USD weights 3 2 1
#| moonmoney tax 10000 USD 19%
#| moonmoney convert 10000 USD EUR 0.85
#|
///|
fn currency_of(code : String) -> @moonmoney.Currency {
match @moonmoney.Currency::find(code) {
Some(currency) => currency
None => abort("moonmoney: unknown currency \{code}")
}
}
///|
fn integer_of(text : String) -> Int64 {
@string.parse_int64(text) catch {
_ => abort("moonmoney: \{text} is not a whole number")
}
}
///|
fn rate_of(text : String) -> @moonmoney.Rate {
@moonmoney.Rate::parse(text) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
}
///|
fn percent_of(text : String) -> @moonmoney.Percent {
@moonmoney.Percent::parse(text) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
}
///|
fn render(money : @moonmoney.Money, style : @format.MoneyStyle) -> String {
@format.format(money, style~) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
}
///|
fn style_named(name : String) -> @format.MoneyStyle {
match name {
"plain" => @format.MoneyStyle::plain()
"code" => @format.MoneyStyle::code_first()
"accounting" => @format.MoneyStyle::accounting()
_ =>
match @format.MoneyStyle::for_locale(name) {
Some(style) => style
None => abort("moonmoney: no style or locale named \{name}")
}
}
}
///|
fn main {
let argv = @env.args()
let args : Array[String] = []
for index in 1.. {
if args.length() != 2 {
abort("moonmoney: parse takes one amount")
}
let money = @parse.amount(args[1]) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
println(render(money, @format.MoneyStyle::code_first()))
println(" minor units: \{money.minor}")
println(" currency: \{money.currency.label()}")
}
"format" => {
if args.length() < 3 || args.length() > 4 {
abort("moonmoney: format takes [locale]")
}
let money = @moonmoney.Money::of_minor(
integer_of(args[1]),
currency_of(args[2]),
)
let style = if args.length() == 4 {
style_named(args[3])
} else {
@format.MoneyStyle::standard()
}
println(render(money, style))
}
"split" => {
let currency = if args.length() >= 3 {
currency_of(args[2])
} else {
abort("moonmoney: split takes ...")
}
if args.length() < 4 {
abort("moonmoney: split takes ")
}
let money = @moonmoney.Money::of_minor(integer_of(args[1]), currency)
let shares = if args[3] == "weights" {
let weights : Array[Int64] = []
for index in 4.. abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
} else {
money.allocate(integer_of(args[3]).to_int()) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
}
let total = @moonmoney.Money::sum(shares, currency) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
for index in 0.. {
if args.length() != 4 {
abort("moonmoney: tax takes ")
}
let net = @moonmoney.Money::of_minor(
integer_of(args[1]),
currency_of(args[2]),
)
let rate = percent_of(args[3])
let tax = net.tax_on(rate) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
let gross = net.with_tax(rate) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
println(" net: \{render(net, @format.MoneyStyle::standard())}")
println(" rate: \{rate.text()}")
println(" tax: \{render(tax, @format.MoneyStyle::standard())}")
println(" gross: \{render(gross, @format.MoneyStyle::standard())}")
}
"convert" => {
if args.length() != 5 {
abort("moonmoney: convert takes ")
}
let money = @moonmoney.Money::of_minor(
integer_of(args[1]),
currency_of(args[2]),
)
let converted = money.convert(currency_of(args[3]), rate_of(args[4])) catch {
error => abort("moonmoney: " + @moonmoney.MoneyError::message(error))
}
println(
" \{render(money, @format.MoneyStyle::code_first())} at \{args[4]} is \{render(converted, @format.MoneyStyle::code_first())}",
)
}
"currencies" => {
let prefix = if args.length() >= 2 { args[1].to_upper() } else { "" }
let mut shown = 0
for currency in @moonmoney.Currency::all() {
if prefix == "" || currency.code.has_prefix(prefix) {
println(
" \{currency.code} \{currency.exponent} dp \{currency.display_symbol()} \{currency.name}",
)
shown += 1
}
}
println(" \{shown} of \{@moonmoney.Currency::count()}")
}
"version" => println("moonmoney 0.1.0")
"help" => println(USAGE)
_ => {
println(USAGE)
abort("moonmoney: unknown command \{args[0]}")
}
}
}