use crate::base::{clean, splitnum, Cards, Lang, N2WError, Node, Result};
use crate::currency::{default_to_currency, CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const GENDER_STEM: &str = "è";
const NEGWORD: &str = "menys ";
const SEPARATOR_DEFAULT: &str = " amb";
const SEPARATOR_UNSET: &str = ",";
const RECURSION_LIMIT: u32 = 900;
fn log1000_trunc(value: &BigInt) -> u32 {
const LN_1000: f64 = 6.907755278982137_f64;
let x = value
.to_u64()
.expect("log1000_trunc call sites are guarded by value < 1e18") as f64;
(x.ln() / LN_1000).trunc() as u32
}
fn pow10(e: u32) -> BigInt {
BigInt::from(10u8).pow(e)
}
fn float_repr_precision(f: f64) -> u32 {
let s = format!("{:e}", f);
let (mantissa, exp) = match s.split_once('e') {
Some(parts) => parts,
None => return 0,
};
let exp: i32 = match exp.parse() {
Ok(e) => e,
Err(_) => return 0,
};
let ndigits = mantissa.chars().filter(|c| c.is_ascii_digit()).count() as i32;
let decpt = exp + 1;
if decpt > -4 && decpt <= 16 && decpt >= ndigits {
1
} else {
(decpt - ndigits).unsigned_abs()
}
}
fn decimal_to_f64(value: &BigDecimal) -> f64 {
f64::from_str(&value.to_string()).unwrap_or(f64::NAN)
}
pub struct LangCa {
cards: Cards,
maxval: BigInt,
ords_tbl: HashMap<u128, &'static str>,
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_adjectives: HashMap<&'static str, &'static str>,
}
impl Default for LangCa {
fn default() -> Self {
Self::new()
}
}
impl LangCa {
pub fn new() -> Self {
let mut cards = Cards::new();
let high = ["quadr", "tr", "b", "m"];
let cap: i64 = 3 + 6 * high.len() as i64;
let mut n = cap;
for word in high.iter() {
if n <= 3 {
break;
}
cards.insert(pow10((n - 3) as u32), format!("{}ilió", word));
n -= 6;
}
for (k, v) in [
(1000i64, "mil"),
(100, "cent"),
(90, "noranta"),
(80, "vuitanta"),
(70, "setanta"),
(60, "seixanta"),
(50, "cinquanta"),
(40, "quaranta"),
(30, "trenta"),
] {
cards.insert(BigInt::from(k), v);
}
let low = [
"vint-i-nou",
"vint-i-vuit",
"vint-i-set",
"vint-i-sis",
"vint-i-cinc",
"vint-i-quatre",
"vint-i-tres",
"vint-i-dos",
"vint-i-un",
"vint",
"dinou",
"divuit",
"disset",
"setze",
"quinze",
"catorze",
"tretze",
"dotze",
"onze",
"deu",
"nou",
"vuit",
"set",
"sis",
"cinc",
"quatre",
"tres",
"dos",
"un",
"zero",
];
let len = low.len();
for (i, word) in low.iter().enumerate() {
cards.insert(BigInt::from(len - 1 - i), *word);
}
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
let ords_tbl: HashMap<u128, &'static str> = [
(1u128, "primer"),
(2, "segon"),
(3, "tercer"),
(4, "quart"),
(5, "cinqu"),
(6, "sis"),
(7, "set"),
(8, "vuit"),
(9, "nov"),
(10, "des"),
(11, "onz"),
(12, "dotz"),
(13, "tretz"),
(14, "catorz"),
(15, "quinz"),
(16, "setz"),
(17, "disset"),
(18, "divuit"),
(19, "dinov"),
(20, "vint"),
(30, "trent"),
(40, "quarant"),
(50, "cinquant"),
(60, "seixant"),
(70, "setant"),
(80, "vuitant"),
(90, "norant"),
(100, "cent"),
(200, "dos-cent"),
(300, "tres-cent"),
(400, "quatre-cent"),
(500, "cinc-cent"),
(600, "sis-cent"),
(700, "set-cent"),
(800, "vuit-cent"),
(900, "nou-cent"),
(1_000, "mil"),
(1_000_000, "milion"),
(1_000_000_000, "mil milion"),
(1_000_000_000_000, "bilion"),
(1_000_000_000_000_000, "mil bilion"),
]
.into_iter()
.collect();
let currency_forms: HashMap<&'static str, CurrencyForms> = [
("EUR", &["euro", "euros"][..], &["cèntim", "cèntims"][..]),
(
"ESP",
&["pesseta", "pessetes"][..],
&["cèntim", "cèntims"][..],
),
("USD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("PEN", &["sol", "sols"][..], &["cèntim", "cèntims"][..]),
("CRC", &["colón", "colons"][..], &["centau", "centaus"][..]),
("AUD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("CAD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("GBP", &["lliura", "lliures"][..], &["penic", "penics"][..]),
("RUB", &["ruble", "rubles"][..], &["copec", "copecs"][..]),
("SEK", &["corona", "corones"][..], &["öre", "öre"][..]),
("NOK", &["corona", "corones"][..], &["øre", "øre"][..]),
("PLN", &["zloty", "zlotys"][..], &["grosz", "groszy"][..]),
("MXN", &["peso", "pesos"][..], &["centau", "centaus"][..]),
("RON", &["leu", "lei"][..], &["ban", "bani"][..]),
("INR", &["rupia", "rupies"][..], &["paisa", "paise"][..]),
(
"HUF",
&["fòrint", "fòrints"][..],
&["fillér", "fillérs"][..],
),
("FRF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
("CNY", &["iuan", "iuans"][..], &["fen", "jiao"][..]),
("CZK", &["corona", "corones"][..], &["haléř", "haléřů"][..]),
(
"NIO",
&["córdoba", "córdobas"][..],
&["centau", "centaus"][..],
),
(
"VES",
&["bolívar", "bolívars"][..],
&["cèntim", "cèntims"][..],
),
("BRL", &["real", "reals"][..], &["centau", "centaus"][..]),
("CHF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
("JPY", &["ien", "iens"][..], &["sen", "sen"][..]),
("KRW", &["won", "wons"][..], &["jeon", "jeon"][..]),
("KPW", &["won", "wons"][..], &["chŏn", "chŏn"][..]),
("TRY", &["lira", "lires"][..], &["kuruş", "kuruş"][..]),
("ZAR", &["rand", "rands"][..], &["cèntim", "cèntims"][..]),
("KZT", &["tenge", "tenge"][..], &["tin", "tin"][..]),
(
"UAH",
&["hrívnia", "hrívnies"][..],
&["kopiika", "kopíok"][..],
),
("THB", &["baht", "bahts"][..], &["satang", "satang"][..]),
("AED", &["dirham", "dirhams"][..], &["fils", "fulūs"][..]),
("AFN", &["afgani", "afganis"][..], &["puli", "puls"][..]),
("ALL", &["lek", "lekë"][..], &["qqindarka", "qindarkë"][..]),
("AMD", &["dram", "drams"][..], &["luma", "lumas"][..]),
("ANG", &["florí", "florins"][..], &["cèntim", "cèntims"][..]),
(
"AOA",
&["kwanza", "kwanzes"][..],
&["cèntim", "cèntims"][..],
),
("ARS", &["peso", "pesos"][..], &["centau", "centaus"][..]),
("AWG", &["florí", "florins"][..], &["centau", "centaus"][..]),
("AZN", &["manat", "manats"][..], &["qəpik", "qəpik"][..]),
("BBD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("BDT", &["taka", "taka"][..], &["poisha", "poisha"][..]),
("BGN", &["lev", "leva"][..], &["stotinka", "stotinki"][..]),
("BHD", &["dinar", "dinars"][..], &["fils", "fulūs"][..]),
("BIF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
("BMD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("BND", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
(
"BOB",
&["boliviano", "bolivianos"][..],
&["centau", "centaus"][..],
),
("BSD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
(
"BTN",
&["ngultrum", "ngultrums"][..],
&["chetrum", "chetrums"][..],
),
("BWP", &["pula", "pula"][..], &["thebe", "thebe"][..]),
("BYN", &["ruble", "rubles"][..], &["copec", "copecs"][..]),
("BYR", &["ruble", "rubles"][..], &["copec", "copecs"][..]),
("BZD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
("CDF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
("CLP", &["peso", "pesos"][..], &["centau", "centaus"][..]),
("COP", &["peso", "pesos"][..], &["centau", "centaus"][..]),
("CUP", &["peso", "pesos"][..], &["centau", "centaus"][..]),
("CVE", &["escut", "escuts"][..], &["centau", "centaus"][..]),
(
"CYP",
&["lliura", "lliures"][..],
&["cèntim", "cèntims"][..],
),
("DJF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
("DKK", &["corona", "corones"][..], &["øre", "øre"][..]),
("DOP", &["peso", "pesos"][..], &["centau", "centaus"][..]),
("DZD", &["dinar", "dinars"][..], &["cèntim", "cèntims"][..]),
("ECS", &["sucre", "sucres"][..], &["centau", "centaus"][..]),
(
"EGP",
&["lliura", "lliures"][..],
&["piastre", "piastres"][..],
),
("ERN", &["nakfa", "nakfes"][..], &["cèntim", "cèntims"][..]),
("ETB", &["birr", "birr"][..], &["cèntim", "cèntims"][..]),
("FJD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("FKP", &["lliura", "lliures"][..], &["penic", "penics"][..]),
("GEL", &["lari", "laris"][..], &["tetri", "tetri"][..]),
("GHS", &["cedi", "cedis"][..], &["pesewa", "pesewas"][..]),
("GIP", &["lliura", "lliures"][..], &["penic", "penics"][..]),
("GMD", &["dalasi", "dalasis"][..], &["butut", "bututs"][..]),
("GNF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
(
"GTQ",
&["quetzal", "quetzals"][..],
&["centau", "centaus"][..],
),
("GYD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("HKD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
(
"HNL",
&["lempira", "lempires"][..],
&["centau", "centaus"][..],
),
("HRK", &["kuna", "kuna"][..], &["lipa", "lipa"][..]),
(
"HTG",
&["gourde", "gourdes"][..],
&["cèntim", "cèntims"][..],
),
("IDR", &["rúpia", "rúpies"][..], &["cèntim", "cèntims"][..]),
("ILS", &["xéquel", "xéquels"][..], &["agorà", "agorot"][..]),
("IQD", &["dinar", "dinars"][..], &["fils", "fils"][..]),
("IRR", &["rial", "rials"][..], &["dinar", "dinars"][..]),
("ISK", &["corona", "corones"][..], &["eyrir", "aurar"][..]),
("ITL", &["lira", "lires"][..], &["cèntim", "cèntims"][..]),
("JMD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
(
"JOD",
&["dinar", "dinars"][..],
&["piastra", "piastres"][..],
),
(
"KES",
&["xiling", "xílings"][..],
&["cèntim", "cèntims"][..],
),
("KGS", &["som", "som"][..], &["tyiyn", "tyiyn"][..]),
("KHR", &["riel", "riels"][..], &["cèntim", "cèntims"][..]),
("KMF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
("KWD", &["dinar", "dinars"][..], &["fils", "fils"][..]),
("KYD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
("LAK", &["kip", "kips"][..], &["at", "at"][..]),
(
"LBP",
&["lliura", "lliures"][..],
&["piastra", "piastres"][..],
),
("LKR", &["rúpia", "rúpies"][..], &["cèntim", "cèntims"][..]),
("LRD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
("LSL", &["loti", "maloti"][..], &["sente", "lisente"][..]),
("LTL", &["lita", "litai"][..], &["cèntim", "cèntims"][..]),
("LYD", &["dinar", "dinars"][..], &["dírham", "dírhams"][..]),
(
"MAD",
&["dírham", "dirhams"][..],
&["cèntim", "cèntims"][..],
),
("MDL", &["leu", "lei"][..], &["ban", "bani"][..]),
(
"MGA",
&["ariary", "ariary"][..],
&["iraimbilanja", "iraimbilanja"][..],
),
("MKD", &["denar", "denari"][..], &["deni", "deni"][..]),
("MMK", &["kyat", "kyats"][..], &["pya", "pyas"][..]),
("MNT", &["tögrög", "tögrög"][..], &["möngö", "möngö"][..]),
("MOP", &["pataca", "pataques"][..], &["avo", "avos"][..]),
(
"MRO",
&["ouguiya", "ouguiya"][..],
&["khoums", "khoums"][..],
),
(
"MRU",
&["ouguiya", "ouguiya"][..],
&["khoums", "khoums"][..],
),
("MUR", &["rupia", "rúpies"][..], &["cèntim", "cèntims"][..]),
("MVR", &["rufiyaa", "rufiyaa"][..], &["laari", "laari"][..]),
(
"MWK",
&["kwacha", "kwacha"][..],
&["tambala", "tambala"][..],
),
("MYR", &["ringgit", "ringgits"][..], &["sen", "sens"][..]),
(
"MZN",
&["metical", "meticals"][..],
&["centau", "centaus"][..],
),
("NAD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
("NGN", &["naira", "naires"][..], &["kobo", "kobos"][..]),
("NPR", &["rupia", "rupies"][..], &["paisa", "paises"][..]),
("NZD", &["dòlar", "dòlars"][..], &["centau", "centaus"][..]),
("OMR", &["rial", "rials"][..], &["baisa", "baisa"][..]),
(
"PAB",
&["dòlar", "dòlars"][..],
&["centésimo", "centésimos"][..],
),
("PGK", &["kina", "kina"][..], &["toea", "toea"][..]),
("PHP", &["peso", "pesos"][..], &["centau", "centaus"][..]),
("PKR", &["rupia", "rupies"][..], &["paisa", "paise"][..]),
("PLZ", &["zloty", "zlotys"][..], &["grosz", "groszy"][..]),
(
"PYG",
&["guaraní", "guaranís"][..],
&["cèntim", "cèntims"][..],
),
("QAR", &["rial", "rials"][..], &["dírham", "dírhams"][..]),
(
"QTQ",
&["quetzal", "quetzals"][..],
&["centau", "centaus"][..],
),
("RSD", &["dinar", "dinars"][..], &["para", "para"][..]),
("RUR", &["ruble", "rubles"][..], &["copec", "copecs"][..]),
("RWF", &["franc", "francs"][..], &["cèntim", "cèntims"][..]),
(
"SAR",
&["riyal", "riyals"][..],
&["hàl·lala", "hàl·lalat"][..],
),
("SBD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
("SCR", &["rupia", "rupies"][..], &["cèntim", "cèntims"][..]),
(
"SDG",
&["lliura", "lliures"][..],
&["piastre", "piastres"][..],
),
("SGD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
("SHP", &["lliura", "lliures"][..], &["penic", "penics"][..]),
("SLL", &["leonE", "leones"][..], &["cèntim", "cèntims"][..]),
("SRD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
(
"SSP",
&["lliura", "lliures"][..],
&["piastre", "piastres"][..],
),
("STD", &["dobra", "dobrAs"][..], &["cèntim", "cèntims"][..]),
("SVC", &["colón", "colons"][..], &["centau", "centaus"][..]),
(
"SYP",
&["lliura", "lliures"][..],
&["piastre", "piastres"][..],
),
(
"SZL",
&["lilangeni", "emalangeni"][..],
&["cèntim", "cèntims"][..],
),
("TJS", &["somoni", "somoni"][..], &["diram", "diram"][..]),
("TMT", &["manat", "manats"][..], &["teňňesi", "teňňesi"][..]),
(
"TND",
&["dinar", "dinars"][..],
&["mil·lim", "mil·limat"][..],
),
("TOP", &["paanga", "paangas"][..], &["seniti", "seniti"][..]),
("TTD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
(
"TWD",
&["nou dòlar", "nous dòlars"][..],
&["fen", "fen"][..],
),
(
"TZS",
&["xíling", "xílings"][..],
&["cèntim", "cèntims"][..],
),
(
"UGX",
&["xíling", "xílings"][..],
&["cèntim", "cèntims"][..],
),
(
"UYU",
&["peso", "pesos"][..],
&["centèsim", "centèsims"][..],
),
("UZS", &["som", "som"][..], &["tiyin", "tiyin"][..]),
("VND", &["dong", "dongs"][..], &["xu", "xu"][..]),
("VUV", &["vatu", "vatus"][..], &["cèntim", "cèntims"][..]),
("WST", &["tala", "tala"][..], &["sene", "sene"][..]),
(
"XAF",
&["franc CFA", "francs CFA"][..],
&["cèntim", "cèntims"][..],
),
("XCD", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
(
"XOF",
&["franc CFA", "francs CFA"][..],
&["cèntim", "cèntims"][..],
),
(
"XPF",
&["franc CFP", "francs CFP"][..],
&["cèntim", "cèntims"][..],
),
("YER", &["rial", "rials"][..], &["fils", "fils"][..]),
("YUM", &["dinar", "dinars"][..], &["para", "para"][..]),
("ZMW", &["kwacha", "kwacha"][..], &["ngwee", "ngwee"][..]),
("ZWL", &["dòlar", "dòlars"][..], &["cèntim", "cèntims"][..]),
]
.into_iter()
.map(|(k, u, s)| (k, CurrencyForms::new(u, s)))
.collect();
let currency_adjectives: HashMap<&'static str, &'static str> = [
("AUD", "Australian"),
("BYN", "Belarusian"),
("CAD", "Canadian"),
("EEK", "Estonian"),
("USD", "US"),
("RUB", "Russian"),
("NOK", "Norwegian"),
("MXN", "Mexican"),
("RON", "Romanian"),
("INR", "Indian"),
("HUF", "Hungarian"),
("ISK", "íslenskar"),
("UZS", "Uzbekistan"),
("SAR", "Saudi"),
("JPY", "Japanese"),
("KRW", "Korean"),
]
.into_iter()
.collect();
LangCa {
cards,
maxval,
ords_tbl,
exclude_title: vec!["i".into(), "menys".into(), "coma".into()],
currency_forms,
currency_adjectives,
}
}
fn ords(&self, key: &BigInt) -> Result<&'static str> {
key.to_u128()
.and_then(|k| self.ords_tbl.get(&k).copied())
.ok_or_else(|| N2WError::Key(format!("{}", key)))
}
fn ords_3(&self, key: &BigInt) -> Result<&'static str> {
let word = match key.to_u32() {
Some(1) => "unè",
Some(2) => "dosè",
Some(3) => "tresè",
Some(4) => "quatrè",
Some(5) => "cinquè",
Some(6) => "sisè",
Some(7) => "setè",
Some(8) => "vuitè",
Some(9) => "novè",
_ => return Err(N2WError::Key(format!("{}", key))),
};
Ok(word)
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"El número negatiu {} no pot ser tractat com un ordinal.",
value
)));
}
Ok(())
}
fn verify_ordinal_float(&self, value: &FloatValue) -> Result<BigInt> {
if let FloatValue::Float { value: f, .. } = value {
if f.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".into(),
));
}
if f.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
}
match value.as_whole_int() {
None => Err(N2WError::Type(
"El float no pot ser tractat com un ordinal.".into(),
)),
Some(i) if i.is_negative() => Err(N2WError::Type(format!(
"El número negatiu {} no pot ser tractat com un ordinal.",
i
))),
Some(i) => Ok(i),
}
}
fn to_ordinal_inner(&self, value: &BigInt, depth: u32) -> Result<String> {
self.verify_ordinal(value)?;
if depth > RECURSION_LIMIT {
return Err(N2WError::Value(format!(
"RecursionError: maximum recursion depth exceeded in to_ordinal({})",
value
)));
}
let five = BigInt::from(5);
let ten = BigInt::from(10);
let twenty = BigInt::from(20);
let thirty = BigInt::from(30);
let hundred = BigInt::from(100);
let two_hundred = BigInt::from(200);
let thousand = pow10(3);
let text: String = if value.is_zero() {
String::new()
} else if value < &five {
self.ords(value)?.to_string()
} else if value <= &twenty {
format!("{}{}", self.ords(value)?, GENDER_STEM)
} else if value <= &thirty {
let frac = value % &ten;
if frac.is_zero() {
format!("{}{}", self.ords(value)?, GENDER_STEM)
} else {
format!("{}{}{}", self.ords(&twenty)?, "-i-", self.ords_3(&frac)?)
}
} else if value < &hundred {
let dec = (value / &ten) * &ten;
let frac = value - &dec;
if frac.is_zero() {
format!("{}{}", self.ords(&dec)?, GENDER_STEM)
} else {
format!("{}{}{}{}", self.ords(&dec)?, "a", "-", self.ords_3(&frac)?)
}
} else if value == &hundred {
format!("{}{}", self.ords(value)?, GENDER_STEM)
} else if value < &two_hundred {
let cen = (value / &hundred) * &hundred;
format!(
"{} {}",
self.ords(&cen)?,
self.to_ordinal_inner(&(value - &cen), depth + 1)?
)
} else if value < &thousand {
let cen = (value / &hundred) * &hundred;
format!(
"{}{} {}",
self.ords(&cen)?,
"s",
self.to_ordinal_inner(&(value - &cen), depth + 1)?
)
} else if value == &thousand {
format!("{}{}", self.ords(value)?, GENDER_STEM)
} else if value < &pow10(6) {
let dec = BigInt::from(1000u16).pow(log1000_trunc(value));
let (high_part, low_part) = value.div_mod_floor(&dec);
let cardinal = if high_part.is_one() {
String::new()
} else {
self.to_cardinal(&high_part)?
};
format!(
"{} {} {}",
cardinal,
self.ords(&dec)?,
self.to_ordinal_inner(&low_part, depth + 1)?
)
} else if value < &pow10(18) {
let dec = BigInt::from(1000u16).pow(log1000_trunc(value));
let (high_part, low_part) = value.div_mod_floor(&dec);
let cardinal = if high_part.is_one() {
String::new()
} else {
self.to_cardinal(&high_part)?
};
let o = self.ords(&dec)?;
format!(
"{}{}{} {}",
cardinal,
o,
GENDER_STEM,
self.to_ordinal_inner(&low_part, depth + 1)?
)
} else {
let part1 = self.to_cardinal(value)?;
let chars: Vec<char> = part1.chars().collect();
let keep = chars.len().saturating_sub(1);
format!("{}{}", chars[..keep].iter().collect::<String>(), "onè")
};
Ok(text.trim().to_string())
}
}
impl Lang for LangCa {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" amb"
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"coma"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
let (ltext, cnum) = l;
let (rtext, nnum) = r;
let mut ctext = ltext.to_string();
let mut ntext = rtext.to_string();
let hundred = BigInt::from(100);
let million = BigInt::from(1_000_000);
if cnum.is_one() {
if nnum < &million {
return (ntext, nnum.clone());
}
ctext = "un".to_string();
}
if nnum < cnum {
if cnum < &hundred {
return (format!("{}-{}", ctext, ntext), cnum + nnum);
}
return (format!("{} {}", ctext, ntext), cnum + nnum);
} else if (nnum % &million).is_zero() && cnum > &BigInt::one() {
let chars: Vec<char> = ntext.chars().collect();
let keep = chars.len().saturating_sub(3);
ntext = chars[..keep].iter().collect::<String>() + "lions";
}
if nnum == &hundred {
ntext.push('s');
ctext.push('-');
} else {
ntext = format!(" {}", ntext);
}
(format!("{}{}", ctext, ntext), cnum * nnum)
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let mut out = String::new();
let mut v = value.clone();
if v.is_negative() {
v = v.abs();
out = format!("{} ", self.negword().trim());
}
if &v >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) ha de ser inferior a {}.",
v,
self.maxval()
)));
}
let tree = splitnum(self, &v).ok_or_else(|| {
N2WError::Overflow(format!(
"abs({}) ha de ser inferior a {}.",
v,
self.maxval()
))
})?;
let words = match clean(self, tree) {
Node::Leaf(t, _) => t,
Node::List(_) => return Err(N2WError::Type("clean did not reduce".into())),
};
Ok(self.title(&format!("{}{}", out, words)))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.to_ordinal_inner(value, 0)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let i = self.verify_ordinal_float(value)?;
self.to_ordinal(&i)
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
let i = self.verify_ordinal_float(value)?;
match i.to_u32() {
Some(1) => Ok("1r".to_string()),
Some(2) => Ok("2n".to_string()),
Some(3) => Ok("3r".to_string()),
Some(4) => Ok("4t".to_string()),
_ => Ok(format!("{}{}", repr_str, GENDER_STEM)),
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let f = match value {
FloatValue::Float { value, .. } => *value,
FloatValue::Decimal { value, .. } => decimal_to_f64(value),
};
if f.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".into(),
));
}
if f.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
let precision = match value {
FloatValue::Float { precision, .. } => *precision,
FloatValue::Decimal { .. } => float_repr_precision(f),
};
let (pre, post) = float2tuple(&FloatValue::Float {
value: f,
precision,
});
let post_str = post.to_string();
let leading_zeros = precision as i64 - post_str.chars().count() as i64;
let post_str = if leading_zeros > 0 {
format!("{}{}", "0".repeat(leading_zeros as usize), post_str)
} else {
post_str
};
let mut out = vec![self.to_cardinal(&pre)?];
if value.is_negative() && pre.is_zero() {
out.insert(0, self.negword().trim().to_string());
}
out.push(self.title(self.pointword()));
for _ in 0..leading_zeros.max(0) {
out.push(self.to_cardinal(&BigInt::zero())?);
}
let post_val = BigInt::from_str(&post_str).map_err(|e| N2WError::Value(e.to_string()))?;
if post_val.is_positive() || leading_zeros == 0 {
out.push(self.to_cardinal(&post_val)?);
}
Ok(out.join(" "))
}
fn cardinal_from_decimal(&self, value: &BigDecimal) -> Result<String> {
let f = decimal_to_f64(value);
self.to_cardinal_float(
&FloatValue::Float {
value: f,
precision: float_repr_precision(f),
},
None,
)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
match value.to_u32() {
Some(1) => Ok("1r".to_string()),
Some(2) => Ok("2n".to_string()),
Some(3) => Ok("3r".to_string()),
Some(4) => Ok("4t".to_string()),
_ => Ok(format!("{}{}", value, GENDER_STEM)),
}
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn lang_name(&self) -> &str {
"Num2Word_CA"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn currency_adjective(&self, code: &str) -> Option<&str> {
self.currency_adjectives.get(code).copied()
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let form = if n.is_one() { 0 } else { 1 };
forms
.get(form)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
) -> Result<String> {
let separator = separator.unwrap_or(self.default_separator());
let separator = if separator == SEPARATOR_UNSET {
SEPARATOR_DEFAULT
} else {
separator
};
if let CurrencyValue::Int(v) = val {
let forms = match self.currency_forms.get(currency) {
Some(f) => f,
None => {
return default_to_currency(self, val, currency, cents, separator, adjective)
}
};
let cr1 = &forms.unit;
let minus_str = if v.is_negative() { NEGWORD } else { "" };
let abs_val = v.abs();
let money_str = self.to_cardinal(&abs_val)?;
let currency_str = if abs_val.is_one() {
&cr1[0]
} else if cr1.len() > 1 {
&cr1[1]
} else {
&cr1[0]
};
return Ok(format!("{} {} {}", minus_str, money_str, currency_str)
.trim()
.to_string());
}
default_to_currency(self, val, currency, cents, separator, adjective)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn f(lang: &LangCa, value: f64, precision: u32) -> String {
lang.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(lang: &LangCa, lit: &str, precision: u32) -> String {
lang.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(lit).unwrap(),
precision,
},
None,
)
.unwrap()
}
#[test]
fn corpus_float_rows() {
let l = LangCa::new();
assert_eq!(f(&l, 0.5, 1), "zero coma cinc");
assert_eq!(f(&l, 1.5, 1), "un coma cinc");
assert_eq!(f(&l, 2.25, 2), "dos coma vint-i-cinc");
assert_eq!(f(&l, 3.14, 2), "tres coma catorze");
assert_eq!(f(&l, 0.01, 2), "zero coma zero un");
assert_eq!(f(&l, 0.1, 1), "zero coma un");
assert_eq!(f(&l, 0.99, 2), "zero coma noranta-nou");
assert_eq!(f(&l, 1.01, 2), "un coma zero un");
assert_eq!(f(&l, 12.34, 2), "dotze coma trenta-quatre");
assert_eq!(f(&l, 99.99, 2), "noranta-nou coma noranta-nou");
assert_eq!(f(&l, 100.5, 1), "cent coma cinc");
assert_eq!(
f(&l, 1234.56, 2),
"mil dos-cents trenta-quatre coma cinquanta-sis"
);
assert_eq!(f(&l, -0.5, 1), "menys zero coma cinc");
assert_eq!(f(&l, -1.5, 1), "menys un coma cinc");
assert_eq!(f(&l, -12.34, 2), "menys dotze coma trenta-quatre");
assert_eq!(f(&l, 1.005, 3), "un coma zero zero cinc");
assert_eq!(f(&l, 2.675, 3), "dos coma sis-cents setanta-cinc");
}
#[test]
fn corpus_decimal_rows() {
let l = LangCa::new();
assert_eq!(d(&l, "0.01", 2), "zero coma zero un");
assert_eq!(d(&l, "1.10", 2), "un coma un");
assert_eq!(d(&l, "12.345", 3), "dotze coma tres-cents quaranta-cinc");
assert_eq!(d(&l, "0.001", 3), "zero coma zero zero un");
assert_eq!(
d(&l, "98746251323029.99", 2),
"noranta-vuit bilions set-cents quaranta-sis mil dos-cents \
cinquanta-un milions tres-cents vint-i-tres mil vint-i-nou \
coma noranta-vuit"
);
}
#[test]
fn ca_specific_quirks() {
let l = LangCa::new();
assert_eq!(f(&l, 5.0, 1), "cinc coma zero");
assert_eq!(f(&l, 0.0, 1), "zero coma zero");
assert_eq!(f(&l, 100.0, 1), "cent coma zero");
assert_eq!(
f(&l, 1e16, 16),
"deu mil bilions coma zero zero zero zero zero zero zero zero \
zero zero zero zero zero zero zero"
);
assert_eq!(
f(&l, 1.2345678901234568e16, 0),
"dotze mil tres-cents quaranta-cinc bilions sis-cents setanta-vuit \
mil nou-cents un milions dos-cents trenta-quatre mil cinc-cents \
seixanta-vuit coma"
);
let v = FloatValue::Float {
value: 2.675,
precision: 3,
};
for p in [None, Some(1), Some(3), Some(5)] {
assert_eq!(
l.to_cardinal_float(&v, p).unwrap(),
"dos coma sis-cents setanta-cinc"
);
}
}
#[test]
fn repr_precision_matches_python() {
for (v, want) in [
(0.5, 1u32),
(2.675, 3),
(12.34, 2),
(0.0, 1),
(1.0, 1),
(5.0, 1),
(100.0, 1),
(-1.5, 1),
(1e15, 1),
(1e16, 16),
(1e17, 17),
(1e21, 21),
(1e-4, 4),
(1e-5, 5),
(1e-20, 20),
(1.5e-25, 26),
(1.2345e-7, 11),
(98746251323029.98, 2),
(1.2345678901234568e16, 0),
(9.876543210987654e16, 1),
] {
assert_eq!(float_repr_precision(v), want, "precision of {:e}", v);
}
}
#[test]
fn fractional_cents_uses_ca_float_path() {
let l = LangCa::new();
assert_eq!(
l.cardinal_from_decimal(&BigDecimal::from_str("1.2500").unwrap())
.unwrap(),
"un coma vint-i-cinc"
);
let v = CurrencyValue::parse("1.0125", false, true, true).unwrap();
assert_eq!(
l.to_currency(&v, "USD", true, None, false).unwrap(),
"un dòlar amb un coma vint-i-cinc centaus"
);
}
#[test]
fn non_finite_raises_like_python() {
let l = LangCa::new();
assert!(matches!(
l.to_cardinal_float(
&FloatValue::Float {
value: f64::NAN,
precision: 1
},
None
),
Err(N2WError::Value(_))
));
assert!(matches!(
l.to_cardinal_float(
&FloatValue::Float {
value: f64::INFINITY,
precision: 1
},
None
),
Err(N2WError::Overflow(_))
));
}
}