use crate::base::{Lang, N2WError, Result};
use crate::currency::{parse_currency_parts, CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const ONES: [&str; 10] = [
"", "üks", "kaks", "kolm", "neli", "viis", "kuus", "seitse", "kaheksa", "üheksa",
];
const TENS: [&str; 10] = [
"",
"kümme",
"kakskümmend",
"kolmkümmend",
"nelikümmend",
"viiskümmend",
"kuuskümmend",
"seitsekümmend",
"kaheksakümmend",
"üheksakümmend",
];
const ORDINALS_ONES: [&str; 10] = [
"", "esimene", "teine", "kolmas", "neljas", "viies", "kuues", "seitsmes", "kaheksas", "üheksas",
];
const ORDINALS_TENS: [&str; 10] = [
"",
"kümnes",
"kahekümnes",
"kolmekümnes",
"nelikümnes",
"viiekümnes",
"kuuekümnes",
"seitsmekümnes",
"kaheksakümnes",
"üheksakümnes",
];
const TEENS: [&str; 9] = [
"üksteist",
"kaksteist",
"kolmteist",
"neliteist",
"viisteist",
"kuusteist",
"seitseteist",
"kaheksateist",
"üheksateist",
];
const TEENS_ORDINALS: [&str; 9] = [
"üheteistkümnes",
"kaheteistkümnes",
"kolmeteistkümnes",
"neljateistkümnes",
"viieteistkümnes",
"kuueteistkümnes",
"seitsmeteistkümnes",
"kaheksateistkümnes",
"üheksateistkümnes",
];
const HUNDREDS: [&str; 10] = [
"",
"ükssada",
"kakssada",
"kolmsada",
"nelisada",
"viissada",
"kuussada",
"seitsesada",
"kaheksasada",
"üheksasada",
];
const NEGWORD: &str = "miinus ";
fn big(n: i64) -> BigInt {
BigInt::from(n)
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const SENT: [&str; 2] = ["sent", "senti"];
const KROON: [&str; 2] = ["kroon", "krooni"];
const OORI: [&str; 2] = ["ööri", "ööri"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("EUR", CurrencyForms::new(&["euro", "eurot"], &SENT));
m.insert("USD", CurrencyForms::new(&["dollar", "dollarit"], &SENT));
m.insert(
"GBP",
CurrencyForms::new(&["nael", "naela"], &["penn", "penni"]),
);
m.insert("SEK", CurrencyForms::new(&KROON, &OORI));
m.insert("NOK", CurrencyForms::new(&KROON, &OORI));
m.insert("DKK", CurrencyForms::new(&KROON, &OORI));
m.insert(
"RUB",
CurrencyForms::new(&["rubla", "rubla"], &["kopikas", "kopikat"]),
);
m
}
fn index_form(forms: &[String], singular: bool) -> Result<&str> {
forms
.get(if singular { 0 } else { 1 })
.map(String::as_str)
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn ordinal_ones_at(n: &BigInt) -> Result<&'static str> {
let idx: usize = if n.is_negative() {
if *n < big(-10) {
return Err(N2WError::Index("list index out of range".into()));
}
(n + 10u32).to_usize().expect("bounded to 0..=9")
} else {
n.to_usize().expect("bounded to 0..=9")
};
Ok(ORDINALS_ONES[idx])
}
pub struct LangEt {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangEt {
pub fn new() -> Self {
LangEt {
currency_forms: build_currency_forms(),
}
}
fn int_to_word(&self, n: &BigInt) -> String {
if n.is_zero() {
return "null".to_string();
}
let mut parts: Vec<String> = Vec::new();
let mut n = n.clone();
for &(scale, one_form, many_form) in &[
(1_000_000_000_000i64, "üks triljon", "triljonit"),
(1_000_000_000i64, "üks miljard", "miljardit"),
(1_000_000i64, "üks miljon", "miljonit"),
] {
let s = big(scale);
if n >= s {
let (q, r) = n.div_rem(&s);
if q.is_one() {
parts.push(one_form.to_string());
} else {
parts.push(format!("{} {}", self.int_to_word(&q), many_form));
}
n = r;
}
}
let thousand = big(1000);
if n >= thousand {
let (q, r) = n.div_rem(&thousand);
if q.is_one() {
parts.push("tuhat".to_string());
} else if q == big(100) {
parts.push("sada tuhat".to_string());
} else {
parts.push(format!("{} tuhat", self.int_to_word(&q)));
}
n = r;
}
let hundred = big(100);
if n >= hundred {
let (q, r) = n.div_rem(&hundred);
parts.push(HUNDREDS[q.to_usize().expect("bounded to 1..=9")].to_string());
n = r;
}
let small = n.to_usize().expect("bounded to 0..=99");
if (11..=19).contains(&small) {
parts.push(TEENS[small - 11].to_string());
} else {
let mut small = small;
if small >= 10 {
parts.push(TENS[small / 10].to_string());
small %= 10;
}
if small > 0 {
parts.push(ONES[small].to_string());
}
}
parts.join(" ")
}
fn int_to_cardinal(&self, n: &BigInt) -> String {
if n.is_zero() {
return "null".to_string();
}
if n.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&-n));
}
self.int_to_word(n)
}
fn int_to_ordinal(&self, n: &BigInt) -> Result<String> {
if n.is_zero() {
return Ok("nullis".to_string());
}
if *n < big(10) {
return ordinal_ones_at(n).map(|s| s.to_string());
}
if *n == big(10) {
return Ok("kümnes".to_string());
}
if *n < big(20) {
let i = n.to_usize().expect("bounded to 11..=19");
return Ok(TEENS_ORDINALS[i - 11].to_string());
}
if *n < big(100) {
let v = n.to_usize().expect("bounded to 20..=99");
let (tens_val, ones_val) = (v / 10, v % 10);
return Ok(if ones_val == 0 {
ORDINALS_TENS[tens_val].to_string()
} else {
format!("{} {}", TENS[tens_val], ORDINALS_ONES[ones_val])
});
}
if *n == big(100) {
return Ok("sajas".to_string());
}
if *n == big(1000) {
return Ok("tuhandes".to_string());
}
Ok(format!("{}s", self.int_to_cardinal(n)))
}
fn year_word_sim(&self, n0: &BigDecimal) -> Result<String> {
let mut n = n0.clone();
let mut parts: Vec<String> = Vec::new();
for (scale, one_form, many_form) in [
(1_000_000_000_000i64, "üks triljon", "triljonit"),
(1_000_000_000i64, "üks miljard", "miljardit"),
(1_000_000i64, "üks miljon", "miljonit"),
] {
let s = BigDecimal::from(scale);
if n >= s {
let q = (&n / &s).with_scale(0);
if q == BigDecimal::from(1) {
parts.push(one_form.to_string());
} else {
parts.push(format!("{} {}", self.year_word_sim(&q)?, many_form));
}
n = n - &q * &s;
}
}
let thousand = BigDecimal::from(1000);
if n >= thousand {
let q = (&n / &thousand).with_scale(0);
if q == BigDecimal::from(1) {
parts.push("tuhat".to_string());
} else if q == BigDecimal::from(100) {
parts.push("sada tuhat".to_string());
} else {
parts.push(format!("{} tuhat", self.year_word_sim(&q)?));
}
n = n - &q * &thousand;
}
let hundred = BigDecimal::from(100);
if n >= hundred {
let q = (&n / &hundred).with_scale(0);
let i = q
.as_bigint_and_exponent()
.0
.to_usize()
.expect("bounded to 1..=9");
parts.push(HUNDREDS[i].to_string());
n = n - &q * &hundred;
}
let ten = BigDecimal::from(10);
let twenty = BigDecimal::from(20);
if n > ten && n < twenty {
if n.is_integer() {
let i = n
.with_scale(0)
.as_bigint_and_exponent()
.0
.to_usize()
.expect("bounded to 11..=19");
parts.push(TEENS[i - 11].to_string());
} else {
return Err(N2WError::Key(format!("{}", n)));
}
} else {
if n >= ten {
return Err(N2WError::Type(
"list indices must be integers or slices, not float".into(),
));
}
if n > BigDecimal::from(0) {
return Err(N2WError::Type(
"list indices must be integers or slices, not float".into(),
));
}
}
Ok(parts.join(" "))
}
}
impl Default for LangEt {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangEt {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" ja "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"koma"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
Ok(self.int_to_cardinal(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.int_to_ordinal(value)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(self.int_to_cardinal(value))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Decimal { value: dec, .. } => {
let pre = dec.with_scale(0).as_bigint_and_exponent().0;
Ok(self.int_to_cardinal(&pre))
}
FloatValue::Float {
value: f,
precision,
} => {
let (pre, post) = float2tuple(value);
if *f == f.trunc() {
return Ok(self.int_to_cardinal(&pre));
}
let precision = *precision as usize;
let mut pre = pre;
let mut result = String::new();
if *f < 0.0 {
result.push_str(NEGWORD);
pre = pre.abs();
}
result.push_str(&self.int_to_cardinal(&pre));
if precision > 0 {
result.push(' ');
result.push_str(self.pointword());
let post_str = post.to_string();
let pad = precision.saturating_sub(post_str.len());
let padded = format!("{}{}", "0".repeat(pad), post_str);
for ch in padded.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("non-digit {:?} in fraction", ch))
})? as usize;
result.push(' ');
result.push_str(ONES[d]);
}
}
Ok(result.trim().to_string())
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
match value {
FloatValue::Float { value: f, .. } if f.is_nan() => {
return Ok("s".to_string());
}
FloatValue::Float { value: f, .. } if f.is_infinite() => {
return Err(N2WError::Custom {
module: "builtins",
class: "RecursionError",
msg: "maximum recursion depth exceeded".into(),
});
}
_ => {}
}
let num = match value {
FloatValue::Float { value, .. } => {
BigInt::from_f64(value.trunc()).expect("finite after the guards above")
}
FloatValue::Decimal { value, .. } => value.with_scale(0).as_bigint_and_exponent().0,
};
self.int_to_ordinal(&num)
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
let d = match value {
FloatValue::Float { value: f, .. } => {
if f.is_nan() {
return Ok(String::new());
}
if f.is_infinite() {
return Err(N2WError::Custom {
module: "builtins",
class: "RecursionError",
msg: "maximum recursion depth exceeded".into(),
});
}
BigDecimal::from_f64(*f).expect("finite")
}
FloatValue::Decimal { value, .. } => value.clone(),
};
if d.is_zero() {
return Ok("null".to_string());
}
if d < BigDecimal::from(0) {
return Ok(format!("{}{}", NEGWORD, self.year_word_sim(&-d)?));
}
self.year_word_sim(&d)
}
fn lang_name(&self) -> &str {
"Num2Word_ET"
}
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 is_integer_input = matches!(val, CurrencyValue::Int(_));
let has_fractional_cents = match val {
CurrencyValue::Int(_) => false,
CurrencyValue::Decimal { value, .. } => {
let scaled = value * BigDecimal::from(100);
&scaled - scaled.with_scale(0) != BigDecimal::zero()
}
};
let (left, right, is_negative) =
parse_currency_parts(val, false, has_fractional_cents, 100);
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let cr1 = &forms.unit;
let cr2 = &forms.subunit;
let minus_str = if is_negative { "miinus " } else { "" };
let money_str = self.int_to_cardinal(&left);
let currency_str = index_form(cr1, left.is_one())?;
if is_integer_input {
return Ok(format!("{}{} {}", minus_str, money_str, currency_str));
}
let cents_str = if cents {
if has_fractional_cents {
if right.is_positive() {
self.cardinal_from_decimal(&right)?
} else {
"null".to_string()
}
} else {
let right_int = right.as_bigint_and_exponent().0;
if right_int.is_positive() {
self.int_to_cardinal(&right_int)
} else {
"null".to_string()
}
}
} else {
if has_fractional_cents {
let f = right
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", right)))?;
format!("{}", f)
} else {
right.as_bigint_and_exponent().0.to_string()
}
};
let cents_currency = index_form(cr2, right == BigDecimal::one())?;
Ok(format!(
"{}{} {}{}{} {}",
minus_str, money_str, currency_str, separator, cents_str, cents_currency
))
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use std::str::FromStr;
fn f(l: &LangEt, value: f64, precision: u32) -> String {
l.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(l: &LangEt, s: &str) -> String {
l.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
},
None,
)
.unwrap()
}
#[test]
fn float_corpus_rows() {
let l = LangEt::new();
assert_eq!(f(&l, 0.5, 1), "null koma viis");
assert_eq!(f(&l, 1.5, 1), "üks koma viis");
assert_eq!(f(&l, 3.14, 2), "kolm koma üks neli");
assert_eq!(f(&l, 12.34, 2), "kaksteist koma kolm neli");
assert_eq!(f(&l, -12.34, 2), "miinus kaksteist koma kolm neli");
assert_eq!(f(&l, 2.25, 2), "kaks koma kaks viis");
assert_eq!(f(&l, 0.1, 1), "null koma üks");
assert_eq!(f(&l, 1.005, 3), "üks koma viis");
assert_eq!(f(&l, 2.675, 3), "kaks koma kuus seitse viis");
assert_eq!(f(&l, 0.01, 2), "null koma üks");
assert_eq!(f(&l, 1.01, 2), "üks koma üks");
assert_eq!(f(&l, 0.99, 2), "null koma üheksa üheksa");
assert_eq!(f(&l, 99.99, 2), "üheksakümmend üheksa koma üheksa üheksa");
assert_eq!(f(&l, 100.5, 1), "ükssada koma viis");
assert_eq!(
f(&l, 1234.56, 2),
"tuhat kakssada kolmkümmend neli koma viis kuus"
);
assert_eq!(f(&l, -0.5, 1), "miinus null koma viis");
assert_eq!(f(&l, -1.5, 1), "miinus üks koma viis");
assert_eq!(f(&l, 1.0, 1), "üks");
assert_eq!(f(&l, 0.0, 1), "null");
}
#[test]
fn decimal_corpus_rows() {
let l = LangEt::new();
assert_eq!(d(&l, "0.01"), "null");
assert_eq!(d(&l, "1.10"), "üks");
assert_eq!(d(&l, "12.345"), "kaksteist");
assert_eq!(d(&l, "0.001"), "null");
assert_eq!(d(&l, "-12.34"), "miinus kaksteist");
assert_eq!(
d(&l, "98746251323029.99"),
"üheksakümmend kaheksa triljonit seitsesada nelikümmend kuus \
miljardit kakssada viiskümmend üks miljonit kolmsada kakskümmend \
kolm tuhat kakskümmend üheksa"
);
}
}