use crate::base::{set_low_numwords, set_mid_numwords, Cards, Kwargs, Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use num_bigint::BigInt;
use crate::floatpath::FloatValue;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const GENDER_STEM: &str = "o";
const ORDS: &[(i64, &str)] = &[
(1, "primer"),
(2, "segund"),
(3, "tercer"),
(4, "cuart"),
(5, "quint"),
(6, "sext"),
(7, "séptim"),
(8, "octav"),
(9, "noven"),
(10, "décim"),
(20, "vigésim"),
(30, "trigésim"),
(40, "cuadragésim"),
(50, "quincuagésim"),
(60, "sexagésim"),
(70, "septuagésim"),
(80, "octogésim"),
(90, "nonagésim"),
(100, "centésim"),
(200, "ducentésim"),
(300, "tricentésim"),
(400, "cuadrigentésim"),
(500, "quingentésim"),
(600, "sexcentésim"),
(700, "septigentésim"),
(800, "octigentésim"),
(900, "noningentésim"),
(1_000, "milésim"),
(1_000_000, "millonésim"),
(1_000_000_000, "billonésim"),
(1_000_000_000_000, "trillonésim"),
(1_000_000_000_000_000, "cuadrillonésim"),
];
fn ords_get(key: i64) -> Option<&'static str> {
ORDS.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
}
fn strip_last_chars(s: &str, n: usize) -> String {
let count = s.chars().count();
if n >= count {
return String::new();
}
s.chars().take(count - n).collect()
}
fn pow10(n: u32) -> BigInt {
BigInt::from(10u32).pow(n)
}
fn log1000_floor(value: u64) -> i32 {
((value as f64).ln() / 1000f64.ln()) as i32
}
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 LangEsHn {
cards: Cards,
maxval: BigInt,
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangEsHn {
fn default() -> Self {
Self::new()
}
}
impl LangEsHn {
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(pow10((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 mut currency_forms: HashMap<&'static str, CurrencyForms> = HashMap::new();
currency_forms.insert(
"HNL",
CurrencyForms::new(&["lempira", "lempiras"], &["centavo", "centavos"]),
);
LangEsHn {
cards,
maxval,
exclude_title: vec!["y".into(), "menos".into(), "punto".into()],
currency_forms,
}
}
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_gender(&self, value: &BigInt, gender: &str) -> Result<String> {
let gender_stem = if gender == "f" { "a" } else { "o" };
self.verify_ordinal(value)?;
let text: String = if value.is_zero() {
String::new()
} else if value <= &BigInt::from(10) {
let v = value.to_i64().expect("0 < value <= 10");
format!("{}{}", ords_get(v).expect("ords 1..=10 present"), gender_stem)
} else if value <= &BigInt::from(29) {
let v = value.to_i64().expect("11 <= value <= 29");
let dec = (v / 10) * 10;
format!(
"{}{}{}",
ords_get(dec).expect("ords 10/20 present").replace('é', "e"),
"o",
self.to_ordinal_gender(&BigInt::from(v % 10), gender)?
)
} else if value <= &BigInt::from(100) {
let v = value.to_i64().expect("30 <= value <= 100");
let dec = (v / 10) * 10;
format!(
"{}{} {}",
ords_get(dec).expect("ords 30..=100 present"),
gender_stem,
self.to_ordinal_gender(&BigInt::from(v - dec), gender)?
)
} else if value <= &BigInt::from(1000) {
let v = value.to_i64().expect("101 <= value <= 1000");
let cen = (v / 100) * 100;
format!(
"{}{} {}",
ords_get(cen).expect("ords 100..=900,1000 present"),
gender_stem,
self.to_ordinal_gender(&BigInt::from(v - cen), gender)?
)
} else if value < &pow10(18) {
let v = value.to_u64().expect("1000 < value < 10^18 fits u64");
let k = log1000_floor(v);
let dec = BigInt::from(1000u32).pow(k as u32);
let (high_part, low_part) = value.div_rem(&dec);
let cardinal = if !high_part.is_one() {
self.to_cardinal(&high_part)?
} else {
String::new()
};
let stem = dec
.to_i64()
.and_then(ords_get)
.ok_or_else(|| N2WError::Key(format!("{}", dec)))?;
if &low_part == value {
return Err(N2WError::Value(format!(
"maximum recursion depth exceeded in comparison (Python \
RecursionError: math.log float overshoot made dec={} > \
value={}, so to_ordinal recurses on itself)",
dec, value
)));
}
format!(
"{}{}{} {}",
cardinal,
stem,
gender_stem,
self.to_ordinal_gender(&low_part, gender)?
)
} else {
self.to_cardinal(value)?
};
Ok(text.trim().replace("oo", "o"))
}
}
impl Lang for LangEsHn {
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 {
"HNL"
}
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 five = BigInt::from(5);
let seven = BigInt::from(7);
let nine = BigInt::from(9);
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let million = BigInt::from(1_000_000);
if cnum == &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", strip_last_chars(&ntext, 3));
}
if nnum == &hundred {
if cnum == &five {
ctext = "quinien".to_string();
ntext = String::new();
} else if cnum == &seven {
ctext = "sete".to_string();
} else if cnum == &nine {
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", strip_last_chars(&ctext, 3));
} else if ctext.ends_with("uno") {
ctext = strip_last_chars(&ctext, 1);
}
}
ntext = format!(" {}", ntext);
}
(format!("{}{}", ctext, ntext), cnum * nnum)
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
crate::base::default_to_cardinal(self, value)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.to_ordinal_gender(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_gender(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 currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let idx = if n.is_one() { 0 } else { 1 };
forms
.get(idx)
.cloned()
.ok_or_else(|| N2WError::Index("list index out of range".into()))
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
) -> Result<String> {
let result = crate::currency::default_to_currency(
self,
val,
currency,
cents,
separator.unwrap_or(self.default_separator()),
adjective,
)?;
Ok(result.replace("uno", "un"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn flt(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn dec(s: &str, precision: u32) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
}
}
#[test]
fn cardinal_float_corpus_rows() {
let l = LangEsHn::new();
let rows: Vec<(FloatValue, &str)> = vec![
(flt(0.5, 1), "cero punto cinco"),
(flt(1.5, 1), "uno punto cinco"),
(flt(2.25, 2), "dos punto dos cinco"),
(flt(3.14, 2), "tres punto uno cuatro"),
(flt(0.01, 2), "cero punto cero uno"),
(flt(0.1, 1), "cero punto uno"),
(flt(0.99, 2), "cero punto nueve nueve"),
(flt(1.01, 2), "uno punto cero uno"),
(flt(12.34, 2), "doce punto tres cuatro"),
(flt(99.99, 2), "noventa y nueve punto nueve nueve"),
(flt(100.5, 1), "cien punto cinco"),
(
flt(1234.56, 2),
"mil doscientos treinta y cuatro punto cinco seis",
),
(flt(-0.5, 1), "menos cero punto cinco"),
(flt(-1.5, 1), "menos uno punto cinco"),
(flt(-12.34, 2), "menos doce punto tres cuatro"),
(flt(1.005, 3), "uno punto cero cero cinco"),
(flt(2.675, 3), "dos punto seis siete cinco"),
];
for (v, want) in rows {
let got = l.to_cardinal_float(&v, None).unwrap();
assert_eq!(got, want, "to_cardinal_float({:?})", v);
}
}
#[test]
fn cardinal_dec_corpus_rows() {
let l = LangEsHn::new();
let rows: Vec<(FloatValue, &str)> = vec![
(dec("0.01", 2), "cero punto cero uno"),
(dec("1.10", 2), "uno punto uno cero"),
(dec("12.345", 3), "doce punto tres cuatro cinco"),
(
dec("98746251323029.99", 2),
"noventa y ocho billones setecientos cuarenta y seis mil \
doscientos cincuenta y un millones trescientos veintitrés mil \
veintinueve punto nueve nueve",
),
(dec("0.001", 3), "cero punto cero cero uno"),
];
for (v, want) in rows {
let got = l.to_cardinal_float(&v, None).unwrap();
assert_eq!(got, want, "to_cardinal_float({:?})", v);
}
}
}