use crate::base::{set_low_numwords, set_mid_numwords, Cards, Lang, N2WError, Result};
use crate::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;
const BIG_UNITS: [&str; 6] = [
"trilliard",
"trillion",
"billiard",
"billion",
"milliard",
"million",
];
const ORDS: [(&str, &str); 2] = [("cinq", "cinquième"), ("neuf", "neuvième")];
fn gen_high_numwords(units: &[&str], tens: &[&str], lows: &[&str]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for t in tens {
for u in units {
out.push(format!("{}{}", u, t));
}
}
out.reverse();
const REPLACEMENTS: &[(&str, &str)] = &[
("novemn", "noven"),
("novemo", "novo"),
("octoo", "octo"),
("quintd", "quind"),
("quintn", "quin"),
("quintq", "quinq"),
("quints", "quins"),
("quintt", "quint"),
("quintv", "quinv"),
("septenn", "septen"),
("septent", "sept"),
("sexn", "sen"),
("sexs", "ses"),
("tresd", "tred"),
("tresn", "tren"),
("tress", "tres"),
("tresv", "trev"),
("unno", "uno"),
];
for (k, v) in REPLACEMENTS {
out = out.iter().map(|o| o.replace(k, v)).collect();
}
out.extend(lows.iter().map(|s| s.to_string()));
out
}
fn pow10(n: i64) -> BigInt {
BigInt::from(10u8).pow(n as u32)
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const DOLLARS: [&str; 2] = ["dollar", "dollars"];
const CENTS: [&str; 2] = ["cent", "cents"];
const CENTIMES: [&str; 2] = ["centime", "centimes"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("EUR", CurrencyForms::new(&["euro", "euros"], &CENTIMES));
m.insert("USD", CurrencyForms::new(&DOLLARS, &CENTS));
m.insert("FRF", CurrencyForms::new(&["franc", "francs"], &CENTIMES));
m.insert("CAD", CurrencyForms::new(&DOLLARS, &CENTS));
m.insert("AUD", CurrencyForms::new(&DOLLARS, &CENTS));
m.insert("NZD", CurrencyForms::new(&DOLLARS, &CENTS));
m.insert("HKD", CurrencyForms::new(&DOLLARS, &CENTS));
m.insert("GBP", CurrencyForms::new(&["livre", "livres"], &["penny", "pence"]));
m.insert("CNY", CurrencyForms::new(&["yuan", "yuans"], &["fen", "jiaos"]));
m.insert("JPY", CurrencyForms::new(&["yen", "yens"], &["sen", "sens"]));
m.insert("INR", CurrencyForms::new(&["roupie", "roupies"], &["paisa", "paisas"]));
m.insert("RUB", CurrencyForms::new(&["rouble", "roubles"], &["kopeck", "kopecks"]));
m.insert("KRW", CurrencyForms::new(&["won", "wons"], &["jeon", "jeons"]));
m.insert("MXN", CurrencyForms::new(&["peso", "pesos"], &["centavo", "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 py_float_str(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f.is_sign_positive() {
"inf".to_string()
} else {
"-inf".to_string()
};
}
let a = f.abs();
if a != 0.0 && (a >= 1e16 || a < 1e-4) {
let s = format!("{:e}", f);
if let Some((m, e)) = s.split_once('e') {
let (sign, digits) = match e.strip_prefix('-') {
Some(d) => ("-", d.to_string()),
None => ("+", e.to_string()),
};
let digits = if digits.len() < 2 {
format!("0{}", digits)
} else {
digits
};
return format!("{}e{}{}", m, sign, digits);
}
s
} else if f.fract() == 0.0 {
format!("{:.1}", f)
} else {
format!("{}", f)
}
}
fn py_num_str(v: &FloatValue) -> String {
match v {
FloatValue::Float { value, .. } => py_float_str(*value),
FloatValue::Decimal { value, .. } => crate::strnum::python_decimal_str(value),
}
}
pub struct LangFrCh {
cards: Cards,
maxval: BigInt,
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_adjectives: HashMap<&'static str, &'static str>,
}
impl Default for LangFrCh {
fn default() -> Self {
Self::new()
}
}
impl LangFrCh {
pub fn new() -> Self {
let lows = ["non", "oct", "sept", "sext", "quint", "quadr", "tr", "b", "m"];
let units = [
"", "un", "duo", "tres", "quattuor", "quint", "sex", "septen", "octo", "novem",
];
let tens = [
"dec",
"vigint",
"trigint",
"quadragint",
"quinquagint",
"sexagint",
"septuagint",
"octogint",
"nonagint",
];
let mut high = vec!["cent".to_string()];
high.extend(gen_high_numwords(&units, &tens, &lows));
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(pow10(n), format!("{}illiard", word));
cards.insert(pow10(n - 3), format!("{}illion", word));
n -= 6;
}
set_mid_numwords(
&mut cards,
&[
(1000, "mille"),
(100, "cent"),
(90, "nonante"),
(80, "huitante"),
(70, "septante"),
(60, "soixante"),
(50, "cinquante"),
(40, "quarante"),
(30, "trente"),
],
);
set_low_numwords(
&mut cards,
&[
"vingt",
"dix-neuf",
"dix-huit",
"dix-sept",
"seize",
"quinze",
"quatorze",
"treize",
"douze",
"onze",
"dix",
"neuf",
"huit",
"sept",
"six",
"cinq",
"quatre",
"trois",
"deux",
"un",
"zéro",
],
);
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
LangFrCh {
cards,
maxval,
exclude_title: vec!["et".into(), "virgule".into(), "moins".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(())
}
fn verify_ordinal_float(&self, value: &FloatValue) -> Result<BigInt> {
match value.as_whole_int() {
Some(i) => {
if i.is_negative() {
Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
py_num_str(value)
)))
} else {
Ok(i)
}
}
None => Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
py_num_str(value)
))),
}
}
}
impl Lang for LangFrCh {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" et"
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"moins "
}
fn pointword(&self) -> &str {
"virgule"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
let (ltext, cnum) = l;
let (rtext, nnum) = r;
let mut ctext = ltext.to_string();
let mut ntext = rtext.to_string();
let eighty = BigInt::from(80);
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let million = BigInt::from(1_000_000);
let ten = BigInt::from(10);
if cnum.is_one() {
if nnum < &million {
return (rtext.to_string(), nnum.clone());
}
} else {
let ends_in_80 = (cnum - &eighty).mod_floor(&hundred).is_zero();
let round_hundred = (cnum % &hundred).is_zero() && cnum < &thousand;
if (ends_in_80 || round_hundred)
&& nnum < &million
&& ctext.chars().last() == Some('s')
{
ctext.pop();
}
if cnum < &thousand
&& nnum != &thousand
&& ntext.chars().last() != Some('s')
&& (nnum % &hundred).is_zero()
{
ntext.push('s');
}
}
if nnum < cnum && cnum < &hundred {
if (nnum % &ten).is_one() {
return (format!("{} et {}", ctext, ntext), cnum + nnum);
}
return (format!("{}-{}", ctext, ntext), cnum + nnum);
}
if nnum > cnum {
return (format!("{} {}", ctext, ntext), cnum * nnum);
}
(format!("{} {}", ctext, ntext), cnum + nnum)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
if value.is_one() {
return Ok("premier".to_string());
}
let word = self.to_cardinal(value)?;
for unit in BIG_UNITS {
if word == format!("un {}", unit) || word.ends_with(&format!(" {}s", unit)) {
let plural = word.ends_with('s');
let trimmed = word.trim_end_matches('s');
let stripped = trimmed.strip_prefix("un ").unwrap_or(trimmed);
return Ok(format!(
"{}ième{}",
stripped,
if plural { "s" } else { "" }
));
}
if word == unit {
return Ok(format!("{}ième", unit));
}
}
let mut w = word;
let mut replaced: Option<String> = None;
for (src, repl) in ORDS {
if let Some(head) = w.strip_suffix(src) {
replaced = Some(format!("{}{}", head, repl));
break;
}
}
if let Some(hit) = replaced {
w = hit;
} else {
if w.chars().last() == Some('e') {
w.pop();
}
if w.ends_with("ts") || w.ends_with("ents") {
w.pop();
}
w.push_str("ième");
}
Ok(w)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
let suffix = if value.is_one() { "er" } else { "me" };
Ok(format!("{}{}", value, suffix))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let i = self.verify_ordinal_float(value)?;
self.to_ordinal(&i)
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
let i = self.verify_ordinal_float(value)?;
Ok(format!(
"{}{}",
repr_str,
if i.is_one() { "er" } else { "me" }
))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
let i = 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(),
));
}
BigInt::from_f64(f.trunc()).ok_or_else(|| {
N2WError::Value("cannot convert float NaN to integer".into())
})?
}
FloatValue::Decimal { value: d, .. } => d.with_scale(0).as_bigint_and_exponent().0,
};
self.to_cardinal(&i)
}
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() { "demi" } else { "demis" }).to_string()
} else if abs_d == BigInt::from(3) {
"tiers".to_string()
} else if abs_d == BigInt::from(4) {
(if abs_n.is_one() { "quart" } else { "quarts" }).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 sign = if is_negative {
format!("{} ", self.negword().trim())
} else {
String::new()
};
Ok(format!("{}{} {}", sign, self.to_cardinal(&abs_n)?, den_word))
}
fn lang_name(&self) -> &str {
"Num2Word_FR_CH"
}
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.abs() <= BigInt::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());
if let CurrencyValue::Int(v) = val {
let forms = match self.currency_forms.get(currency) {
Some(f) => f,
None => return self.to_cardinal(v),
};
let _ = adjective;
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 cr1 = &forms.unit;
let currency_str = if abs_val <= BigInt::one() {
cr1.first()
} else if cr1.len() > 1 {
cr1.get(1)
} else {
cr1.first()
}
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
return Ok(format!("{}{} {}", minus_str, money_str, currency_str));
}
crate::currency::default_to_currency(self, val, currency, cents, separator, adjective)
}
}