///|
/// Currency codes whose minor unit has no fractional part.
///
/// ISO 4217 gives these currencies an exponent of `0`, so a statement that
/// writes `TRNAMT>1500` means 1500 units, not 15.00. Treating them as
/// two-decimal currencies is the classic way an importer is off by a factor of
/// a hundred.
fn has_no_minor_unit(code : String) -> Bool {
  match code {
    "BIF"
    | "CLP"
    | "DJF"
    | "GNF"
    | "ISK"
    | "JPY"
    | "KMF"
    | "KRW"
    | "PYG"
    | "RWF"
    | "UGX"
    | "UYI"
    | "VND"
    | "VUV"
    | "XAF"
    | "XOF"
    | "XPF"
    | "XAG"
    | "XAU"
    | "XBA"
    | "XBB"
    | "XBC"
    | "XBD"
    | "XDR"
    | "XPD"
    | "XPT"
    | "XSU"
    | "XTS"
    | "XUA"
    | "XXX" => true
    _ => false
  }
}

///|
/// Currency codes whose minor unit has three fractional digits.
fn has_three_minor_digits(code : String) -> Bool {
  match code {
    "BHD" | "IQD" | "JOD" | "KWD" | "LYD" | "OMR" | "TND" => true
    _ => false
  }
}

///|
/// Currency codes whose minor unit has four fractional digits.
fn has_four_minor_digits(code : String) -> Bool {
  match code {
    "CLF" | "UYW" => true
    _ => false
  }
}

///|
/// Currency codes whose minor unit has the usual two fractional digits.
///
/// The list is the ISO 4217 set that a retail bank statement can carry; a code
/// outside it is reported as unknown rather than assumed to be two digits,
/// because guessing a scale is how a hundred becomes one.
fn has_two_minor_digits(code : String) -> Bool {
  match code {
    "AED"
    | "AFN"
    | "ALL"
    | "AMD"
    | "ANG"
    | "AOA"
    | "ARS"
    | "AUD"
    | "AWG"
    | "AZN"
    | "BAM"
    | "BBD"
    | "BDT"
    | "BGN"
    | "BMD"
    | "BND"
    | "BOB"
    | "BRL"
    | "BSD"
    | "BTN"
    | "BWP"
    | "BYN"
    | "BZD"
    | "CAD"
    | "CDF"
    | "CHE"
    | "CHF"
    | "CHW"
    | "CNY"
    | "COP"
    | "COU"
    | "CRC"
    | "CUP"
    | "CVE"
    | "CZK"
    | "DKK"
    | "DOP"
    | "DZD"
    | "EGP"
    | "ERN"
    | "ETB"
    | "EUR"
    | "FJD"
    | "FKP"
    | "GBP"
    | "GEL"
    | "GHS"
    | "GIP"
    | "GMD"
    | "GTQ"
    | "GYD"
    | "HKD"
    | "HNL"
    | "HTG"
    | "HUF"
    | "IDR"
    | "ILS"
    | "INR"
    | "IRR"
    | "JMD"
    | "KES"
    | "KGS"
    | "KHR"
    | "KPW"
    | "KYD"
    | "KZT"
    | "LAK"
    | "LBP"
    | "LKR"
    | "LRD"
    | "LSL"
    | "MAD"
    | "MDL"
    | "MGA"
    | "MKD"
    | "MMK"
    | "MNT"
    | "MOP"
    | "MRU"
    | "MUR"
    | "MVR"
    | "MWK"
    | "MXN"
    | "MYR"
    | "MZN"
    | "NAD"
    | "NGN"
    | "NIO"
    | "NOK"
    | "NPR"
    | "NZD"
    | "PAB"
    | "PEN"
    | "PGK"
    | "PHP"
    | "PKR"
    | "PLN"
    | "QAR"
    | "RON"
    | "RSD"
    | "RUB"
    | "SAR"
    | "SBD"
    | "SCR"
    | "SDG"
    | "SEK"
    | "SGD"
    | "SHP"
    | "SLE"
    | "SOS"
    | "SRD"
    | "SSP"
    | "STN"
    | "SVC"
    | "SYP"
    | "SZL"
    | "THB"
    | "TJS"
    | "TMT"
    | "TOP"
    | "TRY"
    | "TTD"
    | "TWD"
    | "TZS"
    | "UAH"
    | "USD"
    | "UYU"
    | "UZS"
    | "VES"
    | "WST"
    | "XCD"
    | "YER"
    | "ZAR"
    | "ZMW"
    | "ZWG"
    | "ZWL" => true
    _ => false
  }
}

///|
/// How many fractional digits the currency's minor unit uses.
///
/// `Some(2)` for most currencies, `Some(0)` for the yen, won and the other
/// no-fraction currencies, `Some(3)` for the dinar family and `Some(4)` for the
/// Chilean and Uruguayan unit-of-account codes. `None` means the code is not one
/// this library knows, and the caller must choose a scale itself rather than
/// have one guessed.
///
/// The argument is matched without regard to case, because a bank may write
/// `CURDEF>usd`.
pub fn minor_unit_scale(currency : String) -> Int? {
  let code = currency.to_upper()
  if has_no_minor_unit(code) {
    Some(0)
  } else if has_three_minor_digits(code) {
    Some(3)
  } else if has_four_minor_digits(code) {
    Some(4)
  } else if has_two_minor_digits(code) {
    Some(2)
  } else {
    None
  }
}

///|
/// Whether the currency code is one this library knows.
pub fn is_known_currency(currency : String) -> Bool {
  minor_unit_scale(currency) is Some(_)
}

///|
/// The scale an amount falls back to when the document names no currency.
///
/// Two digits: it is what most currencies use, and the importer says so instead
/// of pretending the choice was read from the document.
pub fn default_minor_unit_scale() -> Int {
  2
}

///|
/// Convert an amount using the scale the currency implies.
///
/// This is the conversion a ledger import usually wants: the document states
/// both the amount and the currency, so the scale does not have to be passed in
/// by hand. `None` means the amount is malformed, needs more fractional digits
/// than the currency has, overflows `Int64`, or names an unknown currency.
pub fn amount_in_currency(amount : String, currency : String) -> Int64? {
  match minor_unit_scale(currency) {
    Some(scale) => amount_to_minor_units(amount, scale)
    None => None
  }
}

///|
/// Render an integer count of minor units using the currency's scale.
pub fn amount_from_currency(minor : Int64, currency : String) -> String? {
  match minor_unit_scale(currency) {
    Some(scale) => minor_units_to_amount(minor, scale)
    None => None
  }
}

///|
/// The currency code as it should be written in a report: upper-cased, or the
/// fallback when the document declared nothing.
pub fn currency_label(currency : String) -> String {
  if currency == "" {
    "UNKNOWN"
  } else {
    currency.to_upper()
  }
}

///|
/// The minor-unit name a report can print next to an amount, or the empty
/// string for an unknown currency.
pub fn minor_unit_name(currency : String) -> String {
  match minor_unit_scale(currency) {
    Some(0) => "unit"
    Some(3) => "mill"
    Some(4) => "unit"
    Some(_) => "cent"
    None => ""
  }
}