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 NEGWORD: &str = "minus ";
const POINTWORD: &str = "tuqaa";
const ONES: [&str; 10] = [
"", "tokko", "lama", "sadii", "afur", "shan", "ja'a", "torba", "saddeet", "sagal",
];
const TENS: [&str; 10] = [
"",
"kudhan",
"digdama",
"soddoma",
"afurtama",
"shantama",
"jaatama",
"torbaatama",
"saddeettama",
"sagaltama",
];
const HUNDRED: &str = "dhibba";
const THOUSAND: &str = "kuma";
const MILLION: &str = "miliyoona";
const ZEROWORD: &str = "zeeroo";
const AND: &str = " fi ";
const CURRENCY_FORMS: [(&str, [&str; 2], [&str; 2]); 3] = [
("ETB", ["birrii", "birrii"], ["saantima", "saantima"]),
("USD", ["doolaara", "doolaara"], ["saantima", "saantima"]),
("EUR", ["yuuroo", "yuuroo"], ["saantima", "saantima"]),
];
fn split_currency(val: &CurrencyValue) -> Result<(BigInt, BigInt)> {
let d = match val {
CurrencyValue::Int(v) => return Ok((v.abs(), BigInt::zero())),
CurrencyValue::Decimal { value, .. } => value.abs(),
};
let (digits, scale) = d.as_bigint_and_exponent();
if scale < 0 {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
d
)));
}
if scale == 0 {
return Ok((digits, BigInt::zero()));
}
let s = digits.to_string();
let scale = scale as usize;
let (int_part, frac_part) = if s.len() > scale {
let (a, b) = s.split_at(s.len() - scale);
(a.to_string(), b.to_string())
} else {
("0".to_string(), format!("{:0>width$}", s, width = scale))
};
let left = int_part.parse::<BigInt>().unwrap_or_else(|_| BigInt::zero());
let head: String = frac_part.chars().take(2).collect();
let right = format!("{:0<2}", head)
.parse::<BigInt>()
.unwrap_or_else(|_| BigInt::zero());
Ok((left, right))
}
fn py_float_str(f: f64) -> String {
let neg = f.is_sign_negative();
let sci = format!("{:e}", f.abs()); 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");
let (int_part, frac_part) = mant.split_once('.').unwrap_or((mant, ""));
let digits = BigInt::from_str(&format!("{}{}", int_part, frac_part))
.expect("mantissa digits are ASCII 0-9");
let scale = frac_part.len() as i64 - exp;
let body = py_str(&digits, scale);
if neg {
format!("-{}", body)
} else {
body
}
}
fn py_str(digits: &BigInt, scale: i64) -> String {
debug_assert!(!digits.is_negative(), "py_str expects abs(value)'s digits");
let ds = digits.to_string();
let decpt = ds.len() as i64 - scale;
if decpt > 16 || decpt < -3 {
let mantissa = if ds.len() > 1 {
format!("{}.{}", &ds[..1], &ds[1..])
} else {
ds
};
let exp = decpt - 1;
let sign = if exp < 0 { '-' } else { '+' };
return format!("{}e{}{:02}", mantissa, sign, exp.abs());
}
if decpt <= 0 {
format!("0.{}{}", "0".repeat((-decpt) as usize), ds)
} else if decpt as usize >= ds.len() {
format!("{}{}.0", ds, "0".repeat(decpt as usize - ds.len()))
} else {
format!("{}.{}", &ds[..decpt as usize], &ds[decpt as usize..])
}
}
fn py_decimal_str(value: &BigDecimal) -> String {
let neg = value.is_negative();
let (coeff, scale) = value.abs().as_bigint_and_exponent();
let int_str = coeff.to_string(); let ndigits = int_str.len() as i64;
let exp = -scale; let leftdigits = exp + ndigits;
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_str),
)
} else if dotplace >= ndigits {
(
format!("{}{}", int_str, "0".repeat((dotplace - ndigits) as usize)),
String::new(),
)
} else {
let dp = dotplace as usize;
(int_str[..dp].to_string(), format!(".{}", &int_str[dp..]))
};
let exp_part = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
let body = format!("{}{}{}", intpart, fracpart, exp_part);
if neg {
format!("-{}", body)
} else {
body
}
}
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 ones_or_zeeroo(ch: char) -> Result<&'static str> {
let d = py_int(&ch.to_string())?;
let idx = d
.to_usize()
.expect("a single decimal digit is 0..=9, always a valid usize");
Ok(if ONES[idx].is_empty() {
ZEROWORD
} else {
ONES[idx]
})
}
pub struct LangOm {
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl Default for LangOm {
fn default() -> Self {
Self::new()
}
}
impl LangOm {
pub fn new() -> Self {
let (_, fb_unit, fb_subunit) = CURRENCY_FORMS[0];
LangOm {
exclude_title: vec!["fi".to_string(), "tuqaa".to_string(), "minus".to_string()],
currency_forms: CURRENCY_FORMS
.iter()
.map(|(code, unit, subunit)| (*code, CurrencyForms::new(unit, subunit)))
.collect(),
fallback_forms: CurrencyForms::new(&fb_unit, &fb_subunit),
}
}
fn digit(n: &BigInt) -> usize {
n.to_usize().unwrap_or(0)
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZEROWORD.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[Self::digit(number)].to_string();
}
if number < &hundred {
let (t, o) = number.div_rem(&ten);
let mut out = TENS[Self::digit(&t)].to_string();
if !o.is_zero() {
out.push_str(AND);
out.push_str(ONES[Self::digit(&o)]);
}
return out;
}
if number < &thousand {
let (h, r) = number.div_rem(&hundred);
let mut out = String::new();
if h > BigInt::one() {
out.push_str(ONES[Self::digit(&h)]);
out.push(' ');
}
out.push_str(HUNDRED);
if !r.is_zero() {
out.push_str(AND);
out.push_str(&self.int_to_word(&r));
}
return out;
}
if number < &million {
let (t, r) = number.div_rem(&thousand);
let mut out = String::new();
if t > BigInt::one() {
out.push_str(&self.int_to_word(&t));
out.push(' ');
}
out.push_str(THOUSAND);
if !r.is_zero() {
out.push_str(AND);
out.push_str(&self.int_to_word(&r));
}
return out;
}
if number < &billion {
let (m, r) = number.div_rem(&million);
let mut out = String::new();
if m > BigInt::one() {
out.push_str(&self.int_to_word(&m));
out.push(' ');
}
out.push_str(MILLION);
if !r.is_zero() {
out.push_str(AND);
out.push_str(&self.int_to_word(&r));
}
return out;
}
number.to_string()
}
fn cardinal_from_pystr(&self, n: &str) -> Result<String> {
let n = n.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = self.cardinal_from_pystr(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
match n.split_once('.') {
Some((left, right)) => {
let left_int = py_int(left)?;
let mut ret = format!("{} {}", self.int_to_word(&left_int), POINTWORD);
for ch in right.chars() {
ret.push(' ');
ret.push_str(ones_or_zeeroo(ch)?);
}
Ok(ret.trim().to_string())
}
None => Ok(self.int_to_word(&py_int(n)?)),
}
}
}
impl Lang for LangOm {
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("tokkoffaa".to_string());
}
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(format!("{}ffaa", cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}ffaa", 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 {
"ETB"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"tuqaa"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = self.int_to_word(&value.abs());
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(self.int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value.is_one() {
return Ok("tokkoffaa".to_string());
}
Ok(format!("{}ffaa", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}ffaa", 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 n = match value {
FloatValue::Float { value, .. } => py_float_str(*value),
FloatValue::Decimal { value, .. } => py_decimal_str(value),
};
self.cardinal_from_pystr(&n)
}
fn lang_name(&self) -> &str {
"Num2Word_OM"
}
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 is_negative = val.is_negative();
let (left, right) = split_currency(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(&format!(
"{}{} {}",
separator,
self.int_to_word(&right),
subunit
));
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod float_tests {
use super::*;
fn card_f(f: f64) -> Result<String> {
let precision = match format!("{}", f).split_once('.') {
Some((_, frac)) if !frac.contains('e') => frac.len() as u32,
_ => 0,
};
LangOm::new().to_cardinal_float(&FloatValue::Float { value: f, precision }, None)
}
fn card_d(s: &str) -> Result<String> {
let v = FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
};
LangOm::new().to_cardinal_float(&v, None)
}
#[test]
fn bigdecimal_does_not_normalise() {
for (s, want_digits, want_scale) in [
("1.10", "110", 2i64),
("5.00", "500", 2),
("0.01", "1", 2),
("0.001", "1", 3),
("98746251323029.99", "9874625132302999", 2),
("1E+16", "1", -16),
("1E-7", "1", 7),
] {
let (d, sc) = BigDecimal::from_str(s).unwrap().as_bigint_and_exponent();
assert_eq!(
(d.to_string().as_str(), sc),
(want_digits, want_scale),
"{s}"
);
}
}
#[test]
fn py_str_round_trips_repr() {
for s in [
"0.5", "0.01", "1.0", "12.34", "99.99", "1234.56", "100.0", "0.0001",
"1000000000000000.0", "1e+16", "1e+21", "1e-05", "0.0",
] {
let (d, sc) = BigDecimal::from_str(s).unwrap().as_bigint_and_exponent();
assert_eq!(py_str(&d, sc), s);
}
}
#[test]
fn to_cardinal_float_matches_corpus() {
for (f, want) in [
(0.0, "zeeroo tuqaa zeeroo"),
(0.5, "zeeroo tuqaa shan"),
(1.0, "tokko tuqaa zeeroo"),
(1.5, "tokko tuqaa shan"),
(2.25, "lama tuqaa lama shan"),
(3.14, "sadii tuqaa tokko afur"),
(0.01, "zeeroo tuqaa zeeroo tokko"),
(0.1, "zeeroo tuqaa tokko"),
(0.99, "zeeroo tuqaa sagal sagal"),
(1.01, "tokko tuqaa zeeroo tokko"),
(12.34, "kudhan fi lama tuqaa sadii afur"),
(99.99, "sagaltama fi sagal tuqaa sagal sagal"),
(100.5, "dhibba tuqaa shan"),
(
1234.56,
"kuma fi lama dhibba fi soddoma fi afur tuqaa shan ja'a",
),
(-0.5, "minus zeeroo tuqaa shan"),
(-1.5, "minus tokko tuqaa shan"),
(-12.34, "minus kudhan fi lama tuqaa sadii afur"),
(1.005, "tokko tuqaa zeeroo zeeroo shan"),
(2.675, "lama tuqaa ja'a torba shan"),
] {
assert_eq!(card_f(f).unwrap(), want, "{f}");
}
}
#[test]
fn to_cardinal_float_negative_zero() {
assert_eq!(card_f(-0.0).unwrap(), "minus zeeroo tuqaa zeeroo");
}
#[test]
fn to_cardinal_float_exponential_raises() {
for f in [1e16, 1e21, 1e-5, 1.5e20, 1.25e20] {
match card_f(f) {
Err(N2WError::Value(_)) => {}
other => panic!("{f}: expected ValueError, got {other:?}"),
}
}
}
#[test]
fn to_cardinal_decimal_matches_corpus() {
for (s, want) in [
("0.01", "zeeroo tuqaa zeeroo tokko"),
("1.10", "tokko tuqaa tokko zeeroo"),
("12.345", "kudhan fi lama tuqaa sadii afur shan"),
("98746251323029.99", "98746251323029 tuqaa sagal sagal"),
("0.001", "zeeroo tuqaa zeeroo zeeroo tokko"),
("5", "shan"),
("5.00", "shan tuqaa zeeroo zeeroo"),
("0.0", "zeeroo tuqaa zeeroo"),
("-0.5", "minus zeeroo tuqaa shan"),
("-12.34", "minus kudhan fi lama tuqaa sadii afur"),
] {
assert_eq!(card_d(s).unwrap(), want, "{s}");
}
}
#[test]
fn to_cardinal_decimal_exponential_raises() {
for s in ["1E+16", "1E-7"] {
match card_d(s) {
Err(N2WError::Value(_)) => {}
other => panic!("{s}: expected ValueError, got {other:?}"),
}
}
}
}