lucre 0.2.0

An ergonomic library for handling money.
Documentation
use std::{env, fmt::Write, fs, path::Path};

/// Generates the ISO 4217 catalog from `isodata.tsv`: one `Currency`
/// constant per row, a slice of the whole table, and lookups by alphabetic
/// and numeric code. The output is included into `lib.rs`.
fn main() {
    println!("cargo::rerun-if-changed=isodata.tsv");

    let tsv = fs::read_to_string("isodata.tsv").expect("isodata.tsv is readable");

    let mut consts = String::new();
    let mut alpha_arms = String::new();
    let mut numeric_arms = String::new();
    let mut codes = Vec::new();

    for line in tsv.lines().skip(1).filter(|l| !l.trim().is_empty()) {
        let fields: Vec<&str> = line.split('\t').collect();
        let (alpha, numeric, name, symbol, exponent) =
            (fields[0], fields[1], fields[2], fields[4], fields[6]);
        let flags = fields.get(7).copied().unwrap_or("");

        assert!(
            alpha.len() == 3 && alpha.bytes().all(|b| b.is_ascii_uppercase()),
            "malformed alphabetic code: {alpha:?}"
        );
        assert!(!symbol.is_empty(), "missing symbol for {alpha}");
        let numeric: u32 = numeric.parse().expect("numeric code");
        let minor_digits: u32 = if exponent.is_empty() {
            0
        } else {
            exponent.parse().expect("exponent")
        };

        let doc = match flags {
            "" => format!("{name}."),
            "fund" => format!("{name}; fund code."),
            "special" => format!("{name}; special code."),
            s if s.starts_with("superseded(") && s.ends_with(')') => {
                format!("{name}; superseded by {}.", &s[11..s.len() - 1])
            }
            other => panic!("unrecognized flags column: {other:?}"),
        };

        writeln!(consts, "    /// {doc}").unwrap();
        writeln!(consts, "    pub const {alpha}: Currency = Currency {{").unwrap();
        writeln!(
            consts,
            "        alphabetic_code: IsoAlphabeticCode(*b\"{alpha}\"),"
        )
        .unwrap();
        writeln!(consts, "        numeric_code: IsoNumericCode({numeric}),").unwrap();
        writeln!(consts, "        minor_digits: {minor_digits},").unwrap();
        writeln!(consts, "        symbol: {symbol:?},").unwrap();
        writeln!(consts, "    }};").unwrap();

        writeln!(
            alpha_arms,
            "            \"{alpha}\" => Some(Currency::{alpha}),"
        )
        .unwrap();
        writeln!(
            numeric_arms,
            "            {numeric} => Some(Currency::{alpha}),"
        )
        .unwrap();
        codes.push(alpha.to_string());
    }

    let count = codes.len();
    let table = codes
        .iter()
        .map(|c| format!("    Currency::{c},"))
        .collect::<Vec<_>>()
        .join("\n");

    let code = format!(
        "impl Currency {{
{consts}}}

const ISO_CURRENCIES: [Currency; {count}] = [
{table}
];

impl Currency {{
    /// Every ISO 4217 currency, including fund, superseded, and special codes.
    pub fn all() -> &'static [Currency] {{
        &ISO_CURRENCIES
    }}

    /// Look up a currency by its three-letter code, e.g. `\"USD\"`. Case-sensitive.
    pub fn from_alphabetic_code(code: &str) -> Option<Currency> {{
        match code {{
{alpha_arms}            _ => None,
        }}
    }}

    /// Look up a currency by its ISO numeric code, e.g. `840`.
    pub fn from_numeric_code(code: u32) -> Option<Currency> {{
        match code {{
{numeric_arms}            _ => None,
        }}
    }}
}}
"
    );

    let out_dir = env::var("OUT_DIR").unwrap();
    fs::write(Path::new(&out_dir).join("iso_currencies.rs"), code).unwrap();
}