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_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const ZERO: &str = "zero";
const NEGWORD: &str = "minus ";
const ONES: [&str; 10] = [
"",
"איינס",
"צוויי",
"דרײַ",
"פיר",
"פינף",
"זעקס",
"זיבן",
"אַכט",
"נײַן",
];
const TENS: [&str; 10] = [
"",
"צען",
"צוואַנציק",
"דרײַסיק",
"פערציק",
"פופציק",
"זעכציק",
"זיבעציק",
"אַכציק",
"נײַנציק",
];
const HUNDRED: &str = "הונדערט";
const THOUSAND: &str = "טויזנט";
const MILLION: &str = "מיליאָן";
const ORDINAL_SUFFIX: &str = "-טער";
const FALLBACK_LIMIT: u64 = 1_000_000_000;
const LANG_NAME: &str = "Num2Word_YI";
const EURO: &str = "אייראָ";
const EURO_CENT: &str = "צענט";
const FALLBACK_CODE: &str = "EUR";
const DEFAULT_SEPARATOR: &str = " ";
const BASE_SEPARATOR_DEFAULT: &str = ",";
fn separator_for(passed: &str) -> &str {
if passed == BASE_SEPARATOR_DEFAULT {
DEFAULT_SEPARATOR
} else {
passed
}
}
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)
}
pub struct LangYi {
forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl LangYi {
pub fn new() -> Self {
let mut forms = HashMap::new();
forms.insert("EUR", CurrencyForms::new(&[EURO, EURO], &[EURO_CENT, EURO_CENT]));
forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
let fallback_forms = forms[FALLBACK_CODE].clone();
LangYi { 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()));
}
if number >= &BigInt::from(FALLBACK_LIMIT) {
return number.to_string();
}
let n = number
.to_u64()
.expect("0 < number < 10**9 always fits in u64");
self.int_to_word_small(n)
}
fn int_to_word_small(&self, n: u64) -> String {
if n == 0 {
return ZERO.to_string();
}
if n < 10 {
ONES[n as usize].to_string()
} else if n < 100 {
let tens_val = (n / 10) as usize;
let ones_val = (n % 10) as usize;
if ones_val == 0 {
TENS[tens_val].to_string()
} else {
format!("{} {}", TENS[tens_val], ONES[ones_val])
}
} else if n < 1000 {
let hundreds_val = (n / 100) as usize;
let remainder = n % 100;
let mut result = format!("{} {}", ONES[hundreds_val], HUNDRED);
if remainder != 0 {
result.push(' ');
result.push_str(&self.int_to_word_small(remainder));
}
result
} else if n < 1_000_000 {
let thousands_val = n / 1000;
let remainder = n % 1000;
let mut result = format!("{} {}", self.int_to_word_small(thousands_val), THOUSAND);
if remainder != 0 {
result.push(' ');
result.push_str(&self.int_to_word_small(remainder));
}
result
} else {
let millions_val = n / 1_000_000;
let remainder = n % 1_000_000;
let mut result = format!("{} {}", self.int_to_word_small(millions_val), MILLION);
if remainder != 0 {
result.push(' ');
result.push_str(&self.int_to_word_small(remainder));
}
result
}
}
fn cardinal_from_repr(&self, n: &str) -> Result<String> {
let n = n.trim();
let (n, ret) = match n.strip_prefix('-') {
Some(rest) => (rest, NEGWORD),
None => (n, ""),
};
match n.split_once('.') {
Some((left, right)) => {
let mut out = String::from(ret);
out.push_str(&self.int_to_word(&py_int(left)?));
out.push(' ');
out.push_str(self.pointword());
out.push(' ');
for digit in right.chars() {
out.push_str(&self.int_to_word(&BigInt::from(py_int_digit(digit)?)));
out.push(' ');
}
Ok(out.trim().to_string())
}
None => Ok(format!("{}{}", ret, self.int_to_word(&py_int(n)?))
.trim()
.to_string()),
}
}
}
impl Default for LangYi {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangYi {
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> {
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> {
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 {
"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, magnitude) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, self.int_to_word(&magnitude))
.trim()
.to_string())
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let n = match value {
FloatValue::Float { value, .. } => py_str_f64(*value),
FloatValue::Decimal { value, .. } => py_str_decimal(value),
};
self.cardinal_from_repr(&n)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
let cardinal = self.to_cardinal(value)?;
Ok(format!("{}{}", cardinal, 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 lang_name(&self) -> &str {
LANG_NAME
}
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 is_negative = val.is_negative();
let (left, right) = match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => {
let v = d.abs();
let integer = v.with_scale(0);
let frac = &v - &integer;
let right = (frac * BigDecimal::from(100)).with_scale(0);
(
integer.as_bigint_and_exponent().0,
right.as_bigint_and_exponent().0,
)
}
};
let forms = self.forms.get(currency).unwrap_or(&self.fallback_forms);
let one = BigInt::one();
let mut result = format!(
"{} {}",
self.int_to_word(&left),
if left == one { &forms.unit[0] } else { &forms.unit[1] }
);
if cents && !right.is_zero() {
result.push_str(separator_for(separator));
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(if right == one {
&forms.subunit[0]
} else {
&forms.subunit[1]
});
}
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> {
LangYi::new().to_cardinal_float(&FloatValue::Float { value: v, precision }, None)
}
fn d(s: &str, precision: u32) -> Result<String> {
LangYi::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(), "zero point zero");
assert_eq!(f(0.5, 1).unwrap(), "zero point פינף");
assert_eq!(f(1.0, 1).unwrap(), "איינס point zero");
assert_eq!(f(1.5, 1).unwrap(), "איינס point פינף");
assert_eq!(f(2.25, 2).unwrap(), "צוויי point צוויי פינף");
assert_eq!(f(3.14, 2).unwrap(), "דרײַ point איינס פיר");
assert_eq!(f(0.01, 2).unwrap(), "zero point zero איינס");
assert_eq!(f(0.1, 1).unwrap(), "zero point איינס");
assert_eq!(f(0.99, 2).unwrap(), "zero point נײַן נײַן");
assert_eq!(f(1.01, 2).unwrap(), "איינס point zero איינס");
assert_eq!(f(12.34, 2).unwrap(), "צען צוויי point דרײַ פיר");
assert_eq!(f(99.99, 2).unwrap(), "נײַנציק נײַן point נײַן נײַן");
assert_eq!(f(100.5, 1).unwrap(), "איינס הונדערט point פינף");
assert_eq!(
f(1234.56, 2).unwrap(),
"איינס טויזנט צוויי הונדערט דרײַסיק פיר point פינף זעקס"
);
assert_eq!(f(-0.5, 1).unwrap(), "minus zero point פינף");
assert_eq!(f(-1.5, 1).unwrap(), "minus איינס point פינף");
assert_eq!(f(-12.34, 2).unwrap(), "minus צען צוויי point דרײַ פיר");
assert_eq!(f(1.005, 3).unwrap(), "איינס point zero zero פינף");
assert_eq!(f(2.675, 3).unwrap(), "צוויי point זעקס זיבן פינף");
}
#[test]
fn corpus_decimal_rows() {
assert_eq!(d("0.01", 2).unwrap(), "zero point zero איינס");
assert_eq!(d("1.10", 2).unwrap(), "איינס point איינס zero");
assert_eq!(d("12.345", 3).unwrap(), "צען צוויי point דרײַ פיר פינף");
assert_eq!(
d("98746251323029.99", 2).unwrap(),
"98746251323029 point נײַן נײַן"
);
assert_eq!(d("0.001", 3).unwrap(), "zero point zero zero איינס");
}
#[test]
fn live_interpreter_rows() {
assert_eq!(
f(1234567.89, 2).unwrap(),
"איינס מיליאָן צוויי הונדערט דרײַסיק פיר טויזנט פינף הונדערט זעכציק זיבן point אַכט נײַן"
);
assert_eq!(f(1500000000.25, 2).unwrap(), "1500000000 point צוויי פינף");
assert_eq!(f(-0.0, 1).unwrap(), "minus zero point zero");
assert_eq!(d("-12.34", 2).unwrap(), "minus צען צוויי point דרײַ פיר");
}
#[test]
fn f64_artefacts_come_from_repr_not_float2tuple() {
assert_eq!(f(1.005, 3).unwrap(), "איינס point zero zero פינף");
assert_eq!(f(2.675, 3).unwrap(), "צוויי point זעקס זיבן פינף");
let expected = format!("zero point דרײַ {} פיר", ["zero"; 15].join(" "));
assert_eq!(f(0.1 + 0.2, 17).unwrap(), expected);
}
#[test]
fn precision_override_is_inert() {
let l = LangYi::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(),
"צוויי point זעקס זיבן פינף"
);
}
}
#[test]
fn exponent_form_repr_raises_value_error() {
assert_eq!(f(1e15, 1).unwrap(), "1000000000000000 point zero");
assert_eq!(
value_err(f(1e16, 0)),
"invalid literal for int() with base 10: '1e+16'"
);
assert_eq!(
value_err(f(-1e16, 0)),
"invalid literal for int() with base 10: '1e+16'"
);
assert_eq!(
value_err(f(1e-5, 0)),
"invalid literal for int() with base 10: '1e-05'"
);
assert_eq!(
value_err(f(1.5e20, 0)),
"invalid literal for int() with base 10: 'e'"
);
assert_eq!(
value_err(d("1E-7", 7)),
"invalid literal for int() with base 10: '1E-7'"
);
}
}