use crate::base::{
default_to_cardinal, set_low_numwords, set_mid_numwords, Cards, Kwargs, Lang, N2WError,
Result,
};
use crate::currency::{default_to_currency, CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, Zero};
use std::collections::HashMap;
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const CENTAVOS: [&str; 2] = ["centavo", "centavos"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("NIO", CurrencyForms::new(&["córdoba", "córdobas"], &CENTAVOS));
m.insert(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["céntimo", "céntimos"]),
);
m.insert("USD", CurrencyForms::new(&["dólar", "dólares"], &CENTAVOS));
m
}
fn build_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()
}
const GENDER_STEM: &str = "o";
const RECURSION_MSG: &str = "maximum recursion depth exceeded \
(Python raises RecursionError here: dec > value makes high_part 0, so \
to_ordinal(low_part) re-enters with the same value)";
fn drop_last_chars(s: &str, n: usize) -> String {
let count = s.chars().count();
if count <= n {
return String::new();
}
s.chars().take(count - n).collect()
}
fn log1000_floor(value: &BigInt) -> u32 {
let thresholds: [(&str, u32); 6] = [
("1000", 1),
("1000000", 2),
("1000000000", 3),
("1000000000000", 4),
("999999999999996", 5), ("999999999999995072", 6), ];
let mut k = 0;
for (t, kk) in thresholds.iter() {
let t: BigInt = t.parse().expect("static threshold literal");
if value >= &t {
k = *kk;
} else {
break;
}
}
k
}
fn float_ordinal_reject(value: &FloatValue) -> N2WError {
if let FloatValue::Float { value: f, .. } = value {
if f.is_nan() {
return N2WError::Value("cannot convert float NaN to integer".into());
}
if f.is_infinite() {
return N2WError::Overflow("cannot convert float infinity to integer".into());
}
}
let shown = match value {
FloatValue::Float { value: f, .. } => format!("{}", f),
FloatValue::Decimal { value: d, .. } => crate::strnum::python_decimal_str(d),
};
N2WError::Type(format!(
"El float {} no puede ser tratado como un ordinal.",
shown
))
}
fn float_trunc_int(value: &FloatValue) -> Result<BigInt> {
match value {
FloatValue::Float { value: f, .. } => {
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(),
));
}
Ok(BigInt::from_f64(f.trunc()).expect("finite whole f64 converts exactly"))
}
FloatValue::Decimal { value: d, .. } => {
Ok(d.with_scale(0).as_bigint_and_exponent().0)
}
}
}
pub struct LangEsNi {
cards: Cards,
maxval: BigInt,
ords: HashMap<BigInt, &'static str>,
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_adjectives: HashMap<&'static str, &'static str>,
}
impl Default for LangEsNi {
fn default() -> Self {
Self::new()
}
}
impl LangEsNi {
pub fn new() -> Self {
let mut cards = Cards::new();
let high = ["non", "oct", "sept", "sext", "quint", "cuatr", "tr", "b", "m"];
let cap = 3 + 6 * high.len() as i64;
let mut n = cap;
for word in high.iter() {
if n <= 3 {
break;
}
cards.insert(
BigInt::from(10u8).pow((n - 3) as u32),
format!("{}illón", word),
);
n -= 6;
}
set_mid_numwords(
&mut cards,
&[
(1000, "mil"),
(100, "cien"),
(90, "noventa"),
(80, "ochenta"),
(70, "setenta"),
(60, "sesenta"),
(50, "cincuenta"),
(40, "cuarenta"),
(30, "treinta"),
],
);
set_low_numwords(
&mut cards,
&[
"veintinueve",
"veintiocho",
"veintisiete",
"veintiséis",
"veinticinco",
"veinticuatro",
"veintitrés",
"veintidós",
"veintiuno",
"veinte",
"diecinueve",
"dieciocho",
"diecisiete",
"dieciséis",
"quince",
"catorce",
"trece",
"doce",
"once",
"diez",
"nueve",
"ocho",
"siete",
"seis",
"cinco",
"cuatro",
"tres",
"dos",
"uno",
"cero",
],
);
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
let ords: HashMap<BigInt, &'static str> = [
(BigInt::from(1), "primer"),
(BigInt::from(2), "segund"),
(BigInt::from(3), "tercer"),
(BigInt::from(4), "cuart"),
(BigInt::from(5), "quint"),
(BigInt::from(6), "sext"),
(BigInt::from(7), "séptim"),
(BigInt::from(8), "octav"),
(BigInt::from(9), "noven"),
(BigInt::from(10), "décim"),
(BigInt::from(20), "vigésim"),
(BigInt::from(30), "trigésim"),
(BigInt::from(40), "cuadragésim"),
(BigInt::from(50), "quincuagésim"),
(BigInt::from(60), "sexagésim"),
(BigInt::from(70), "septuagésim"),
(BigInt::from(80), "octogésim"),
(BigInt::from(90), "nonagésim"),
(BigInt::from(100), "centésim"),
(BigInt::from(200), "ducentésim"),
(BigInt::from(300), "tricentésim"),
(BigInt::from(400), "cuadrigentésim"),
(BigInt::from(500), "quingentésim"),
(BigInt::from(600), "sexcentésim"),
(BigInt::from(700), "septigentésim"),
(BigInt::from(800), "octigentésim"),
(BigInt::from(900), "noningentésim"),
(BigInt::from(1_000i64), "milésim"),
(BigInt::from(1_000_000i64), "millonésim"),
(BigInt::from(1_000_000_000i64), "billonésim"),
(BigInt::from(1_000_000_000_000i64), "trillonésim"),
(BigInt::from(1_000_000_000_000_000i64), "cuadrillonésim"),
]
.into_iter()
.collect();
LangEsNi {
cards,
maxval,
ords,
exclude_title: vec!["y".into(), "menos".into(), "punto".into()],
currency_forms: build_currency_forms(),
currency_adjectives: build_currency_adjectives(),
}
}
fn ords_get(&self, key: &BigInt) -> Result<&'static str> {
self.ords
.get(key)
.copied()
.ok_or_else(|| N2WError::Key(format!("{}", key)))
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"El número negativo {} no puede ser tratado como un ordinal.",
value
)));
}
Ok(())
}
fn to_ordinal_gendered(&self, value: &BigInt, gender: &str) -> Result<String> {
let mut gender_stem = if gender == "f" { "a" } else { "o" };
self.verify_ordinal(value)?;
let ten = BigInt::from(10);
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let e18 = BigInt::from(10u8).pow(18);
let text: String = if value.is_zero() {
String::new()
} else if value <= &ten {
format!("{}{}", self.ords_get(value)?, gender_stem)
} else if value <= &BigInt::from(29) {
gender_stem = "o";
let dec = (value / &ten) * &ten;
format!(
"{}{}{}",
self.ords_get(&dec)?.replace('é', "e"),
gender_stem,
self.to_ordinal_gendered(&(value % &ten), gender)?
)
} else if value <= &hundred {
let dec = (value / &ten) * &ten;
format!(
"{}{} {}",
self.ords_get(&dec)?,
gender_stem,
self.to_ordinal_gendered(&(value - &dec), gender)?
)
} else if value <= &thousand {
let cen = (value / &hundred) * &hundred;
format!(
"{}{} {}",
self.ords_get(&cen)?,
gender_stem,
self.to_ordinal_gendered(&(value - &cen), gender)?
)
} else if value < &e18 {
let dec = thousand.pow(log1000_floor(value));
let (high_part, low_part) = value.div_mod_floor(&dec);
let cardinal = if !high_part.is_one() {
self.to_cardinal(&high_part)?
} else {
String::new()
};
let ordword = self.ords_get(&dec)?;
if high_part.is_zero() {
return Err(N2WError::Value(format!("{}: {}", RECURSION_MSG, value)));
}
format!(
"{}{}{} {}",
cardinal,
ordword,
gender_stem,
self.to_ordinal_gendered(&low_part, gender)?
)
} else {
self.to_cardinal(value)?
};
Ok(text.trim().replace("oo", "o"))
}
}
impl Lang for LangEsNi {
fn es_currency_ordinal_fires(&self) -> bool {
true
}
fn str_to_number(&self, s: &str) -> crate::base::Result<crate::strnum::ParsedNumber> {
crate::lang_es::es_str_to_number(s)
}
fn default_currency(&self) -> &str {
"NIO"
}
fn default_separator(&self) -> &str {
" con"
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"menos "
}
fn pointword(&self) -> &str {
"punto"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
let (ctext_in, cnum) = l;
let (ntext_in, nnum) = r;
let mut ctext = ctext_in.to_string();
let mut ntext = ntext_in.to_string();
let one = BigInt::one();
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let million = BigInt::from(1_000_000);
if cnum.is_one() {
if nnum < &million {
return (ntext, nnum.clone());
}
ctext = "un".to_string();
} else if cnum == &hundred && !(nnum % &thousand).is_zero() {
ctext.push('t');
ctext.push_str(GENDER_STEM);
}
if nnum < cnum {
if cnum < &hundred {
return (format!("{} y {}", ctext, ntext), cnum + nnum);
}
return (format!("{} {}", ctext, ntext), cnum + nnum);
} else if (nnum % &million).is_zero() && cnum > &one {
ntext = format!("{}lones", drop_last_chars(&ntext, 3));
}
if nnum == &hundred {
if cnum == &BigInt::from(5) {
ctext = "quinien".to_string();
ntext = String::new();
} else if cnum == &BigInt::from(7) {
ctext = "sete".to_string();
} else if cnum == &BigInt::from(9) {
ctext = "nove".to_string();
}
ntext.push('t');
ntext.push_str(GENDER_STEM);
ntext.push('s');
} else {
if nnum >= &thousand {
if ctext.ends_with("veintiuno") {
ctext = format!("{}ún", drop_last_chars(&ctext, 3));
} else if ctext.ends_with("uno") {
ctext = drop_last_chars(&ctext, 1);
}
}
ntext = format!(" {}", ntext);
}
(format!("{}{}", ctext, ntext), cnum * nnum)
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let v = value.abs();
if &v >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) deber ser inferior a {}.",
v,
self.maxval()
)));
}
default_to_cardinal(self, value)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.to_ordinal_gendered(value, "m")
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
Ok(format!("{}º", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
match value.as_whole_int() {
Some(i) => self.to_ordinal(&i),
None => Err(float_ordinal_reject(value)),
}
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
match value.as_whole_int() {
Some(i) if !i.is_negative() => Ok(format!("{}º", repr_str)),
Some(_) => Err(N2WError::Type(format!(
"El número negativo {} no puede ser tratado como un ordinal.",
repr_str
))),
None => Err(float_ordinal_reject(value)),
}
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.to_cardinal(&float_trunc_int(value)?)
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["gender"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let gender = if kw.str("gender") == Some("f") { "f" } else { "m" };
self.to_ordinal_gendered(value, gender)
}
fn to_ordinal_num_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["gender"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
self.verify_ordinal(value)?;
let marker = if kw.str("gender") == Some("f") { "ª" } else { "º" };
Ok(format!("{}{}", value, marker))
}
fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["suffix", "longval"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
self.to_year(value)
}
fn to_fraction(&self, numerator: &BigInt, denominator: &BigInt) -> Result<String> {
if denominator.is_zero() {
return Err(N2WError::ZeroDivision("denominator must not be zero".into()));
}
if denominator.is_one() || numerator.is_zero() {
return self.to_cardinal(numerator);
}
let is_negative = numerator.is_negative() ^ denominator.is_negative();
let abs_n = numerator.abs();
let abs_d = denominator.abs();
let den_word = if abs_d == BigInt::from(2) {
(if abs_n.is_one() { "medio" } else { "medios" }).to_string()
} else if abs_d == BigInt::from(3) {
(if abs_n.is_one() { "tercio" } else { "tercios" }).to_string()
} else if abs_d == BigInt::from(4) {
(if abs_n.is_one() { "cuarto" } else { "cuartos" }).to_string()
} else {
let mut w = self.to_ordinal(&abs_d)?;
if !abs_n.is_one() && !w.ends_with('s') {
w.push('s');
}
w
};
let num_word = if abs_n.is_one() {
"un".to_string()
} else {
self.to_cardinal(&abs_n)?
};
let sign = if is_negative { "menos " } else { "" };
Ok(format!("{}{} {}", sign, num_word, den_word))
}
fn lang_name(&self) -> &str {
"Num2Word_ES_NI"
}
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 result = default_to_currency(self, val, currency, cents, separator, adjective)?;
Ok(result.replace("uno", "un"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn int(s: &str) -> CurrencyValue {
CurrencyValue::parse(s, true, false, false).unwrap()
}
fn float(s: &str) -> CurrencyValue {
CurrencyValue::parse(s, false, true, true).unwrap()
}
fn cur(lang: &LangEsNi, code: &str, v: CurrencyValue) -> Result<String> {
lang.to_currency(&v, code, true, None, false)
}
#[test]
fn currency_corpus_rows() {
let l = LangEsNi::new();
let rows: Vec<(&str, CurrencyValue, &str)> = vec![
("EUR", int("0"), "cero euros"),
("EUR", int("1"), "un euro"),
("EUR", int("2"), "dos euros"),
("EUR", int("100"), "cien euros"),
("EUR", float("12.34"), "doce euros con treinta y cuatro céntimos"),
("EUR", float("0.01"), "cero euros con un céntimo"),
("EUR", float("1.0"), "un euro con cero céntimos"),
(
"EUR",
float("99.99"),
"noventa y nueve euros con noventa y nueve céntimos",
),
(
"EUR",
float("1234.56"),
"mil doscientos treinta y cuatro euros con cincuenta y seis céntimos",
),
(
"EUR",
float("-12.34"),
"menos doce euros con treinta y cuatro céntimos",
),
("EUR", int("1000000"), "un millón euros"),
("EUR", float("0.5"), "cero euros con cincuenta céntimos"),
("USD", int("0"), "cero dólares"),
("USD", int("1"), "un dólar"),
("USD", int("2"), "dos dólares"),
("USD", int("100"), "cien dólares"),
("USD", float("12.34"), "doce dólares con treinta y cuatro centavos"),
("USD", float("0.01"), "cero dólares con un centavo"),
("USD", float("1.0"), "un dólar con cero centavos"),
(
"USD",
float("99.99"),
"noventa y nueve dólares con noventa y nueve centavos",
),
(
"USD",
float("1234.56"),
"mil doscientos treinta y cuatro dólares con cincuenta y seis centavos",
),
(
"USD",
float("-12.34"),
"menos doce dólares con treinta y cuatro centavos",
),
("USD", int("1000000"), "un millón dólares"),
("USD", float("0.5"), "cero dólares con cincuenta centavos"),
];
for (code, val, want) in rows {
assert_eq!(cur(&l, code, val.clone()).unwrap(), want, "{code} {val:?}");
}
}
#[test]
fn currency_missing_codes_raise_not_implemented() {
let l = LangEsNi::new();
let args = [
int("0"),
int("1"),
int("2"),
int("100"),
float("12.34"),
float("0.01"),
float("1.0"),
float("99.99"),
float("1234.56"),
float("-12.34"),
int("1000000"),
float("0.5"),
];
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
for a in args.iter() {
match cur(&l, code, a.clone()) {
Err(N2WError::NotImplemented(msg)) => assert_eq!(
msg,
format!("Currency code \"{code}\" not implemented for \"Num2Word_ES_NI\"")
),
other => panic!("{code} {a:?}: expected NotImplemented, got {other:?}"),
}
}
}
}
#[test]
fn cheque_corpus_rows() {
let l = LangEsNi::new();
let v = BigDecimal::from_str("1234.56").unwrap();
assert_eq!(
l.to_cheque(&v, "EUR").unwrap(),
"MIL DOSCIENTOS TREINTA Y CUATRO AND 56/100 EUROS"
);
assert_eq!(
l.to_cheque(&v, "USD").unwrap(),
"MIL DOSCIENTOS TREINTA Y CUATRO AND 56/100 DÓLARES"
);
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
match l.to_cheque(&v, code) {
Err(N2WError::NotImplemented(msg)) => assert_eq!(
msg,
format!("Currency code \"{code}\" not implemented for \"Num2Word_ES_NI\"")
),
other => panic!("cheque {code}: expected NotImplemented, got {other:?}"),
}
}
assert_eq!(
l.to_cheque(&BigDecimal::from_str("1.0").unwrap(), "NIO").unwrap(),
"UNO AND 00/100 CÓRDOBAS"
);
assert_eq!(
l.to_cheque(&BigDecimal::from_str("21.0").unwrap(), "NIO").unwrap(),
"VEINTIUNO AND 00/100 CÓRDOBAS"
);
assert_eq!(
l.to_cheque(&BigDecimal::from_str("-1234.56").unwrap(), "NIO").unwrap(),
"MINUS MIL DOSCIENTOS TREINTA Y CUATRO AND 56/100 CÓRDOBAS"
);
}
#[test]
fn currency_nio_default() {
let l = LangEsNi::new();
assert_eq!(l.default_currency(), "NIO");
assert_eq!(l.default_separator(), " con");
let rows: Vec<(CurrencyValue, &str)> = vec![
(int("0"), "cero córdobas"),
(int("1"), "un córdoba"),
(int("2"), "dos córdobas"),
(int("100"), "cien córdobas"),
(int("-1"), "menos un córdoba"),
(int("1000000"), "un millón córdobas"),
(float("1.0"), "un córdoba con cero centavos"),
(float("0.01"), "cero córdobas con un centavo"),
(float("1.01"), "un córdoba con un centavo"),
(float("0.5"), "cero córdobas con cincuenta centavos"),
(float("12.34"), "doce córdobas con treinta y cuatro centavos"),
(
float("-12.34"),
"menos doce córdobas con treinta y cuatro centavos",
),
(
float("1234.56"),
"mil doscientos treinta y cuatro córdobas con cincuenta y seis centavos",
),
];
for (val, want) in rows {
assert_eq!(cur(&l, "NIO", val.clone()).unwrap(), want, "{val:?}");
}
}
#[test]
fn blanket_uno_replace_quirks() {
let l = LangEsNi::new();
assert_eq!(cur(&l, "NIO", int("21")).unwrap(), "veintiun córdobas");
assert_eq!(cur(&l, "NIO", int("-21")).unwrap(), "menos veintiun córdobas");
assert_eq!(
cur(&l, "NIO", float("21.21")).unwrap(),
"veintiun córdobas con veintiun centavos"
);
assert_eq!(
cur(&l, "NIO", float("101.0")).unwrap(),
"ciento un córdobas con cero centavos"
);
assert_eq!(
cur(&l, "NIO", float("31.0")).unwrap(),
"treinta y un córdobas con cero centavos"
);
assert_eq!(
cur(&l, "EUR", float("2000000.21")).unwrap(),
"dos millones euros con veintiun céntimos"
);
}
#[test]
fn currency_adjective_only_bites_on_usd() {
let l = LangEsNi::new();
assert_eq!(
l.to_currency(&int("1"), "USD", true, None, true).unwrap(),
"un US dólar"
);
assert_eq!(
l.to_currency(&int("2"), "USD", true, None, true).unwrap(),
"dos US dólares"
);
assert_eq!(
l.to_currency(&float("12.34"), "USD", true, None, true).unwrap(),
"doce US dólares con treinta y cuatro centavos"
);
assert_eq!(
l.to_currency(&int("1"), "NIO", true, None, true).unwrap(),
"un córdoba"
);
assert_eq!(
l.to_currency(&int("1"), "EUR", true, None, true).unwrap(),
"un euro"
);
}
#[test]
fn cents_terse_and_explicit_separator() {
let l = LangEsNi::new();
assert_eq!(
l.to_currency(&float("12.34"), "NIO", false, None, false).unwrap(),
"doce córdobas con 34 centavos"
);
assert_eq!(
l.to_currency(&float("1.0"), "NIO", false, None, false).unwrap(),
"un córdoba con 00 centavos"
);
assert_eq!(
l.to_currency(&float("0.01"), "NIO", false, None, false).unwrap(),
"cero córdobas con 01 centavo"
);
assert_eq!(
l.to_currency(&float("12.34"), "NIO", true, Some(" y"), false).unwrap(),
"doce córdobas y treinta y cuatro centavos"
);
assert_eq!(l.currency_precision("NIO"), 100);
assert_eq!(l.currency_precision("KWD"), 100);
}
use crate::floatpath::FloatValue;
#[test]
fn cardinal_float_corpus_rows() {
let l = LangEsNi::new();
let rows: Vec<(f64, u32, &str)> = vec![
(0.5, 1, "cero punto cinco"),
(1.5, 1, "uno punto cinco"),
(2.25, 2, "dos punto dos cinco"),
(3.14, 2, "tres punto uno cuatro"),
(0.01, 2, "cero punto cero uno"),
(0.1, 1, "cero punto uno"),
(0.99, 2, "cero punto nueve nueve"),
(1.01, 2, "uno punto cero uno"),
(12.34, 2, "doce punto tres cuatro"),
(99.99, 2, "noventa y nueve punto nueve nueve"),
(100.5, 1, "cien punto cinco"),
(1234.56, 2, "mil doscientos treinta y cuatro punto cinco seis"),
(-0.5, 1, "menos cero punto cinco"),
(-1.5, 1, "menos uno punto cinco"),
(-12.34, 2, "menos doce punto tres cuatro"),
(1.005, 3, "uno punto cero cero cinco"),
(2.675, 3, "dos punto seis siete cinco"),
];
for (value, precision, want) in rows {
let v = FloatValue::Float { value, precision };
assert_eq!(
l.to_cardinal_float(&v, None).unwrap(),
want,
"{value}"
);
}
}
#[test]
fn cardinal_decimal_corpus_rows() {
let l = LangEsNi::new();
let rows: Vec<(&str, &str)> = vec![
("0.01", "cero punto cero uno"),
("1.10", "uno punto uno cero"),
("12.345", "doce punto tres cuatro cinco"),
(
"98746251323029.99",
"noventa y ocho billones setecientos cuarenta y seis mil \
doscientos cincuenta y un millones trescientos veintitrés mil \
veintinueve punto nueve nueve",
),
("0.001", "cero punto cero cero uno"),
];
for (lit, want) in rows {
let d = BigDecimal::from_str(lit).unwrap();
let precision = d.as_bigint_and_exponent().1 as u32;
let v = FloatValue::Decimal { value: d, precision };
assert_eq!(l.to_cardinal_float(&v, None).unwrap(), want, "{lit}");
}
}
}