pub mod vocab;
use crate::error::{Error, Result};
use crate::options::{Gender, OptionKind, OptionSpec, OptionValue};
use std::fmt;
use std::str::FromStr;
macro_rules! currencies {
($( $(#[$meta:meta])* $code:ident = $exponent:literal ),+ $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
#[allow(clippy::upper_case_acronyms)]
pub enum Currency {
$( $(#[$meta])* $code, )+
}
impl Currency {
pub const ALL: &'static [Currency] = &[$(Currency::$code),+];
pub const CODES: &'static [&'static str] = &[$(stringify!($code)),+];
#[must_use]
pub const fn code(self) -> &'static str {
match self { $(Currency::$code => stringify!($code),)+ }
}
#[must_use]
pub const fn exponent(self) -> u8 {
match self { $(Currency::$code => $exponent,)+ }
}
}
impl FromStr for Currency {
type Err = crate::options::ParseValueError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
$( if s.eq_ignore_ascii_case(stringify!($code)) { return Ok(Currency::$code); } )+
Err(crate::options::ParseValueError)
}
}
};
}
currencies! {
AED = 2,
AUD = 2,
AZN = 2,
BDT = 2,
BHD = 3,
BRL = 2,
CAD = 2,
CNY = 2,
CZK = 2,
DKK = 2,
DZD = 2,
EGP = 2,
ETB = 2,
EUR = 2,
GBP = 2,
GEL = 2,
GHS = 2,
HUF = 2,
IDR = 0,
ILS = 2,
INR = 2,
IQD = 3,
IRR = 0,
JOD = 3,
JPY = 0,
KES = 2,
KRW = 0,
KWD = 3,
LBP = 2,
LYD = 3,
MAD = 2,
MXN = 2,
MYR = 2,
NGN = 2,
NOK = 2,
NZD = 2,
OMR = 3,
PHP = 2,
PKR = 2,
PLN = 2,
QAR = 2,
RON = 2,
RSD = 2,
RUB = 2,
SAR = 2,
SDG = 2,
SEK = 2,
SGD = 2,
SYP = 2,
THB = 2,
TND = 3,
TRY = 2,
TWD = 2,
UAH = 2,
USD = 2,
VND = 0,
YER = 2,
ZAR = 2,
}
impl Currency {
#[must_use]
pub const fn minor_unit_digits(self) -> u32 {
if self.exponent() == 3 { 3 } else { 2 }
}
#[must_use]
pub const fn has_minor_unit(self) -> bool {
self.exponent() != 0
}
}
impl fmt::Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.code())
}
}
impl OptionValue for Currency {
const KIND: OptionKind = OptionKind::Enum(Currency::CODES);
fn parse(spec: &OptionSpec, raw: &str) -> Result<Self> {
let table = match spec.kind {
OptionKind::Currency(table) => Some(table),
_ => None,
};
let Ok(currency) = raw.trim().parse::<Currency>() else {
return Err(Error::InvalidOptionValue {
key: spec.name,
value: raw.to_string(),
allowed: table.map_or_else(
|| Currency::CODES.to_vec(),
|t| t.codes().map(Currency::code).collect(),
),
});
};
if let Some(table) = table {
table.words(currency)?;
}
Ok(currency)
}
fn render(&self) -> String {
self.code().to_string()
}
}
pub fn assert_minor_unit(minor: u32, currency: Currency) -> Result<()> {
if minor != 0 && !currency.has_minor_unit() {
return Err(Error::NoMinorUnit { currency });
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CurrencyWords {
pub major: &'static [&'static str],
pub minor: Option<&'static [&'static str]>,
pub major_gender: Option<Gender>,
pub minor_gender: Option<Gender>,
}
impl CurrencyWords {
#[must_use]
pub const fn new(
major: &'static [&'static str],
minor: Option<&'static [&'static str]>,
) -> Self {
CurrencyWords {
major,
minor,
major_gender: None,
minor_gender: None,
}
}
#[must_use]
pub const fn gendered(self, major: Gender, minor: Gender) -> Self {
CurrencyWords {
major_gender: Some(major),
minor_gender: Some(minor),
..self
}
}
#[must_use]
pub fn major_at(&self, index: usize) -> &'static str {
clamp(self.major, index)
}
#[must_use]
pub fn minor_at(&self, index: usize) -> &'static str {
clamp(self.minor.expect("currency has a minor unit"), index)
}
}
fn clamp(forms: &'static [&'static str], index: usize) -> &'static str {
forms.get(index).or(forms.last()).copied().unwrap_or("")
}
#[derive(Debug)]
pub struct CurrencyTable {
pub language: &'static str,
pub entries: &'static [(Currency, CurrencyWords)],
}
impl CurrencyTable {
#[must_use]
pub const fn new(
language: &'static str,
entries: &'static [(Currency, CurrencyWords)],
) -> Self {
CurrencyTable { language, entries }
}
#[must_use]
pub fn get(&self, currency: Currency) -> Option<&'static CurrencyWords> {
self.entries
.iter()
.find(|(c, _)| *c == currency)
.map(|(_, words)| words)
}
pub fn words(&self, currency: Currency) -> Result<&'static CurrencyWords> {
self.get(currency)
.ok_or_else(|| Error::UnsupportedCurrency {
currency,
allowed: self.codes().collect(),
})
}
#[must_use]
pub fn contains(&self, currency: Currency) -> bool {
self.get(currency).is_some()
}
pub fn codes(&self) -> impl Iterator<Item = Currency> + '_ {
self.entries.iter().map(|(c, _)| *c)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codes_round_trip() {
for &c in Currency::ALL {
assert_eq!(c.code().parse::<Currency>(), Ok(c));
assert_eq!(c.code().to_lowercase().parse::<Currency>(), Ok(c));
assert_eq!(c.code().len(), 3);
}
assert_eq!(Currency::ALL.len(), 58);
assert!("XXX".parse::<Currency>().is_err());
assert_eq!(Currency::TND.minor_unit_digits(), 3);
assert_eq!(Currency::JPY.minor_unit_digits(), 2);
assert_eq!(Currency::USD.minor_unit_digits(), 2);
assert!(assert_minor_unit(5, Currency::JPY).is_err());
assert!(assert_minor_unit(0, Currency::JPY).is_ok());
assert!(assert_minor_unit(5, Currency::USD).is_ok());
}
#[test]
fn tables() {
let words = vocab::EN.words(Currency::GBP).unwrap();
assert_eq!(words.major_at(0), "pound");
assert_eq!(words.major_at(1), "pounds");
assert_eq!(words.minor_at(1), "pence");
let taka = vocab::EN.words(Currency::BDT).unwrap();
assert_eq!(taka.major_at(1), "taka");
assert!(matches!(
vocab::EN.words(Currency::JPY),
Err(Error::UnsupportedCurrency {
currency: Currency::JPY,
..
})
));
assert_eq!(vocab::JA.words(Currency::JPY).unwrap().minor, None);
assert_eq!(
vocab::RU.words(Currency::RUB).unwrap().minor_gender,
Some(Gender::Feminine)
);
}
}