use crate::base::{
default_to_cardinal, set_low_numwords, set_mid_numwords, Cards, Lang, N2WError, Result,
};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::{default_to_cardinal_float, FloatValue};
use num_bigint::BigInt;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const HUNDREDS: [&str; 9] = [
"cento",
"duzentos",
"trezentos",
"quatrocentos",
"quinhentos",
"seiscentos",
"setecentos",
"oitocentos",
"novecentos",
];
const ORDS: [[&str; 10]; 3] = [
[
"", "primeiro", "segundo", "terceiro", "quarto", "quinto", "sexto", "sétimo", "oitavo",
"nono",
],
[
"",
"décimo",
"vigésimo",
"trigésimo",
"quadragésimo",
"quinquagésimo",
"sexagésimo",
"septuagésimo",
"octogésimo",
"nonagésimo",
],
[
"",
"centésimo",
"ducentésimo",
"tricentésimo",
"quadrigentésimo",
"quingentésimo",
"seiscentésimo",
"septigentésimo",
"octigentésimo",
"nongentésimo",
],
];
const EXTS: [&str; 7] = [
"mil",
"milhão",
"milhões",
"mil milhões",
"bilião",
"biliões",
"mil biliões",
];
const CURRENCY_EXTS: [&str; 6] = [
"milhão",
"milhões",
"bilião",
"biliões",
"trilião",
"triliões",
];
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const DOLLAR: [&str; 2] = ["dólar", "dólares"];
const CENTS: [&str; 2] = ["cêntimo", "cêntimos"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("AUD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert(
"BRL",
CurrencyForms::new(&["real", "reais"], &["centavo", "centavos"]),
);
m.insert("CAD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("NZD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("HKD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("EUR", CurrencyForms::new(&["euro", "euros"], &CENTS));
m.insert(
"GBP",
CurrencyForms::new(&["libra", "libras"], &["péni", "pence"]),
);
m.insert(
"CNY",
CurrencyForms::new(&["yuan", "yuans"], &["fen", "fen"]),
);
m.insert(
"JPY",
CurrencyForms::new(&["iene", "ienes"], &["sen", "sen"]),
);
m.insert(
"INR",
CurrencyForms::new(&["rupia", "rupias"], &["paisa", "paisas"]),
);
m.insert(
"RUB",
CurrencyForms::new(&["rublo", "rublos"], &["copeque", "copeques"]),
);
m.insert(
"KRW",
CurrencyForms::new(&["won", "wons"], &["jeon", "jeons"]),
);
m.insert(
"MXN",
CurrencyForms::new(&["peso", "pesos"], &["centavo", "centavos"]),
);
m.insert("USD", CurrencyForms::new(&DOLLAR, &CENTS));
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 insert_de(mut result: String, unit: &str) -> String {
for ext in CURRENCY_EXTS.iter() {
if result.contains(&format!("{} {}", ext, unit)) {
result = result.replacen(ext, &format!("{} de", ext), 1);
}
}
result
}
fn thousand_separator(idx: usize) -> Option<&'static str> {
match idx {
3 => Some("milésimo"),
6 => Some("milionésimo"),
9 => Some("milésimo milionésimo"),
12 => Some("bilionésimo"),
15 => Some("milésimo bilionésimo"),
_ => None,
}
}
fn hundreds_word(n: &BigInt) -> Option<&'static str> {
let i = n.to_u32()?;
if (1..=9).contains(&i) {
Some(HUNDREDS[(i - 1) as usize])
} else {
None
}
}
fn drop_last_4_chars(s: &str) -> String {
let cs: Vec<char> = s.chars().collect();
if cs.len() <= 4 {
String::new()
} else {
cs[..cs.len() - 4].iter().collect()
}
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
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)
}
}
}
fn fv_numeric_negative(v: &FloatValue) -> bool {
match v {
FloatValue::Float { value, .. } => *value < 0.0,
FloatValue::Decimal { value, .. } => value.is_negative(),
}
}
fn fv_abs(v: &FloatValue) -> FloatValue {
match v {
FloatValue::Float { value, precision } => FloatValue::Float {
value: value.abs(),
precision: *precision,
},
FloatValue::Decimal { value, precision } => FloatValue::Decimal {
value: value.abs(),
precision: *precision,
},
}
}
fn collapse_ws(s: &str) -> String {
let mut out = String::new();
let mut in_ws = false;
for c in s.chars() {
if c.is_whitespace() {
if !in_ws {
out.push(' ');
in_ws = true;
}
} else {
out.push(c);
in_ws = false;
}
}
out
}
fn re_match_ext(result: &str, ext: &str) -> bool {
let hay: Vec<char> = result.chars().collect();
let needle: Vec<char> = format!("{} e ", ext).chars().collect();
if needle.len() > hay.len() {
return false;
}
for i in 0..=(hay.len() - needle.len()) {
if hay[i..i + needle.len()] != needle[..] {
continue;
}
let j = i + needle.len();
let k = match hay[j..].iter().position(|c| *c == ' ') {
Some(off) => j + off,
None => continue,
};
let w = &hay[j..k];
if !w.iter().all(|c| is_word_char(*c)) {
continue;
}
let word: String = w.iter().collect();
if !(word.ends_with("ento") || word.ends_with("entos")) {
continue;
}
if hay[k + 1..].iter().any(|c| *c == 'e') {
return true;
}
}
false
}
pub struct LangPt {
cards: Cards,
maxval: BigInt,
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_adjectives: HashMap<&'static str, &'static str>,
}
impl Default for LangPt {
fn default() -> Self {
Self::new()
}
}
impl LangPt {
pub fn new() -> Self {
let high = [
"non", "oct", "sept", "sext", "quint", "quatr", "tr", "b", "m",
];
let mut cards = Cards::new();
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!("{}ilião", word),
);
n -= 6;
}
set_mid_numwords(
&mut cards,
&[
(1000, "mil"),
(100, "cem"),
(90, "noventa"),
(80, "oitenta"),
(70, "setenta"),
(60, "sessenta"),
(50, "cinquenta"),
(40, "quarenta"),
(30, "trinta"),
],
);
set_low_numwords(
&mut cards,
&[
"vinte",
"dezanove",
"dezoito",
"dezassete",
"dezasseis",
"quinze",
"catorze",
"treze",
"doze",
"onze",
"dez",
"nove",
"oito",
"sete",
"seis",
"cinco",
"quatro",
"três",
"dois",
"um",
"zero",
],
);
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
LangPt {
cards,
maxval,
exclude_title: vec!["e".into(), "vírgula".into(), "menos".into()],
currency_forms: build_currency_forms(),
currency_adjectives: build_currency_adjectives(),
}
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
}
impl Lang for LangPt {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" e"
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"menos "
}
fn pointword(&self) -> &str {
"vírgula"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
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 hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let million = BigInt::from(1_000_000);
let billion = BigInt::from(1_000_000_000);
if cnum.is_one() {
if nnum < &million {
return (ntext, nnum.clone());
}
ctext = "um".to_string();
} else if cnum == &hundred && !(nnum % &thousand).is_zero() {
ctext = "cento".to_string();
}
if nnum < cnum {
return (format!("{} e {}", ctext, ntext), cnum + nnum);
} else if (nnum % &billion).is_zero() && cnum > &BigInt::one() {
ntext = format!("{}liões", drop_last_4_chars(&ntext));
} else if (nnum % &million).is_zero() && cnum > &BigInt::one() {
ntext = format!("{}lhões", drop_last_4_chars(&ntext));
}
if ntext == "milião" {
ntext = "milhão".to_string();
}
if nnum == &hundred {
ctext = hundreds_word(cnum)
.unwrap_or_else(|| {
panic!(
"lang_pt merge: self.hundreds[{}] missing (Python raises KeyError)",
cnum
)
})
.to_string();
ntext = String::new();
} else {
ntext = format!(" {}", ntext);
}
(format!("{}{}", ctext, ntext), cnum * nnum)
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let mut result = default_to_cardinal(self, value)?;
for ext in EXTS.iter() {
if re_match_ext(&result, ext) {
result = result.replace(&format!("{} e", ext), ext);
}
}
Ok(result)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
let mut result = default_to_cardinal_float(self, value, precision_override)?;
for ext in EXTS.iter() {
if re_match_ext(&result, ext) {
result = result.replace(&format!("{} e", ext), ext);
}
}
Ok(result)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
let value_str = value.to_string();
let mut result: Vec<&str> = Vec::new();
let mut thousand_sep: &str = "";
for (idx, ch) in value_str.chars().rev().enumerate() {
if idx != 0 && idx % 3 == 0 {
thousand_sep = match thousand_separator(idx) {
Some(s) => s,
None => return Err(N2WError::Key(format!("{}", idx))),
};
}
if ch != '0' && !thousand_sep.is_empty() {
result.push(thousand_sep);
thousand_sep = "";
}
let d = ch.to_digit(10).expect("BigInt renders as digits only") as usize;
result.push(ORDS[idx % 3][d]);
}
result.reverse();
let joined = result.join(" ");
let collapsed = collapse_ws(joined.trim());
if collapsed.starts_with("primeiro") && value_str != "1" {
return Ok(collapsed.chars().skip(9).collect());
}
Ok(collapsed)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
Ok(format!("{}º", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
return Ok(format!(
"{} antes de Cristo",
self.to_cardinal(&value.abs())?
));
}
self.to_cardinal(value)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
self.to_ordinal(&float_trunc_int(value)?)
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
match value.as_whole_int() {
None => Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
repr_str
))),
Some(i) if i.is_negative() => Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
repr_str
))),
Some(_) => Ok(format!("{}º", repr_str)),
}
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
if fv_numeric_negative(value) {
return Ok(format!(
"{} antes de Cristo",
self.cardinal_float_entry(&fv_abs(value), None)?
));
}
self.cardinal_float_entry(value, None)
}
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() { "meio" } else { "meios" }.to_string()
} else if abs_d == BigInt::from(3) {
if abs_n.is_one() { "terço" } else { "terços" }.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() {
"um".to_string()
} else {
self.to_cardinal(&abs_n)?
};
let sign = if is_negative {
format!("{} ", self.negword().trim())
} else {
String::new()
};
Ok(format!("{}{} {}", sign, num_word, den_word))
}
fn lang_name(&self) -> &str {
"Num2Word_PT"
}
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> {
if let CurrencyValue::Int(v) = val {
let forms = match self.currency_forms(currency) {
Some(f) => f,
None => return self.to_cardinal(v),
};
let minus_str = if v.is_negative() {
format!("{} ", self.negword().trim())
} else {
String::new()
};
let abs_val = v.abs();
let money_str = self.to_cardinal(&abs_val)?;
let currency_str = if abs_val.is_one() {
&forms.unit[0]
} else {
forms.unit.get(1).unwrap_or(&forms.unit[0])
};
let result = insert_de(
format!("{}{} {}", minus_str, money_str, currency_str),
currency_str,
);
return Ok(result.trim().to_string());
}
let result = crate::currency::default_to_currency(
self,
val,
currency,
cents,
separator.unwrap_or(self.default_separator()),
adjective,
)?;
let forms = match self.currency_forms(currency) {
Some(f) => f,
None => return Err(N2WError::Key(format!("'{}'", currency))),
};
let plural = match forms.unit.get(1) {
Some(p) => p,
None => return Err(N2WError::Index("tuple index out of range".into())),
};
Ok(insert_de(result, plural))
}
}