use crate::base::{KwVal, Kwargs, Lang, N2WError, Result};
use crate::currency::{parse_currency_parts, CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use crate::strnum::{python_decimal_parse, ParsedNumber};
use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const MINUS_PREFIX_WORD: &str = "meno ";
const FLOAT_INFIX_WORD: &str = " virgola ";
const YEAR_SUFFIX: &str = ". urtea";
const SMALL_CARDINALS: [&str; 20] = [
"zero",
"un",
"doî",
"trei",
"quattro",
"çinque",
"sëi",
"sette",
"eutto",
"neuve",
"dexe",
"unze",
"dozze",
"trezze",
"quattòrze",
"chinze",
"sezze",
"dïsette",
"dixeutto",
"dixineuve",
];
const TENS: [&str; 10] = [
"zero",
"dexe",
"vinti",
"trenta",
"quaranta",
"çinquanta",
"sciuscianta",
"settanta",
"ottanta",
"novanta",
];
const HUNDREDS: [&str; 4] = ["zero", "çento", "duxento", "trexento"];
const THOUSANDS: [&str; 4] = ["zero", "mille", "doamia", "træmia"];
const EXPONENTS: [&str; 10] = [
"mion",
"miliardo",
"bilion",
"biliardo",
"trilion",
"triliardo",
"quadrilion",
"quadriliardo",
"quintilion",
"quintiliardo",
];
const SMALL_ORDINALS: [&str; 20] = [
"zero",
"primmo",
"segondo",
"terso",
"quarto",
"quinto",
"sesto",
"setten",
"otten",
"noven",
"dexen",
"unzen",
"dozzen",
"trezzen",
"quattorzen",
"chinzen",
"sezzen",
"dïsetten",
"dixotten",
"dixinoven",
];
const CENTS: [&str; 2] = ["citto", "citti"];
const DOLLAR: [&str; 2] = ["dòllao", "dòllai"];
const FRANC: [&str; 2] = ["franco", "franchi"];
const CURRENCIES_FEM: [&str; 4] = ["GBP", "INR", "ISK", "ITL"];
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("AUD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("CAD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("CHF", CurrencyForms::new(&FRANC, &CENTS));
m.insert(
"CNY",
CurrencyForms::new(&["yuan", "yuan"], &["fen", "fen"]),
);
m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &CENTS));
m.insert("FRF", CurrencyForms::new(&FRANC, &CENTS));
m.insert(
"GBP",
CurrencyForms::new(&["sterliña", "sterliñe"], &["penny", "pence"]),
);
m.insert("HKD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert(
"INR",
CurrencyForms::new(&["rupia", "rupie"], &["paisa", "paise"]),
);
m.insert("ITL", CurrencyForms::new(&["lia", "lie"], &CENTS));
m.insert("JPY", CurrencyForms::new(&["yen", "yen"], &["sen", "sen"]));
m.insert("SGD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("NZD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("USD", CurrencyForms::new(&DOLLAR, &CENTS));
m
}
fn build_currency_adjectives() -> HashMap<&'static str, (&'static str, &'static str)> {
[
("AUD", ("australian", "australien")),
("CAD", ("canadeise", "canadeixi")),
("CHF", ("svissero", "svisseri")),
("FRF", ("franseise", "franseixi")),
("HKD", ("de Hong Kong", "de Hong Kong")),
("ITL", ("italiaña", "italiañe")),
("SGD", ("de Scingapô", "de Scingapô")),
("NZD", ("neozelandeise", "neozelandeixi")),
("USD", ("american", "americhen")),
]
.into_iter()
.collect()
}
fn tuple_index_error() -> N2WError {
N2WError::Index("tuple index out of range".into())
}
fn drop_last(s: &str, n: usize) -> String {
let total = s.chars().count();
s.chars().take(total.saturating_sub(n)).collect()
}
fn last_char(s: &str) -> Option<char> {
s.chars().next_back()
}
fn small_to_cardinal(number: u32, gender: &str) -> String {
debug_assert!(number < 20);
let mut string = SMALL_CARDINALS[number as usize].to_string();
if gender == "f" {
if number == 1 {
string = drop_last(&string, 1) + "ña";
}
if number == 2 {
string = drop_last(&string, 1) + "e";
}
if number == 3 {
string = drop_last(&string, 2) + "æ";
}
}
string
}
fn tens_to_cardinal(number: u32, gender: &str) -> String {
debug_assert!((20..100).contains(&number));
let (tens, units) = (number / 10, number % 10);
let mut string = TENS[tens as usize].to_string();
if units == 0 {
return string;
}
if tens != 2 {
string = drop_last(&string, 1) + "e";
}
if units == 1 || units == 8 {
string = drop_last(&string, 1);
}
let gender = if units != 1 { gender } else { "m" };
string + &small_to_cardinal(units, gender)
}
fn hundreds_to_cardinal(number: u32, gender: &str) -> Result<String> {
debug_assert!((100..1000).contains(&number));
let (hundreds, remainder) = (number / 100, number % 100);
let string = if (hundreds as usize) < HUNDREDS.len() {
let s = HUNDREDS[hundreds as usize].to_string();
if (80..=89).contains(&remainder) {
drop_last(&s, 1)
} else {
s
}
} else {
let s = small_to_cardinal(hundreds, "m").replace('ë', "e");
if (80..=89).contains(&remainder) {
s + "çent"
} else {
s + "çento"
}
};
if remainder == 0 {
Ok(string)
} else {
let gender = if remainder != 1 { gender } else { "m" };
Ok(string + &to_cardinal_gender(&BigInt::from(remainder), gender)?)
}
}
fn thousands_to_cardinal(number: u32, gender: &str) -> Result<String> {
debug_assert!((1000..1_000_000).contains(&number));
let (thousands, remainder) = (number / 1000, number % 1000);
let string = if (thousands as usize) < THOUSANDS.len() {
THOUSANDS[thousands as usize].to_string()
} else {
to_cardinal_gender(&BigInt::from(thousands), "m")?
.replace("doî", "doa")
.replace("trei", "træ")
.replace('ë', "e")
.replace('ò', "o")
+ "mia"
};
if remainder == 0 {
Ok(string)
} else {
let gender = if remainder != 1 { gender } else { "m" };
Ok(string + &to_cardinal_gender(&BigInt::from(remainder), gender)?)
}
}
fn big_number_to_cardinal(number: &BigInt, gender: &str) -> Result<String> {
debug_assert!(*number >= BigInt::from(1_000_000u32));
let million = BigInt::from(1_000_000u32);
let mils = number / &million;
let remainder = number % &million;
let rev_mils: Vec<char> = mils.to_string().chars().rev().collect();
let mut triplets: Vec<u32> = Vec::new();
let mut i = 0;
while i < rev_mils.len() {
let end = (i + 3).min(rev_mils.len());
let chunk: String = rev_mils[i..end].iter().rev().collect();
triplets.push(chunk.parse::<u32>().expect("<=3 decimal digits"));
i += 3;
}
if triplets.len() > EXPONENTS.len() {
return Err(N2WError::NotImplemented(
"The given number is too large.".into(),
));
}
let mut string = if !remainder.is_zero() {
let gender = if !remainder.is_one() { gender } else { "m" };
to_cardinal_gender(&remainder, gender)?
} else {
String::new()
};
for (triplet, exponent) in triplets.iter().zip(EXPONENTS.iter()) {
let prefix = if *triplet == 0 {
String::new()
} else if *triplet == 1 {
format!("un {}", exponent)
} else {
let head = to_cardinal_gender(&BigInt::from(*triplet), "m")? + " ";
if exponent.ends_with('n') {
head + &drop_last(exponent, 1) + "in"
} else {
head + &drop_last(exponent, 1) + "i"
}
};
if !prefix.is_empty() {
if !string.is_empty() {
if string.contains(" e ") {
string = format!("{}, {}", prefix, string);
} else {
string = format!("{} e {}", prefix, string);
}
} else {
string = prefix;
}
}
}
Ok(string)
}
fn to_cardinal_gender(number: &BigInt, gender: &str) -> Result<String> {
if number.is_negative() {
return Ok(MINUS_PREFIX_WORD.to_string() + &to_cardinal_gender(&(-number), gender)?);
}
if *number < BigInt::from(20u32) {
Ok(small_to_cardinal(number.to_u32().expect("< 20"), gender))
} else if *number < BigInt::from(100u32) {
Ok(tens_to_cardinal(number.to_u32().expect("< 100"), gender))
} else if *number < BigInt::from(1000u32) {
hundreds_to_cardinal(number.to_u32().expect("< 1000"), gender)
} else if *number < BigInt::from(1_000_000u32) {
thousands_to_cardinal(number.to_u32().expect("< 10**6"), gender)
} else {
big_number_to_cardinal(number, gender)
}
}
fn small_to_ordinal(number: u32, gender: &str, plural: bool) -> String {
debug_assert!(number < 20);
let mut string = SMALL_ORDINALS[number as usize].to_string();
if gender == "f" && string != "zero" {
if string.ends_with('o') {
string = drop_last(&string, 1) + "a";
} else {
string = drop_last(&string, 1) + "ña";
}
}
if plural && string != "zero" {
if string.ends_with('o') {
string = drop_last(&string, 1) + "i";
} else if string.ends_with('a') {
string = drop_last(&string, 1) + "e";
}
}
string
}
fn big_to_odinal(number: &BigInt, gender: &str, plural: bool) -> Result<String> {
debug_assert!(*number >= BigInt::from(20u32));
let mut string = to_cardinal_gender(number, "m")?;
if string.ends_with("ëi") || string.ends_with("ei") {
string = drop_last(&string, 2) + "ei";
} else if string.ends_with("quattòrze") {
string = drop_last(&string, 4) + "orze";
} else if string.ends_with('î') {
string = drop_last(&string, 1) + "i";
} else if matches!(last_char(&string), Some(c) if "oeai".contains(c)) {
string = drop_last(&string, 1);
}
string += "eximo";
if string.ends_with("mieximo") {
string = drop_last(&string, 5) + "lleximo";
} else if string.ends_with("oeutteximo") {
string = drop_last(&string, 10) + "otteximo";
} else if string.ends_with("eutteximo") {
string = drop_last(&string, 9) + "otteximo";
} else if string.ends_with("neuveximo") {
string = drop_last(&string, 8) + "oveximo";
}
if gender == "f" {
if plural {
string = drop_last(&string, 1) + "e";
} else {
string = drop_last(&string, 1) + "a";
}
} else if plural {
string = drop_last(&string, 1) + "i";
}
Ok(string)
}
fn to_ordinal_gender(number: &BigInt, gender: &str, plural: bool) -> Result<String> {
if number.is_negative() {
return Ok(MINUS_PREFIX_WORD.to_string() + &to_ordinal_gender(&(-number), gender, plural)?);
}
if *number < BigInt::from(20u32) {
Ok(small_to_ordinal(
number.to_u32().expect("< 20"),
gender,
plural,
))
} else {
big_to_odinal(number, gender, plural)
}
}
fn kw_gender(kw: &Kwargs) -> &'static str {
if kw.str("gender") == Some("f") {
"f"
} else {
"m"
}
}
fn kw_truthy(v: Option<&KwVal>) -> bool {
match v {
Some(KwVal::Bool(b)) => *b,
Some(KwVal::Int(i)) => *i != 0,
Some(KwVal::Str(s)) => !s.is_empty(),
Some(KwVal::List(l)) => !l.is_empty(),
Some(KwVal::None) | None => false,
}
}
fn negated(value: &FloatValue) -> FloatValue {
match value {
FloatValue::Float { value, precision } => FloatValue::Float {
value: -value,
precision: *precision,
},
FloatValue::Decimal { value, precision } => FloatValue::Decimal {
value: -value.clone(),
precision: *precision,
},
}
}
fn float_to_words(value: &FloatValue, gender: &str, plural: bool, ordinal: bool) -> Result<String> {
let precision = value.precision() as usize;
let (pre, post) = float2tuple(value);
let post_str = post.to_string();
let float_part = format!(
"{}{}",
"0".repeat(precision.saturating_sub(post_str.chars().count())),
post_str
);
let prefix = if ordinal {
to_ordinal_gender(&pre, gender, plural)?
} else {
to_cardinal_gender(&pre, gender)?
};
let mut digits: Vec<String> = Vec::with_capacity(float_part.chars().count());
for ch in float_part.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})?;
digits.push(to_cardinal_gender(&BigInt::from(d), "m")?);
}
let postfix = digits.join(" ");
Ok(format!("{}{}{}", prefix, FLOAT_INFIX_WORD, postfix))
}
pub struct LangLij {
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_adjectives: HashMap<&'static str, (&'static str, &'static str)>,
}
impl LangLij {
pub fn new() -> Self {
LangLij {
currency_forms: build_currency_forms(),
currency_adjectives: build_currency_adjectives(),
}
}
}
impl Default for LangLij {
fn default() -> Self {
LangLij::new()
}
}
impl Lang for LangLij {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" e"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
to_cardinal_gender(value, "m")
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
to_ordinal_gender(value, "m", false)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(self.to_cardinal(value)? + YEAR_SUFFIX)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
if value.is_negative() {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
self.to_cardinal_float(&negated(value), None)?
));
}
float_to_words(value, "m", false, false)
}
fn to_cardinal_float_kw(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
kw: &Kwargs,
) -> Result<String> {
if !kw.only(&["gender"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let gender = kw_gender(kw);
if let Some(i) = value.as_whole_int() {
return to_cardinal_gender(&i, gender);
}
if value.is_negative() {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
float_to_words(&negated(value), gender, false, false)?
));
}
float_to_words(value, gender, false, false)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if let Some(n) = value.as_whole_int() {
return to_ordinal_gender(&n, "m", false);
}
if value.is_negative() {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
float_to_words(&negated(value), "m", false, true)?
));
}
float_to_words(value, "m", false, true)
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(self.cardinal_float_entry(value, None)? + YEAR_SUFFIX)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::NaN => Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.InvalidOperation'>]".into(),
}),
other => Ok(other),
}
}
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();
if is_negative {
return Err(N2WError::Attribute(
"'Num2Word_LIJ' object has no attribute 'negword'".into(),
));
}
let abs_n = numerator.abs();
let abs_d = denominator.abs();
let num_word = self.to_cardinal(&abs_n)?;
let mut den_word = self.to_ordinal(&abs_d)?;
if !abs_n.is_one() {
den_word += "s"; }
Ok(format!("{} {}", num_word, den_word))
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["gender"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
to_cardinal_gender(value, kw_gender(kw))
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["gender", "plural"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
to_ordinal_gender(value, kw_gender(kw), kw_truthy(kw.get("plural")))
}
fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["longval"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
self.to_year(value)
}
fn lang_name(&self) -> &str {
"Num2Word_LIJ"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
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(tuple_index_error)
}
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 (left, right, is_negative) = parse_currency_parts(val, true, false, 100);
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let cr2 = &forms.subunit;
let mut cr1 = forms.unit.clone();
if adjective {
if let Some((adj_sg, adj_pl)) = self.currency_adjectives.get(currency) {
let sg = cr1.first().ok_or_else(tuple_index_error)?.clone();
let pl = cr1.get(1).ok_or_else(tuple_index_error)?.clone();
cr1 = vec![format!("{} {}", sg, adj_sg), format!("{} {}", pl, adj_pl)];
}
}
let gender = if CURRENCIES_FEM.contains(¤cy) {
"f"
} else {
"m"
};
if is_negative {
return Err(N2WError::Attribute(
"'Num2Word_LIJ' object has no attribute 'negword'".into(),
));
}
let right_int = right.as_bigint_and_exponent().0;
let money_str = to_cardinal_gender(&left, gender)?;
let cents_str = if cents {
to_cardinal_gender(&right_int, "m")?
} else {
format!("{:0>2}", right_int)
};
Ok(format!(
"{} {}{} {} {}",
money_str,
self.pluralize(&left, &cr1)?,
separator,
cents_str,
self.pluralize(&right_int, cr2)?
))
}
}