use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
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 ONES: [&str; 10] = [
"", "un", "dos", "tres", "kuater", "sinku", "seis", "shete", "ocho", "nuebe",
];
const TEENS: [&str; 10] = [
"dies",
"diesun",
"diesdos",
"diestres",
"dieskuater",
"diessinku",
"diesseis",
"diesshete",
"diesocho",
"diesnuebe",
];
const TENS: [&str; 10] = [
"",
"dies",
"binti",
"trinta",
"kuarenta",
"sinkuenta",
"sesenta",
"setenta",
"ochenta",
"nobenta",
];
const ZERO_WORD: &str = "sero";
const HUNDRED: &str = "shen";
const THOUSAND: &str = "mil";
const MILLION: &str = "miyon";
const NEGWORD: &str = "menos ";
const POINTWORD: &str = "punto";
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const SEN: [&str; 2] = ["sèn", "sèn"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("ANG", CurrencyForms::new(&FLORIN, &SEN));
m.insert("AWG", CurrencyForms::new(&FLORIN, &SEN));
m.insert("USD", CurrencyForms::new(&["dolar", "dolar"], &SEN));
m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &SEN));
m
}
const FLORIN: [&str; 2] = ["florin", "florin"];
pub struct LangPap {
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_fallback: CurrencyForms,
}
impl Default for LangPap {
fn default() -> Self {
Self::new()
}
}
impl LangPap {
pub fn new() -> Self {
LangPap {
exclude_title: vec!["i".to_string(), "punto".to_string(), "menos".to_string()],
currency_forms: build_currency_forms(),
currency_fallback: CurrencyForms::new(&FLORIN, &["sèn", "sèn"]),
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if *number < BigInt::from(10) {
return ONES[number.to_usize().unwrap()].to_string();
}
if *number < BigInt::from(20) {
return TEENS[(number - BigInt::from(10)).to_usize().unwrap()].to_string();
}
if *number < BigInt::from(100) {
let (t, o) = number.div_mod_floor(&BigInt::from(10));
let t = t.to_usize().unwrap(); let o = o.to_usize().unwrap(); let mut out = TENS[t].to_string();
if o != 0 {
out.push_str(" i ");
out.push_str(ONES[o]);
}
return out;
}
if *number < BigInt::from(1000) {
let (h, r) = number.div_mod_floor(&BigInt::from(100));
let hu = h.to_usize().unwrap(); let mut out = String::new();
if hu > 1 {
out.push_str(ONES[hu]);
out.push(' ');
}
out.push_str(HUNDRED);
if !r.is_zero() {
out.push_str(" i ");
out.push_str(&self.int_to_word(&r));
}
return out;
}
if *number < BigInt::from(1_000_000) {
let (t, r) = number.div_mod_floor(&BigInt::from(1000));
let mut out = String::new();
if t > BigInt::from(1) {
out.push_str(&self.int_to_word(&t));
out.push(' ');
}
out.push_str(THOUSAND);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
if *number < BigInt::from(1_000_000_000) {
let (m, r) = number.div_mod_floor(&BigInt::from(1_000_000));
let mut out = self.int_to_word(&m);
out.push(' ');
out.push_str(MILLION);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
number.to_string()
}
fn cardinal_from_str(&self, n: &str) -> Result<String> {
let n = n.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = self.cardinal_from_str(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
let (left, right) = match n.split_once('.') {
Some(halves) => halves,
None => return Ok(self.int_to_word(&py_int(n)?)),
};
let mut ret = self.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())
}
}
impl Lang for LangPap {
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 is_one = match value {
FloatValue::Float { value: f, .. } => *f == 1.0,
FloatValue::Decimal { value: d, .. } => d == &bigdecimal::BigDecimal::from(1),
};
if is_one {
return Ok("promé".to_string());
}
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(format!("di {}", cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}i", 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 {
"ANG"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"punto"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if *value < BigInt::zero() {
let inner = self.to_cardinal(&-value)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(self.int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if *value == BigInt::from(1) {
return Ok("promé".to_string());
}
Ok(format!("di {}", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}i", 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> {
let _ = precision_override;
self.cardinal_from_str(&python_str(value))
}
fn lang_name(&self) -> &str {
"Num2Word_PAP"
}
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 separator = separator.unwrap_or(self.default_separator());
let _ = adjective;
let is_negative = val.is_negative();
let (left, right) = match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value, .. } => {
let abs = value.abs();
let int_part = abs.with_scale(0);
let frac = &abs - &int_part;
let cents_part = (frac * BigDecimal::from(100)).with_scale(0);
(
int_part.as_bigint_and_exponent().0,
cents_part.as_bigint_and_exponent().0,
)
}
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.currency_fallback);
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let unit = if left.is_one() {
cr1.first()
} else {
cr1.get(1)
}
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
let mut result = format!("{} {}", self.int_to_word(&left), unit);
if cents && !right.is_zero() {
let subunit = if right.is_one() {
cr2.first()
} else {
cr2.get(1)
}
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(subunit);
}
if is_negative {
result.insert_str(0, NEGWORD);
}
Ok(result.trim().to_string())
}
}
fn python_str(value: &FloatValue) -> String {
match value {
FloatValue::Float { value, .. } => py_str_f64(*value),
FloatValue::Decimal { value, .. } => py_str_decimal(value),
}
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn cur(arg: &str, code: &str) -> String {
let is_int = !arg.contains('.');
let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
LangPap::new()
.to_currency(&v, code, true, None, false)
.unwrap()
}
fn cheque(arg: &str, code: &str) -> Result<String> {
LangPap::new().to_cheque(&BigDecimal::from_str(arg).unwrap(), code)
}
#[test]
fn corpus_currency() {
for (code, unit) in [
("EUR", "euro"),
("USD", "dolar"),
("GBP", "florin"),
("JPY", "florin"),
("KWD", "florin"),
("BHD", "florin"),
("INR", "florin"),
("CNY", "florin"),
("CHF", "florin"),
] {
assert_eq!(cur("0", code), format!("sero {unit}"));
assert_eq!(cur("1", code), format!("un {unit}"));
assert_eq!(cur("2", code), format!("dos {unit}"));
assert_eq!(cur("100", code), format!("shen {unit}"));
assert_eq!(cur("1000000", code), format!("un miyon {unit}"));
assert_eq!(cur("1.0", code), format!("un {unit}"));
assert_eq!(cur("0.01", code), format!("sero {unit} un sèn"));
assert_eq!(cur("0.5", code), format!("sero {unit} sinkuenta sèn"));
assert_eq!(
cur("12.34", code),
format!("diesdos {unit} trinta i kuater sèn")
);
assert_eq!(
cur("-12.34", code),
format!("menos diesdos {unit} trinta i kuater sèn")
);
assert_eq!(
cur("99.99", code),
format!("nobenta i nuebe {unit} nobenta i nuebe sèn")
);
assert_eq!(
cur("1234.56", code),
format!("mil dos shen i trinta i kuater {unit} sinkuenta i seis sèn")
);
}
}
#[test]
fn corpus_cheque() {
assert_eq!(
cheque("1234.56", "EUR").unwrap(),
"MIL DOS SHEN I TRINTA I KUATER AND 56/100 EURO"
);
assert_eq!(
cheque("1234.56", "USD").unwrap(),
"MIL DOS SHEN I TRINTA I KUATER AND 56/100 DOLAR"
);
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
match cheque("1234.56", code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{code}\" not implemented for \"Num2Word_PAP\"")
),
other => panic!("{code}: expected NotImplementedError, got {other:?}"),
}
}
}
#[test]
fn quirks_match_python() {
assert_eq!(cur("2.675", "EUR"), "dos euro sesenta i shete sèn");
assert_eq!(cur("1.005", "EUR"), "un euro");
assert_eq!(cur("12.999", "EUR"), "diesdos euro nobenta i nuebe sèn");
let d = CurrencyValue::Decimal {
value: BigDecimal::from_str("5.00").unwrap(),
has_decimal: true,
is_float: false,
};
assert_eq!(
LangPap::new()
.to_currency(&d, "EUR", true, None, false)
.unwrap(),
"sinku euro"
);
assert_eq!(cur("-0.001", "EUR"), "menos sero euro");
assert_eq!(cur("-1", "EUR"), "menos un euro");
assert_eq!(cur("1000000000", "EUR"), "1000000000 euro");
assert_eq!(cur("1", "AWG"), "un florin");
assert_eq!(cur("1", "ANG"), "un florin");
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
assert_eq!(
LangPap::new()
.to_currency(&v, "EUR", false, None, false)
.unwrap(),
"diesdos euro"
);
assert_eq!(
LangPap::new()
.to_currency(&v, "EUR", true, None, true)
.unwrap(),
"diesdos euro trinta i kuater sèn"
);
assert_eq!(
LangPap::new()
.to_currency(&v, "EUR", true, Some(","), false)
.unwrap(),
"diesdos euro,trinta i kuater sèn"
);
let l = LangPap::new();
let forms = vec!["florin".to_string(), "florins".to_string(), "x".to_string()];
assert_eq!(l.pluralize(&BigInt::one(), &forms).unwrap(), "florin");
assert_eq!(l.pluralize(&BigInt::from(2), &forms).unwrap(), "x");
assert_eq!(l.pluralize(&BigInt::from(2), &[]).unwrap(), "");
}
fn cf(arg: &str) -> String {
let v = FloatValue::Float {
value: arg.parse::<f64>().unwrap(),
precision: 0,
};
LangPap::new().to_cardinal_float(&v, None).unwrap()
}
fn cd(arg: &str) -> String {
let v = FloatValue::Decimal {
value: BigDecimal::from_str(arg).unwrap(),
precision: 0,
};
LangPap::new().to_cardinal_float(&v, None).unwrap()
}
#[test]
fn corpus_cardinal_float() {
assert_eq!(cf("0.0"), "sero punto sero");
assert_eq!(cf("0.5"), "sero punto sinku");
assert_eq!(cf("1.0"), "un punto sero");
assert_eq!(cf("1.5"), "un punto sinku");
assert_eq!(cf("2.25"), "dos punto dos sinku");
assert_eq!(cf("3.14"), "tres punto un kuater");
assert_eq!(cf("0.01"), "sero punto sero un");
assert_eq!(cf("0.1"), "sero punto un");
assert_eq!(cf("0.99"), "sero punto nuebe nuebe");
assert_eq!(cf("1.01"), "un punto sero un");
assert_eq!(cf("12.34"), "diesdos punto tres kuater");
assert_eq!(cf("99.99"), "nobenta i nuebe punto nuebe nuebe");
assert_eq!(cf("100.5"), "shen punto sinku");
assert_eq!(
cf("1234.56"),
"mil dos shen i trinta i kuater punto sinku seis"
);
assert_eq!(cf("-0.5"), "menos sero punto sinku");
assert_eq!(cf("-1.5"), "menos un punto sinku");
assert_eq!(cf("-12.34"), "menos diesdos punto tres kuater");
assert_eq!(cf("1.005"), "un punto sero sero sinku");
assert_eq!(cf("2.675"), "dos punto seis shete sinku");
}
#[test]
fn corpus_cardinal_dec() {
assert_eq!(cd("0.01"), "sero punto sero un");
assert_eq!(cd("1.10"), "un punto un sero");
assert_eq!(cd("12.345"), "diesdos punto tres kuater sinku");
assert_eq!(cd("98746251323029.99"), "98746251323029 punto nuebe nuebe");
assert_eq!(cd("0.001"), "sero punto sero sero un");
}
#[test]
fn float_edges_match_python() {
assert_eq!(cf("-0.0"), "menos sero punto sero");
for (arg, tok) in [("1e16", "1e+16"), ("1e21", "1e+21"), ("1e-05", "1e-05")] {
let v = FloatValue::Float {
value: arg.parse::<f64>().unwrap(),
precision: 0,
};
match LangPap::new().to_cardinal_float(&v, None) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", tok)
),
other => panic!("{arg}: expected ValueError, got {other:?}"),
}
}
let v = FloatValue::Decimal {
value: BigDecimal::from_str("1.23E+4").unwrap(),
precision: 0,
};
match LangPap::new().to_cardinal_float(&v, None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: 'E'")
}
other => panic!("1.23E+4: expected ValueError, got {other:?}"),
}
let v = FloatValue::Float {
value: 0.5,
precision: 1,
};
assert_eq!(
LangPap::new().to_cardinal_float(&v, Some(3)).unwrap(),
"sero punto sinku"
);
}
}