use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "puntos";
const LANG_NAME: &str = "Num2Word_CEB";
const FALLBACK_CURRENCY: &str = "PHP";
const ZERO_WORD: &str = "siro";
const HUNDRED: &str = "gatos";
const THOUSAND: &str = "libo";
const MILLION: &str = "milyon";
const ONES: [&str; 10] = [
"", "usa", "duha", "tulo", "upat", "lima", "unom", "pito", "walo", "siyam",
];
const TENS: [&str; 10] = [
"",
"napulo",
"kawhaan",
"katloan",
"kap-atan",
"kalim-an",
"kan-uman",
"kapitoan",
"kawaloan",
"kasiyaman",
];
fn idx(n: &BigInt) -> usize {
n.to_usize()
.expect("caller proved 0 <= n < 10 before indexing")
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const SENTIMO: [&str; 2] = ["sentimo", "sentimo"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("PHP", CurrencyForms::new(&["piso", "piso"], &SENTIMO));
m.insert("USD", CurrencyForms::new(&["dolyar", "dolyar"], &SENTIMO));
m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &SENTIMO));
m
}
pub struct LangCeb {
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangCeb {
pub fn new() -> Self {
LangCeb {
exclude_title: ["minus", "puntos", "ug"]
.iter()
.map(|s| s.to_string())
.collect(),
currency_forms: build_currency_forms(),
}
}
}
fn py_int(s: &str) -> Result<BigInt> {
BigInt::from_str(s)
.map_err(|_| N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s)))
}
fn py_int_digit(ch: char) -> Result<usize> {
ch.to_digit(10)
.map(|d| d as usize)
.ok_or_else(|| N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch)))
}
fn shortest_repr_digits(a: f64) -> (String, i32) {
let split = |s: &str| -> (String, i32) {
let (mant, exp) = s.split_once('e').expect("{:e} always emits an 'e'");
(
mant.chars().filter(|c| *c != '.').collect(),
exp.parse::<i32>().expect("{:e} exponent is an integer") + 1,
)
};
let shortest = format!("{:e}", a);
let (digits, decpt) = split(&shortest);
let ties_even = format!("{:.*e}", digits.len() - 1, a);
if ties_even.parse::<f64>() == Ok(a) {
return split(&ties_even);
}
(digits, decpt)
}
fn py_str_f64(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 (digits, decpt) = shortest_repr_digits(v.abs());
let ndig = digits.len() as i32;
if decpt <= -4 || decpt > 16 {
let mantissa = if ndig > 1 {
format!("{}.{}", &digits[..1], &digits[1..])
} else {
digits.clone()
};
let exp = decpt - 1;
let (esign, eabs) = if exp < 0 {
("-", -(exp as i64))
} else {
("+", exp as i64)
};
return format!("{}{}e{}{:0>2}", sign, mantissa, esign, eabs);
}
if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), digits)
} else if decpt >= ndig {
format!(
"{}{}{}.0",
sign,
digits,
"0".repeat((decpt - ndig) as usize)
)
} else {
format!(
"{}{}.{}",
sign,
&digits[..decpt as usize],
&digits[decpt as usize..]
)
}
}
fn py_str_decimal(value: &BigDecimal) -> String {
let (coefficient, scale) = value.as_bigint_and_exponent();
let exp = -scale;
let sign = if coefficient.is_negative() { "-" } else { "" };
let int_digits = coefficient.abs().to_string();
let ndig = int_digits.len() 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 {
(
int_digits[..dotplace as usize].to_string(),
format!(".{}", &int_digits[dotplace as usize..]),
)
};
let exponent = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
format!("{}{}{}{}", sign, intpart, fracpart, exponent)
}
fn cardinal_from_repr(n: &str) -> Result<String> {
let n = n.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = cardinal_from_repr(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
let (left, right) = match n.split_once('.') {
Some(halves) => halves,
None => return Ok(int_to_word(&py_int(n)?)),
};
let mut ret = int_to_word(&py_int(left)?);
ret.push(' ');
ret.push_str(POINTWORD);
for digit in right.chars() {
let word = ONES[py_int_digit(digit)?];
ret.push(' ');
ret.push_str(if word.is_empty() { ZERO_WORD } else { word });
}
Ok(ret.trim().to_string())
}
fn pick_form(forms: &[String], n: &BigInt) -> Result<String> {
let i = if n.is_one() { 0 } else { 1 };
forms
.get(i)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
impl Default for LangCeb {
fn default() -> Self {
Self::new()
}
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
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 {
return ONES[idx(number)].to_string();
}
if number < &hundred {
let (t, o) = number.div_mod_floor(&ten);
let mut out = TENS[idx(&t)].to_string();
if !o.is_zero() {
out.push_str(" ug ");
out.push_str(ONES[idx(&o)]);
}
return out;
}
if number < &thousand {
let (h, r) = number.div_mod_floor(&hundred);
let mut out = String::new();
if h > BigInt::one() {
out.push_str(ONES[idx(&h)]);
out.push(' ');
}
out.push_str(HUNDRED);
if !r.is_zero() {
out.push_str(" ug ");
out.push_str(&int_to_word(&r));
}
return out;
}
if number < &million {
let (t, r) = number.div_mod_floor(&thousand);
let mut out = int_to_word(&t);
out.push(' ');
out.push_str(THOUSAND);
if !r.is_zero() {
out.push(' ');
out.push_str(&int_to_word(&r));
}
return out;
}
if number < &billion {
let (m, r) = number.div_mod_floor(&million);
let mut out = int_to_word(&m);
out.push(' ');
out.push_str(MILLION);
if !r.is_zero() {
out.push(' ');
out.push_str(&int_to_word(&r));
}
return out;
}
number.to_string()
}
impl Lang for LangCeb {
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.to_cardinal_float(value, None)?;
if value.as_whole_int() == Some(BigInt::from(1)) {
return Ok("una".to_string());
}
Ok(format!("ika-{}", cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("ika-{}", repr_str))
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
other => Ok(other),
}
}
fn default_currency(&self) -> &str {
"PHP"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"puntos"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = int_to_word(&value.abs());
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(int_to_word(value))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
let _ = precision_override;
let n = match value {
FloatValue::Float { value, .. } => py_str_f64(*value),
FloatValue::Decimal { value, .. } => py_str_decimal(value),
};
cardinal_from_repr(&n)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
let cardinal = self.to_cardinal(value)?;
if value.is_one() {
return Ok("una".to_string());
}
Ok(format!("ika-{}", cardinal))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("ika-{}", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn lang_name(&self) -> &str {
LANG_NAME
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
if forms.is_empty() {
return Ok(String::new());
}
Ok(if n.is_one() {
forms[0].clone()
} else {
forms[forms.len() - 1].clone()
})
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
) -> Result<String> {
let _ = adjective;
let separator = separator.unwrap_or(self.default_separator());
let (is_negative, s) = match val {
CurrencyValue::Int(v) => (v.is_negative(), v.abs().to_string()),
CurrencyValue::Decimal { value, .. } => (value.is_negative(), value.abs().to_string()),
};
let parts: Vec<&str> = s.split('.').collect();
let left = if parts[0].is_empty() {
BigInt::zero()
} else {
py_int(parts[0])?
};
let right = match parts.get(1) {
Some(frac) if !frac.is_empty() => {
let mut cents_digits: String = frac.chars().take(2).collect();
while cents_digits.chars().count() < 2 {
cents_digits.push('0');
}
py_int(¢s_digits)?
}
_ => BigInt::zero(),
};
let forms = self.currency_forms(currency).unwrap_or_else(|| {
self.currency_forms
.get(FALLBACK_CURRENCY)
.expect("FALLBACK_CURRENCY is inserted by build_currency_forms")
});
let mut result = int_to_word(&left);
result.push(' ');
result.push_str(&pick_form(&forms.unit, &left)?);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(&pick_form(&forms.subunit, &right)?);
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn f(v: f64, precision: u32) -> Result<String> {
LangCeb::new().to_cardinal_float(
&FloatValue::Float {
value: v,
precision,
},
None,
)
}
fn d(s: &str, precision: u32) -> Result<String> {
LangCeb::new().to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
},
None,
)
}
fn value_err(r: Result<String>) -> String {
match r {
Err(N2WError::Value(m)) => m,
other => panic!("expected ValueError, got {:?}", other),
}
}
#[test]
fn corpus_float_rows() {
assert_eq!(f(0.0, 1).unwrap(), "siro puntos siro");
assert_eq!(f(0.5, 1).unwrap(), "siro puntos lima");
assert_eq!(f(1.0, 1).unwrap(), "usa puntos siro");
assert_eq!(f(1.5, 1).unwrap(), "usa puntos lima");
assert_eq!(f(2.25, 2).unwrap(), "duha puntos duha lima");
assert_eq!(f(3.14, 2).unwrap(), "tulo puntos usa upat");
assert_eq!(f(0.01, 2).unwrap(), "siro puntos siro usa");
assert_eq!(f(0.1, 1).unwrap(), "siro puntos usa");
assert_eq!(f(0.99, 2).unwrap(), "siro puntos siyam siyam");
assert_eq!(f(1.01, 2).unwrap(), "usa puntos siro usa");
assert_eq!(f(12.34, 2).unwrap(), "napulo ug duha puntos tulo upat");
assert_eq!(
f(99.99, 2).unwrap(),
"kasiyaman ug siyam puntos siyam siyam"
);
assert_eq!(f(100.5, 1).unwrap(), "gatos puntos lima");
assert_eq!(
f(1234.56, 2).unwrap(),
"usa libo duha gatos ug katloan ug upat puntos lima unom"
);
assert_eq!(f(-0.5, 1).unwrap(), "minus siro puntos lima");
assert_eq!(f(-1.5, 1).unwrap(), "minus usa puntos lima");
assert_eq!(
f(-12.34, 2).unwrap(),
"minus napulo ug duha puntos tulo upat"
);
assert_eq!(f(1.005, 3).unwrap(), "usa puntos siro siro lima");
assert_eq!(f(2.675, 3).unwrap(), "duha puntos unom pito lima");
}
#[test]
fn corpus_decimal_rows() {
assert_eq!(d("0.01", 2).unwrap(), "siro puntos siro usa");
assert_eq!(d("1.10", 2).unwrap(), "usa puntos usa siro");
assert_eq!(
d("12.345", 3).unwrap(),
"napulo ug duha puntos tulo upat lima"
);
assert_eq!(
d("98746251323029.99", 2).unwrap(),
"98746251323029 puntos siyam siyam"
);
assert_eq!(d("0.001", 3).unwrap(), "siro puntos siro siro usa");
}
#[test]
fn f64_artefacts_come_from_repr_not_float2tuple() {
assert_eq!(f(1.005, 3).unwrap(), "usa puntos siro siro lima");
assert_eq!(f(2.675, 3).unwrap(), "duha puntos unom pito lima");
let expected = format!("siro puntos tulo {} upat", ["siro"; 15].join(" "));
assert_eq!(f(0.1 + 0.2, 17).unwrap(), expected);
}
#[test]
fn precision_override_is_inert() {
let l = LangCeb::new();
let v = FloatValue::Float {
value: 2.675,
precision: 3,
};
for over in [None, Some(0), Some(1), Some(2), Some(5), Some(17)] {
assert_eq!(
l.to_cardinal_float(&v, over).unwrap(),
"duha puntos unom pito lima"
);
}
}
#[test]
fn exponent_form_repr_raises_value_error() {
assert_eq!(f(1e15, 1).unwrap(), "1000000000000000 puntos siro");
assert_eq!(
value_err(f(1e16, 16)),
"invalid literal for int() with base 10: '1e+16'"
);
assert_eq!(
value_err(f(1e21, 21)),
"invalid literal for int() with base 10: '1e+21'"
);
assert_eq!(f(1e-4, 4).unwrap(), "siro puntos siro siro siro usa");
assert_eq!(
value_err(f(1e-5, 5)),
"invalid literal for int() with base 10: '1e-05'"
);
assert_eq!(
value_err(f(1.5e20, 19)),
"invalid literal for int() with base 10: 'e'"
);
assert_eq!(
value_err(d("1E+2", 2)),
"invalid literal for int() with base 10: '1E+2'"
);
assert_eq!(
value_err(d("1E-7", 7)),
"invalid literal for int() with base 10: '1E-7'"
);
}
#[test]
fn integral_decimal_takes_the_int_branch() {
assert_eq!(d("100", 0).unwrap(), "gatos");
assert_eq!(d("1", 0).unwrap(), "usa");
assert_eq!(d("-1", 0).unwrap(), "minus usa");
assert_eq!(d("5.00", 2).unwrap(), "lima puntos siro siro");
assert_eq!(d("0.0", 1).unwrap(), "siro puntos siro");
}
#[test]
fn negative_zero_float_keeps_its_sign() {
assert_eq!(f(-0.0, 1).unwrap(), "minus siro puntos siro");
assert_eq!(f(0.0, 1).unwrap(), "siro puntos siro");
}
#[test]
fn py_str_f64_matches_cpython_repr() {
assert_eq!(py_str_f64(1.0), "1.0");
assert_eq!(py_str_f64(-0.0), "-0.0");
assert_eq!(py_str_f64(0.0), "0.0");
assert_eq!(py_str_f64(100.0), "100.0");
assert_eq!(py_str_f64(0.5), "0.5");
assert_eq!(py_str_f64(2.675), "2.675");
assert_eq!(py_str_f64(1e15), "1000000000000000.0");
assert_eq!(py_str_f64(1e16), "1e+16");
assert_eq!(py_str_f64(1e-4), "0.0001");
assert_eq!(py_str_f64(1e-5), "1e-05");
assert_eq!(py_str_f64(1.5e20), "1.5e+20");
assert_eq!(py_str_f64(5e-324), "5e-324");
assert_eq!(py_str_f64(1e100), "1e+100");
assert_eq!(py_str_f64(2e16 + 8.0), "2.000000000000001e+16");
assert_eq!(py_str_f64(f64::INFINITY), "inf");
assert_eq!(py_str_f64(f64::NEG_INFINITY), "-inf");
assert_eq!(py_str_f64(f64::NAN), "nan");
assert_eq!(py_str_f64(670352580196876.25), "670352580196876.2");
assert_eq!(py_str_f64(161834668665500.125), "161834668665500.12");
assert_eq!(py_str_f64(9930461221140.8125), "9930461221140.812");
assert_eq!(py_str_f64(-254209913874244.625), "-254209913874244.62");
}
#[test]
fn py_str_decimal_matches_cpython_str() {
let s = |x: &str| py_str_decimal(&BigDecimal::from_str(x).unwrap());
assert_eq!(s("1.10"), "1.10");
assert_eq!(s("98746251323029.99"), "98746251323029.99");
assert_eq!(s("100"), "100");
assert_eq!(s("100.00"), "100.00");
assert_eq!(s("0.0"), "0.0");
assert_eq!(s("0.00"), "0.00");
assert_eq!(s("1E+2"), "1E+2");
assert_eq!(s("0E+2"), "0E+2");
assert_eq!(s("1E+16"), "1E+16");
assert_eq!(s("0.000001"), "0.000001");
assert_eq!(s("0.0000001"), "1E-7");
assert_eq!(s("1E-7"), "1E-7");
assert_eq!(s("-0.5"), "-0.5");
}
}