use crate::base::{Lang as LangTrait, 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_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const ONES: [&str; 10] = [
"",
"bat",
"bi",
"hiru",
"lau",
"bost",
"sei",
"zazpi",
"zortzi",
"bederatzi",
];
const TENS: [&str; 10] = [
"",
"hamar",
"hogei",
"hogeita hamar",
"berrogei",
"berrogeita hamar",
"hirurogei",
"hirurogeita hamar",
"laurogei",
"laurogeita hamar",
];
const ZERO_WORD: &str = "zero";
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "koma";
const SEPARATOR_DEFAULT: &str = " eta ";
const SEPARATOR_UNSET: &str = ",";
fn split_currency(value: &BigDecimal) -> (BigInt, BigInt) {
let integer = value.with_scale(0);
let fraction = value - &integer;
let cents = (fraction * BigDecimal::from(100)).with_scale(0);
(
integer.as_bigint_and_exponent().0,
cents.as_bigint_and_exponent().0,
)
}
fn small(table: &[&str; 10], i: &BigInt) -> String {
table[i.to_usize().expect("caller proved 0 <= i < 10")].to_string()
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
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 {
small(&ONES, number)
} else if number == &ten {
"hamar".to_string()
} else if number < &BigInt::from(20) {
format!("hama{}", small(&ONES, &(number - &ten)))
} else if number < &hundred {
let (ten_val, one_val) = number.div_mod_floor(&ten);
if one_val.is_zero() {
small(&TENS, &ten_val)
} else {
format!("{}ta {}", small(&TENS, &ten_val), small(&ONES, &one_val))
}
} else if number < &thousand {
let (hundred_val, remainder) = number.div_mod_floor(&hundred);
let mut result = if hundred_val.is_one() {
"ehun".to_string()
} else {
format!("{}ehun", small(&ONES, &hundred_val))
};
if !remainder.is_zero() {
result.push_str(" eta ");
result.push_str(&int_to_word(&remainder));
}
result
} else if number < &million {
let (thousand_val, remainder) = number.div_mod_floor(&thousand);
let mut result = if thousand_val.is_one() {
"mila".to_string()
} else {
format!("{} mila", int_to_word(&thousand_val))
};
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
result
} else if number < &billion {
let (million_val, remainder) = number.div_mod_floor(&million);
let mut result = if million_val.is_one() {
"milioi bat".to_string()
} else {
format!("{} milioi", int_to_word(&million_val))
};
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
result
} else {
let (billion_val, remainder) = number.div_mod_floor(&billion);
let mut result = format!("{} mila milioi", int_to_word(&billion_val));
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
result
}
}
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)
}
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 cardinal_from_repr(n: &str) -> Result<String> {
let n = n.trim();
let (mut ret, n) = match n.strip_prefix('-') {
Some(rest) => (NEGWORD.to_string(), rest),
None => (String::new(), n),
};
match n.split_once('.') {
Some((left, right)) => {
ret.push_str(&int_to_word(&py_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
let mut first = true;
for d in right.chars() {
if !first {
ret.push(' ');
}
first = false;
let digit = py_int_digit(d)?;
ret.push_str(&int_to_word(&BigInt::from(digit)));
}
Ok(ret)
}
None => {
ret.push_str(&int_to_word(&py_int(n)?));
Ok(ret)
}
}
}
pub struct LangEu {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangEu {
pub fn new() -> Self {
let currency_forms: HashMap<&'static str, CurrencyForms> = [
("EUR", &["euro", "euro"][..], &["zentimo", "zentimo"][..]),
("USD", &["dolar", "dolar"][..], &["zentabo", "zentabo"][..]),
("GBP", &["libera", "libera"][..], &["penike", "penike"][..]),
]
.into_iter()
.map(|(k, u, s)| (k, CurrencyForms::new(u, s)))
.collect();
LangEu { currency_forms }
}
}
impl Default for LangEu {
fn default() -> Self {
LangEu::new()
}
}
impl LangTrait for LangEu {
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 default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
" eta "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"koma"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
Ok(format!("{}{}", NEGWORD, int_to_word(&value.abs())))
} else {
Ok(int_to_word(value))
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
let _ = precision_override;
let n = match value {
FloatValue::Float { value, .. } => py_str_f64(*value),
FloatValue::Decimal { value, .. } => py_str_decimal(value),
};
cardinal_from_repr(&n)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}garren", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}. urtea", self.to_cardinal(value)?))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!("{}garren", self.cardinal_float_entry(value, None)?))
}
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> {
Ok(format!("{}. urtea", self.cardinal_float_entry(value, None)?))
}
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 lang_name(&self) -> &str {
"Num2Word_EU"
}
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 _ = adjective;
let separator = if separator == SEPARATOR_UNSET {
SEPARATOR_DEFAULT
} else {
separator
};
let (left, right, is_negative) = match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero(), v.is_negative()),
CurrencyValue::Decimal { value: d, .. } => {
let is_negative = d.is_negative();
let (left, right) = split_currency(&d.abs());
(left, right, is_negative)
}
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or_else(|| &self.currency_forms["EUR"]);
let left_str = int_to_word(&left);
let cents_str = if cents && !right.is_zero() {
int_to_word(&right)
} else {
String::new()
};
let mut result = format!("{} {}", left_str, forms.unit[0]);
if !cents_str.is_empty() {
result.push_str(separator);
result.push_str(¢s_str);
result.push(' ');
result.push_str(&forms.subunit[0]);
}
if is_negative {
Ok(format!("{}{}", NEGWORD, result))
} else {
Ok(result)
}
}
}
#[cfg(test)]
mod float_tests {
use super::*;
fn ff(v: f64, p: u32) -> String {
let l = LangEu::new();
match l.to_cardinal_float(&FloatValue::Float { value: v, precision: p }, None) {
Ok(s) => s,
Err(e) => format!("ERR:{:?}", e),
}
}
fn dd(s: &str) -> String {
let l = LangEu::new();
let bd = BigDecimal::from_str(s).unwrap();
let p = bd.as_bigint_and_exponent().1.max(0) as u32;
match l.to_cardinal_float(&FloatValue::Decimal { value: bd, precision: p }, None) {
Ok(s) => s,
Err(e) => format!("ERR:{:?}", e),
}
}
#[test]
fn corpus_float() {
assert_eq!(ff(0.0, 1), "zero koma zero");
assert_eq!(ff(0.5, 1), "zero koma bost");
assert_eq!(ff(1.0, 1), "bat koma zero");
assert_eq!(ff(1.5, 1), "bat koma bost");
assert_eq!(ff(2.25, 2), "bi koma bi bost");
assert_eq!(ff(3.14, 2), "hiru koma bat lau");
assert_eq!(ff(0.01, 2), "zero koma zero bat");
assert_eq!(ff(0.1, 1), "zero koma bat");
assert_eq!(ff(0.99, 2), "zero koma bederatzi bederatzi");
assert_eq!(ff(1.01, 2), "bat koma zero bat");
assert_eq!(ff(12.34, 2), "hamabi koma hiru lau");
assert_eq!(ff(99.99, 2), "laurogeita hamarta bederatzi koma bederatzi bederatzi");
assert_eq!(ff(100.5, 1), "ehun koma bost");
assert_eq!(ff(1234.56, 2), "mila biehun eta hogeita hamarta lau koma bost sei");
assert_eq!(ff(-0.5, 1), "minus zero koma bost");
assert_eq!(ff(-1.5, 1), "minus bat koma bost");
assert_eq!(ff(-12.34, 2), "minus hamabi koma hiru lau");
assert_eq!(ff(1.005, 3), "bat koma zero zero bost");
assert_eq!(ff(2.675, 3), "bi koma sei zazpi bost");
assert_eq!(ff(1000000.5, 1), "milioi bat koma bost");
}
#[test]
fn live_extra() {
assert_eq!(ff(1000000000.5, 1), "bat mila milioi koma bost");
assert_eq!(ff(0.25, 2), "zero koma bi bost");
assert_eq!(ff(10.101, 3), "hamar koma bat zero bat");
assert_eq!(ff(55.5, 1), "berrogeita hamarta bost koma bost");
assert_eq!(ff(-1234.5, 1), "minus mila biehun eta hogeita hamarta lau koma bost");
assert_eq!(ff(-0.0, 1), "minus zero koma zero");
assert_eq!(dd("1E+2"), "ERR:Value(\"invalid literal for int() with base 10: '1E+2'\")");
}
#[test]
fn corpus_dec() {
assert_eq!(dd("0.01"), "zero koma zero bat");
assert_eq!(dd("1.10"), "bat koma bat zero");
assert_eq!(dd("12.345"), "hamabi koma hiru lau bost");
assert_eq!(dd("0.001"), "zero koma zero zero bat");
assert_eq!(
dd("98746251323029.99"),
"laurogeita hamarta zortzi mila zazpiehun eta berrogeita sei mila milioi biehun eta berrogeita hamarta bat milioi hiruehun eta hogeita hiru mila hogeita bederatzi koma bederatzi bederatzi"
);
}
}