use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const ONES: [&str; 10] = [
"", "un", "dos", "tres", "quatre", "cinc", "sièis", "sèt", "uèch", "nòu",
];
const TENS: [&str; 10] = [
"",
"dètz",
"vint",
"trenta",
"quaranta",
"cinquanta",
"seissanta",
"setanta",
"ochanta",
"nonanta",
];
const HUNDRED: &str = "cent";
const THOUSAND: &str = "mil";
const MILLION: &str = "milion";
const POINTWORD: &str = "point";
const SEPARATOR: &str = " ";
const BASE_DEFAULT_SEPARATOR: &str = ",";
pub struct LangOc {
forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangOc {
fn default() -> Self {
Self::new()
}
}
impl LangOc {
pub fn new() -> Self {
let mut forms = HashMap::with_capacity(2);
forms.insert(
"EUR",
CurrencyForms::new(&["èuro", "èuros"], &["centim", "centims"]),
);
forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
LangOc { forms }
}
fn fallback_forms(&self) -> &CurrencyForms {
self.forms
.get("EUR")
.expect("EUR is the first CURRENCY_FORMS entry and is inserted in new()")
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return "zero".to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&number.abs()));
}
let ten = BigInt::from(10);
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 number < &ten {
let i = u32::try_from(number).expect("0 < number < 10") as usize;
ONES[i].to_string()
} else if number < &hundred {
let tens_val = u32::try_from(number / &ten).expect("< 10") as usize;
let ones_val = u32::try_from(number % &ten).expect("< 10") as usize;
if ones_val == 0 {
TENS[tens_val].to_string()
} else {
format!("{} {}", TENS[tens_val], ONES[ones_val])
}
} else if number < &thousand {
let hundreds_val = u32::try_from(number / &hundred).expect("< 10") as usize;
let remainder = number % &hundred;
let mut result = format!("{} {}", ONES[hundreds_val], HUNDRED);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
result
} else if number < &million {
let thousands_val = number / &thousand;
let remainder = number % &thousand;
let mut result = format!("{} {}", self.int_to_word(&thousands_val), THOUSAND);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
result
} else if number < &billion {
let millions_val = number / &million;
let remainder = number % &million;
let mut result = format!("{} {}", self.int_to_word(&millions_val), MILLION);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
result
} else {
number.to_string()
}
}
fn cardinal_from_str(&self, n: &str) -> Result<String> {
let n = n.trim();
let (ret_prefix, n) = match n.strip_prefix('-') {
Some(rest) => (NEGWORD, rest),
None => ("", n),
};
if let Some((left, right)) = n.split_once('.') {
let mut ret = format!(
"{}{} {} ",
ret_prefix,
self.int_to_word(&python_int(left)?),
POINTWORD
);
for ch in right.chars() {
let d = python_int(&ch.to_string())?;
ret.push_str(&self.int_to_word(&d));
ret.push(' ');
}
Ok(ret.trim().to_string())
} else {
Ok(format!("{}{}", ret_prefix, self.int_to_word(&python_int(n)?))
.trim()
.to_string())
}
}
}
fn python_float_repr(v: f64) -> String {
if v.is_nan() {
return "nan".to_string();
}
if v.is_infinite() {
return (if v.is_sign_negative() { "-inf" } else { "inf" }).to_string();
}
let sign = if v.is_sign_negative() { "-" } else { "" };
let a = v.abs();
let s = format!("{:e}", a);
let (mant, exp) = s.split_once('e').expect("LowerExp always emits an 'e'");
let exp: i32 = exp.parse().expect("LowerExp emits an integer exponent");
let mut digits: String = mant.chars().filter(|c| *c != '.').collect();
let mut decpt = exp + 1;
let frac_digits = digits.chars().count() as i32 - decpt;
if frac_digits > 0 && a != 0.0 {
let t = format!("{:.*}", frac_digits as usize, a);
let (ip, fp) = t.split_once('.').expect("frac_digits > 0 forces a point");
let all = format!("{}{}", ip, fp);
let trimmed = all.trim_start_matches('0');
if !trimmed.is_empty() {
let lead = all.chars().count() - trimmed.chars().count();
digits = trimmed.to_string();
decpt = ip.chars().count() as i32 - lead as i32;
}
}
let n = digits.chars().count() as i32;
if decpt <= -4 || decpt > 16 {
let e = decpt - 1;
let mut out = String::from(sign);
let mut it = digits.chars();
out.push(it.next().expect("a finite double has at least one digit"));
if n > 1 {
out.push('.');
out.push_str(it.as_str());
}
out.push('e');
out.push(if e < 0 { '-' } else { '+' });
out.push_str(&format!("{:02}", (e as i64).abs()));
out
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), digits)
} else if decpt >= n {
format!("{}{}{}.0", sign, digits, "0".repeat((decpt - n) as usize))
} else {
let k = decpt as usize;
format!(
"{}{}.{}",
sign,
digits.chars().take(k).collect::<String>(),
digits.chars().skip(k).collect::<String>()
)
}
}
fn python_decimal_str(d: &BigDecimal) -> String {
let (unscaled, scale) = d.as_bigint_and_exponent();
let sign = if unscaled.is_negative() { "-" } else { "" };
let int_digits = unscaled.abs().to_string();
let exp: i64 = -scale;
let ndig = int_digits.chars().count() as i64;
let leftdigits = exp + ndig;
let dotplace = if exp <= 0 && leftdigits > -6 {
leftdigits
} else {
1
};
let (intpart, fracpart) = if dotplace <= 0 {
(
"0".to_string(),
format!(".{}{}", "0".repeat((-dotplace) as usize), int_digits),
)
} else if dotplace >= ndig {
(
format!("{}{}", int_digits, "0".repeat((dotplace - ndig) as usize)),
String::new(),
)
} else {
let k = dotplace as usize;
(
int_digits.chars().take(k).collect::<String>(),
format!(".{}", int_digits.chars().skip(k).collect::<String>()),
)
};
let exp_part = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
format!("{}{}{}{}", sign, intpart, fracpart, exp_part)
}
fn python_str(v: &FloatValue) -> String {
match v {
FloatValue::Float { value, .. } => python_float_repr(*value),
FloatValue::Decimal { value, .. } => python_decimal_str(value),
}
}
fn python_int(s: &str) -> Result<BigInt> {
BigInt::from_str(s).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
s
))
})
}
impl Lang for LangOc {
fn cardinal_float_entry(
&self,
value: &crate::floatpath::FloatValue,
precision_override: Option<u32>,
) -> crate::base::Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(format!("{}-en", cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", repr_str))
}
fn str_to_number(&self, s: &str) -> Result<crate::strnum::ParsedNumber> {
match crate::strnum::python_decimal_parse(s)? {
crate::strnum::ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
p => Ok(p),
}
}
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
POINTWORD
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (ret, n) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, self.int_to_word(&n)).trim().to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-en", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
self.cardinal_from_str(&python_str(value))
}
fn lang_name(&self) -> &str {
"Num2Word_OC"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.forms.get(code)
}
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 separator = if separator == BASE_DEFAULT_SEPARATOR {
SEPARATOR
} else {
separator
};
let is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(i) => i.abs().to_string(),
CurrencyValue::Decimal { value: d, .. } => d.abs().to_string(),
};
let mut segments = s.split('.');
let p0 = segments.next().unwrap_or("");
let p1 = segments.next();
let left = if p0.is_empty() {
BigInt::zero()
} else {
BigInt::from_str(p0).map_err(|e| N2WError::Value(e.to_string()))?
};
let right = match p1 {
Some(frac) if !frac.is_empty() => {
let mut digits: String = frac.chars().take(2).collect();
while digits.chars().count() < 2 {
digits.push('0'); }
BigInt::from_str(&digits).map_err(|e| N2WError::Value(e.to_string()))?
}
_ => BigInt::zero(),
};
let forms = match self.forms.get(currency) {
Some(f) => f,
None => self.fallback_forms(),
};
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let left_str = self.int_to_word(&left);
let mut result = format!(
"{} {}",
left_str,
if left.is_one() { &cr1[0] } else { &cr1[1] }
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(if right.is_one() { &cr2[0] } else { &cr2[1] });
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod float_tests {
use super::*;
fn f(v: f64) -> Result<String> {
LangOc::new().to_cardinal_float(
&FloatValue::Float { value: v, precision: 99 },
None,
)
}
fn d(s: &str) -> Result<String> {
LangOc::new().to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 99,
},
None,
)
}
#[test]
fn corpus_cardinal_float() {
assert_eq!(f(0.0).unwrap(), "zero point zero");
assert_eq!(f(0.5).unwrap(), "zero point cinc");
assert_eq!(f(1.0).unwrap(), "un point zero");
assert_eq!(f(1.5).unwrap(), "un point cinc");
assert_eq!(f(2.25).unwrap(), "dos point dos cinc");
assert_eq!(f(3.14).unwrap(), "tres point un quatre");
assert_eq!(f(0.01).unwrap(), "zero point zero un");
assert_eq!(f(0.1).unwrap(), "zero point un");
assert_eq!(f(0.99).unwrap(), "zero point nòu nòu");
assert_eq!(f(1.01).unwrap(), "un point zero un");
assert_eq!(f(12.34).unwrap(), "dètz dos point tres quatre");
assert_eq!(f(99.99).unwrap(), "nonanta nòu point nòu nòu");
assert_eq!(f(100.5).unwrap(), "un cent point cinc");
assert_eq!(
f(1234.56).unwrap(),
"un mil dos cent trenta quatre point cinc sièis"
);
assert_eq!(f(-0.5).unwrap(), "minus zero point cinc");
assert_eq!(f(-1.5).unwrap(), "minus un point cinc");
assert_eq!(f(-12.34).unwrap(), "minus dètz dos point tres quatre");
}
#[test]
fn corpus_cardinal_decimal() {
assert_eq!(d("0.01").unwrap(), "zero point zero un");
assert_eq!(d("1.10").unwrap(), "un point un zero");
assert_eq!(d("12.345").unwrap(), "dètz dos point tres quatre cinc");
assert_eq!(
d("98746251323029.99").unwrap(),
"98746251323029 point nòu nòu"
);
assert_eq!(d("0.001").unwrap(), "zero point zero zero un");
}
#[test]
fn float_artefacts_are_not_reachable() {
assert_eq!(f(2.675).unwrap(), "dos point sièis sèt cinc");
assert_eq!(f(1.005).unwrap(), "un point zero zero cinc");
assert_eq!(f(2.5).unwrap(), "dos point cinc");
assert_eq!(f(0.005).unwrap(), "zero point zero zero cinc");
}
#[test]
fn shortest_repr_ties_go_to_even() {
assert_eq!(f(670352580196876.2).unwrap(), "670352580196876 point dos");
}
#[test]
fn float_repr_placement() {
assert_eq!(f(1.0).unwrap(), "un point zero");
assert_eq!(f(-1.0).unwrap(), "minus un point zero");
assert_eq!(f(-0.0).unwrap(), "minus zero point zero");
assert_eq!(f(1000000000.5).unwrap(), "1000000000 point cinc");
assert_eq!(f(1e15).unwrap(), "1000000000000000 point zero");
assert_eq!(f(9999999999999998.0).unwrap(), "9999999999999998 point zero");
assert_eq!(f(0.0001).unwrap(), "zero point zero zero zero un");
assert_eq!(
f(123456789.5).unwrap(),
"un cent vint tres milion quatre cent cinquanta sièis mil sèt cent ochanta nòu point cinc"
);
}
#[test]
fn exponent_notation_raises_value_error() {
for (v, lit) in [
(1e16, "1e+16"),
(1e21, "1e+21"),
(1e-5, "1e-05"),
(1e300, "1e+300"),
(-1e16, "1e+16"),
(-1e-5, "1e-05"),
(f64::INFINITY, "inf"),
(f64::NEG_INFINITY, "inf"),
] {
match f(v) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", lit)
),
other => panic!("{}: expected Value, got {:?}", lit, other),
}
}
match f(f64::NAN) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: 'nan'")
}
other => panic!("nan: expected Value, got {:?}", other),
}
for v in [1.5e16, 1.5e-5] {
match f(v) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: 'e'")
}
other => panic!("{}: expected Value, got {:?}", v, other),
}
}
}
#[test]
fn decimal_str_rules() {
assert_eq!(d("0.00").unwrap(), "zero point zero zero");
assert_eq!(d("5.00").unwrap(), "cinc point zero zero");
assert_eq!(d("5").unwrap(), "cinc");
assert_eq!(d("-1").unwrap(), "minus un");
assert_eq!(d("-0.5").unwrap(), "minus zero point cinc");
assert_eq!(d("0.000001").unwrap(), "zero point zero zero zero zero zero un");
match d("0.0000001") {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1E-7'")
}
other => panic!("0.0000001: expected Value, got {:?}", other),
}
match d("1E+16") {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1E+16'")
}
other => panic!("1E+16: expected Value, got {:?}", other),
}
}
#[test]
fn precision_override_is_ignored() {
let l = LangOc::new();
let v = FloatValue::Float { value: 2.675, precision: 3 };
assert_eq!(
l.to_cardinal_float(&v, Some(1)).unwrap(),
"dos point sièis sèt cinc"
);
assert_eq!(
l.to_cardinal_float(&v, Some(9)).unwrap(),
"dos point sièis sèt cinc"
);
}
}