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, ToPrimitive, Zero};
use std::collections::HashMap;
const GENDER_STEM: &str = "o";
fn errmsg_negord(value: &BigInt) -> String {
format!(
"El número negativo {} no puede ser tratado como un ordinal.",
value
)
}
fn errmsg_toobig(value: &BigInt, maxval: &BigInt) -> String {
format!("abs({}) deber ser inferior a {}.", value, maxval)
}
fn log1000_trunc(value: &BigInt) -> u32 {
let x = value.to_f64().expect("caller guarantees value < 1e18");
let k = (x.ln() / 1000f64.ln()).trunc();
k as u32
}
fn drop_last_chars(s: &str, n: usize) -> String {
let chars: Vec<char> = s.chars().collect();
let keep = chars.len().saturating_sub(n);
chars[..keep].iter().collect()
}
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(
"GTQ",
CurrencyForms::new(&["quetzal", "quetzales"], &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()
}
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 LangEsGt {
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 LangEsGt {
fn default() -> Self {
Self::new()
}
}
impl LangEsGt {
pub fn new() -> Self {
let lows = ["non", "oct", "sept", "sext", "quint", "cuatr", "tr", "b", "m"];
let mut cards = Cards::new();
let cap: i64 = 3 + 6 * lows.len() as i64;
let mut n = cap;
for word in lows.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(1000), "milésim"),
(BigInt::from(10u8).pow(6), "millonésim"),
(BigInt::from(10u8).pow(9), "billonésim"),
(BigInt::from(10u8).pow(12), "trillonésim"),
(BigInt::from(10u8).pow(15), "cuadrillonésim"),
]
.into_iter()
.collect();
LangEsGt {
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(key.to_string()))
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(errmsg_negord(value)));
}
Ok(())
}
fn to_ordinal_gender(&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 twenty_nine = BigInt::from(29);
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 <= &twenty_nine {
gender_stem = "o";
let dec = (value / &ten) * &ten;
format!(
"{}{}{}",
self.ords_get(&dec)?.replace('é', "e"),
gender_stem,
self.to_ordinal_gender(&(value % &ten), gender)?
)
} else if value <= &hundred {
let dec = (value / &ten) * &ten;
format!(
"{}{} {}",
self.ords_get(&dec)?,
gender_stem,
self.to_ordinal_gender(&(value - &dec), gender)?
)
} else if value <= &thousand {
let cen = (value / &hundred) * &hundred;
format!(
"{}{} {}",
self.ords_get(&cen)?,
gender_stem,
self.to_ordinal_gender(&(value - &cen), gender)?
)
} else if value < &e18 {
let dec = BigInt::from(1000).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 ord_dec = self.ords_get(&dec)?;
if &low_part == value {
return Err(N2WError::Value(
"maximum recursion depth exceeded in comparison".to_string(),
));
}
format!(
"{}{}{} {}",
cardinal,
ord_dec,
gender_stem,
self.to_ordinal_gender(&low_part, gender)?
)
} else {
self.to_cardinal(value)?
};
Ok(text.trim().replace("oo", "o"))
}
}
impl Lang for LangEsGt {
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 {
"GTQ"
}
fn default_separator(&self) -> &str {
" y"
}
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 to_cardinal(&self, value: &BigInt) -> Result<String> {
let v = value.abs();
if &v >= self.maxval() {
return Err(N2WError::Overflow(errmsg_toobig(&v, self.maxval())));
}
default_to_cardinal(self, value)
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
let (ctext0, cnum) = l;
let (ntext0, nnum) = r;
let mut ctext = ctext0.to_string();
let mut ntext = ntext0.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", drop_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", 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_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 lang_name(&self) -> &str {
"Num2Word_ES_GT"
}
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 = match val {
CurrencyValue::Int(v) => match self.currency_forms.get(currency) {
None => default_to_currency(self, val, currency, cents, separator, adjective)?,
Some(forms) => {
let cr1 = &forms.unit;
let minus_str = if v.is_negative() { self.negword() } else { "" };
let abs_val = v.abs();
let mut money_str = self.to_cardinal(&abs_val)?;
let currency_str: &str = if abs_val.is_one() {
money_str = "un".to_string();
cr1[0].as_str()
} else if cr1.len() > 1 {
cr1[1].as_str()
} else {
cr1[0].as_str()
};
format!("{} {} {}", minus_str, money_str, currency_str)
.trim()
.to_string()
}
},
CurrencyValue::Decimal { .. } => {
let r = default_to_currency(self, val, currency, cents, separator, adjective)?;
let r = r.replace("veintiuno euro", "veintiún euro");
let r = r.replace("veintiuno céntimo", "veintiún céntimo");
let r = r.replace("uno euro", "un euro");
let r = r.replace("uno céntimo", "un céntimo");
let r = r.replace("uno centavo", "un centavo");
r.replace("uno dólar", "un dólar")
}
};
Ok(result.replace("uno", "un"))
}
}