use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const ZERO: &str = "zero";
const NEGWORD: &str = "minus ";
const ONES: [&str; 10] = [
"", "eent", "zwou", "dräi", "véier", "fënnef", "sechs", "siwen", "aacht", "néng",
];
const TENS: [&str; 10] = [
"",
"zéng",
"zwanzeg",
"drësseg",
"véierzeg",
"fofzeg",
"sechzeg",
"siwwenzeg",
"achtzeg",
"nongzeg",
];
const HUNDRED: &str = "honnert";
const THOUSAND: &str = "dausend";
const MILLION: &str = "Millioun";
const ORDINAL_SUFFIX: &str = "-ten";
const LANG_NAME: &str = "Num2Word_LB";
const FALLBACK_CODE: &str = "EUR";
const SEPARATOR_UNSET: &str = ",";
const SEPARATOR_DEFAULT: &str = " ";
fn idx(n: &BigInt) -> usize {
debug_assert!(
!n.is_negative() && n <= &BigInt::from(9),
"digit out of range"
);
n.to_usize()
.expect("digit index is bounded by construction")
}
fn split_str_parts(val: &CurrencyValue) -> (BigInt, BigInt) {
match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => {
let (mantissa, scale) = d.abs().as_bigint_and_exponent();
if scale <= 0 {
let pow = BigInt::from(10).pow((-scale) as u32);
return (mantissa * pow, BigInt::zero());
}
let pow = BigInt::from(10).pow(scale as u32);
let (left, frac) = mantissa.div_rem(&pow);
let right = if scale >= 2 {
frac / BigInt::from(10).pow((scale - 2) as u32)
} else {
frac * BigInt::from(10)
};
(left, right)
}
}
}
fn int_value_error(s: &str) -> N2WError {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
}
fn py_float_sci_repr(a: f64) -> String {
let sci = format!("{:e}", a); let (mant, exp_str) = sci.split_once('e').expect("{:e} always emits an 'e'");
let exp: i64 = exp_str.parse().expect("{:e} exponent is a base-10 integer");
format!(
"{}e{}{:02}",
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
)
}
enum StrRoute {
Point,
WholeDigits(BigInt),
}
fn route_by_str(v: &FloatValue) -> Result<StrRoute> {
match v {
FloatValue::Float { value, .. } => {
let a = value.abs();
if !a.is_finite() {
return Err(int_value_error(if a.is_nan() { "nan" } else { "inf" }));
}
if a >= 1e16 || (a > 0.0 && a < 1e-4) {
return Err(int_value_error(&py_float_sci_repr(a)));
}
Ok(StrRoute::Point)
}
FloatValue::Decimal { value, .. } => {
let s = python_decimal_str(&value.abs());
if s.contains('.') {
Ok(StrRoute::Point)
} else if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {
Ok(StrRoute::WholeDigits(s.parse().expect("digits only")))
} else {
Err(int_value_error(&s))
}
}
}
}
pub struct LangLb {
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl Default for LangLb {
fn default() -> Self {
Self::new()
}
}
impl LangLb {
pub fn new() -> Self {
let mut currency_forms = HashMap::with_capacity(2);
currency_forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euro"], &["cent", "cents"]),
);
currency_forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
let fallback_forms = currency_forms
.get(FALLBACK_CODE)
.cloned()
.expect("FALLBACK_CODE was just inserted above");
LangLb {
currency_forms,
fallback_forms,
}
}
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(1_000);
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 (tens_val, ones_val) = number.div_mod_floor(&ten);
if ones_val.is_zero() {
return TENS[idx(&tens_val)].to_string();
}
return format!("{} {}", TENS[idx(&tens_val)], ONES[idx(&ones_val)]);
}
if number < &thousand {
let (hundreds_val, remainder) = number.div_mod_floor(&hundred);
let mut result = format!("{} {}", ONES[idx(&hundreds_val)], HUNDRED);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
return result;
}
if number < &million {
let (thousands_val, remainder) = number.div_mod_floor(&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));
}
return result;
}
if number < &billion {
let (millions_val, remainder) = number.div_mod_floor(&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));
}
return result;
}
number.to_string()
}
}
impl Lang for LangLb {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"point"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let ret = if value.is_negative() { NEGWORD } else { "" };
Ok(format!("{}{}", ret, self.int_to_word(&value.abs()))
.trim()
.to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", self.to_cardinal(value)?, ORDINAL_SUFFIX))
}
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> {
let precision = value.precision() as usize;
let (negative, left, frac): (bool, BigInt, String) = match value {
FloatValue::Float { value, .. } => {
let negative = value.is_sign_negative();
let s = format!("{:.*}", precision, value.abs());
let (left_str, frac) = match s.split_once('.') {
Some((l, f)) => (l, f.to_string()),
None => (s.as_str(), String::new()),
};
let left = left_str.parse::<BigInt>().map_err(|_| {
N2WError::Value(format!("non-numeric integer part {:?}", left_str))
})?;
(negative, left, frac)
}
FloatValue::Decimal { value: d, .. } => {
let negative = d.is_negative();
let (pre, post) = float2tuple(value);
let post_str = post.to_string();
let frac = format!(
"{}{}",
"0".repeat(precision.saturating_sub(post_str.len())),
post_str
);
(negative, pre.abs(), frac)
}
};
let mut ret = String::new();
if negative {
ret.push_str(NEGWORD);
}
ret.push_str(&self.int_to_word(&left));
if precision > 0 {
ret.push(' ');
ret.push_str(self.pointword());
ret.push(' ');
for ch in frac.chars().take(precision) {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("non-digit {:?} in fractional part", ch))
})?;
ret.push_str(&self.int_to_word(&BigInt::from(d)));
ret.push(' ');
}
}
Ok(ret.trim().to_string())
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
match route_by_str(value)? {
StrRoute::Point => self.to_cardinal_float(value, precision_override),
StrRoute::WholeDigits(abs_int) => {
let ret = if value.is_negative() { NEGWORD } else { "" };
Ok(format!("{}{}", ret, self.int_to_word(&abs_int))
.trim()
.to_string())
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!(
"{}{}",
self.cardinal_float_entry(value, None)?,
ORDINAL_SUFFIX
))
}
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<ParsedNumber> {
python_decimal_parse(s)
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok(format!("{}Infinity.", if negative { "-" } else { "" })),
_ => Err(int_value_error("Infinity")),
}
}
fn nan_result(&self, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok("NaN.".to_string()),
_ => Err(int_value_error("NaN")),
}
}
fn lang_name(&self) -> &str {
LANG_NAME
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_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 == SEPARATOR_UNSET {
SEPARATOR_DEFAULT
} else {
separator
};
let is_negative = val.is_negative();
let (left, right) = split_str_parts(val);
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.fallback_forms);
let one = BigInt::one();
let unit = if left != one {
&forms.unit[1]
} else {
&forms.unit[0]
};
let mut result = format!("{} {}", self.int_to_word(&left), unit);
if cents && !right.is_zero() {
let subunit = if right != one {
&forms.subunit[1]
} else {
&forms.subunit[0]
};
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(subunit);
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod entry_routing_tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn fv(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn dv(s: &str) -> FloatValue {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.max(0) as u32;
FloatValue::Decimal { value, precision }
}
#[test]
fn corpus_cardinal_entry() {
let lb = LangLb::new();
assert_eq!(
lb.cardinal_float_entry(&fv(5.0, 1), None).unwrap(),
"fënnef point zero"
);
assert_eq!(
lb.cardinal_float_entry(&fv(0.0, 1), None).unwrap(),
"zero point zero"
);
assert_eq!(
lb.cardinal_float_entry(&fv(-1000000.0, 1), None).unwrap(),
"minus eent Millioun point zero"
);
assert_eq!(
lb.cardinal_float_entry(&fv(-0.0, 1), None).unwrap(),
"minus zero point zero"
);
assert_eq!(
lb.cardinal_float_entry(&fv(1e9, 1), None).unwrap(),
"1000000000 point zero"
);
assert_eq!(
lb.cardinal_float_entry(&dv("5.00"), None).unwrap(),
"fënnef point zero zero"
);
assert_eq!(
lb.cardinal_float_entry(&dv("12345.000"), None).unwrap(),
"zéng zwou dausend dräi honnert véierzeg fënnef point zero zero zero"
);
assert_eq!(lb.cardinal_float_entry(&dv("0"), None).unwrap(), "zero");
assert_eq!(
lb.cardinal_float_entry(&dv("100"), None).unwrap(),
"eent honnert"
);
}
#[test]
fn corpus_cardinal_entry_scientific_is_valueerror() {
let lb = LangLb::new();
for (v, repr) in [(1e16, "1e+16"), (1e20, "1e+20"), (1e-5, "1e-05")] {
match lb.cardinal_float_entry(&fv(v, 0), None) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", repr)
),
other => panic!("{} expected ValueError, got {:?}", v, other),
}
}
match lb.cardinal_float_entry(&fv(-1e16, 0), None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1e+16'")
}
other => panic!("expected ValueError, got {:?}", other),
}
for (s, repr) in [("1E+2", "1E+2"), ("1E+20", "1E+20"), ("1E+3", "1E+3")] {
match lb.cardinal_float_entry(&dv(s), None) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", repr)
),
other => panic!("{} expected ValueError, got {:?}", s, other),
}
}
}
#[test]
fn corpus_ordinal_entry() {
let lb = LangLb::new();
assert_eq!(
lb.ordinal_float_entry(&fv(5.0, 1)).unwrap(),
"fënnef point zero-ten"
);
assert_eq!(
lb.ordinal_float_entry(&fv(-0.0, 1)).unwrap(),
"minus zero point zero-ten"
);
assert_eq!(
lb.ordinal_float_entry(&fv(3.25, 2)).unwrap(),
"dräi point zwou fënnef-ten"
);
assert_eq!(lb.ordinal_float_entry(&dv("0")).unwrap(), "zero-ten");
assert_eq!(
lb.ordinal_float_entry(&dv("5.00")).unwrap(),
"fënnef point zero zero-ten"
);
assert!(matches!(
lb.ordinal_float_entry(&fv(1e16, 0)),
Err(N2WError::Value(_))
));
}
#[test]
fn corpus_ordinal_num_entry() {
let lb = LangLb::new();
assert_eq!(
lb.ordinal_num_float_entry(&fv(5.0, 1), "5.0").unwrap(),
"5.0."
);
assert_eq!(
lb.ordinal_num_float_entry(&fv(-0.0, 1), "-0.0").unwrap(),
"-0.0."
);
assert_eq!(
lb.ordinal_num_float_entry(&fv(1e16, 0), "1e+16").unwrap(),
"1e+16."
);
assert_eq!(
lb.ordinal_num_float_entry(&dv("1E+2"), "1E+2").unwrap(),
"1E+2."
);
assert_eq!(
lb.ordinal_num_float_entry(&dv("5.00"), "5.00").unwrap(),
"5.00."
);
}
#[test]
fn corpus_year_entry() {
let lb = LangLb::new();
assert_eq!(
lb.year_float_entry(&fv(5.0, 1)).unwrap(),
"fënnef point zero"
);
assert_eq!(
lb.year_float_entry(&fv(-0.0, 1)).unwrap(),
"minus zero point zero"
);
assert!(matches!(
lb.year_float_entry(&fv(1e20, 0)),
Err(N2WError::Value(_))
));
}
#[test]
fn str_to_number_inf_nan_native() {
let lb = LangLb::new();
assert!(matches!(
lb.str_to_number("Infinity"),
Ok(ParsedNumber::Inf { negative: false })
));
assert!(matches!(
lb.str_to_number("-Infinity"),
Ok(ParsedNumber::Inf { negative: true })
));
assert!(matches!(lb.str_to_number("NaN"), Ok(ParsedNumber::NaN)));
for to in ["cardinal", "ordinal", "year"] {
assert!(matches!(lb.inf_result(false, to), Err(N2WError::Value(_))));
assert!(matches!(lb.inf_result(true, to), Err(N2WError::Value(_))));
assert!(matches!(lb.nan_result(to), Err(N2WError::Value(_))));
}
assert_eq!(lb.inf_result(false, "ordinal_num").unwrap(), "Infinity.");
assert_eq!(lb.inf_result(true, "ordinal_num").unwrap(), "-Infinity.");
assert_eq!(lb.nan_result("ordinal_num").unwrap(), "NaN.");
assert!(matches!(lb.str_to_number("5"), Ok(ParsedNumber::Dec(_))));
assert!(matches!(lb.str_to_number("1e3"), Ok(ParsedNumber::Dec(_))));
}
}