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_integer::Integer;
use num_traits::{Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const ZERO_WORD: &str = "нуль";
const NEGWORD: &str = "минус ";
const POINTWORD: &str = "өтөр";
const SEPARATOR: &str = " һәм ";
const BASE_SEPARATOR: &str = ",";
const FALLBACK_CURRENCY: &str = "RUB";
const ONES: [&str; 10] = [
"", "бер", "ике", "өс", "дүрт", "биш", "алты", "ете", "һигеҙ", "туғыҙ",
];
const TENS: [&str; 10] = [
"",
"",
"егерме",
"утыҙ",
"ҡырҡ",
"илле",
"алтмыш",
"етмеш",
"һикһән",
"туҡһан",
];
const TEENS: [&str; 10] = [
"ун",
"ун бер",
"ун ике",
"ун өс",
"ун дүрт",
"ун биш",
"ун алты",
"ун ете",
"ун һигеҙ",
"ун туғыҙ",
];
fn idx(n: &BigInt) -> usize {
n.to_usize()
.expect("guarded by a range check to 0..=99 before this call")
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if *number < BigInt::from(10u32) {
return ONES[idx(number)].to_string();
}
if *number < BigInt::from(20u32) {
return TEENS[idx(number) - 10].to_string();
}
if *number < BigInt::from(100u32) {
let div = number / 10u32;
let rem = number % 10u32;
let mut out = TENS[idx(&div)].to_string();
if !rem.is_zero() {
out.push(' ');
out.push_str(ONES[idx(&rem)]);
}
return out;
}
if *number < BigInt::from(1000u32) {
let div = number / 100u32;
let rem = number % 100u32;
let mut out = ONES[idx(&div)].to_string();
out.push_str(" йөҙ");
if !rem.is_zero() {
out.push(' ');
out.push_str(&int_to_word(&rem));
}
return out;
}
let (scale, scale_word) = if *number < BigInt::from(1_000_000u32) {
(BigInt::from(1_000u32), " мең")
} else if *number < BigInt::from(1_000_000_000u32) {
(BigInt::from(1_000_000u32), " миллион")
} else {
(BigInt::from(1_000_000_000u32), " миллиард")
};
let div = number / &scale;
let rem = number % &scale;
let mut out = int_to_word(&div);
out.push_str(scale_word);
if !rem.is_zero() {
out.push(' ');
out.push_str(&int_to_word(&rem));
}
out
}
fn cardinal(value: &BigInt) -> String {
if value.is_negative() {
format!("{}{}", NEGWORD, int_to_word(&value.abs()))
} else {
int_to_word(value)
}
}
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 float_str(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f < 0.0 { "-inf" } else { "inf" }.to_string();
}
let sign = if f.is_sign_negative() { "-" } else { "" };
let a = f.abs();
let shortest = format!("{:e}", a);
let (mantissa, _) = shortest
.split_once('e')
.expect("{:e} on a finite f64 always emits an exponent");
let k = mantissa.chars().filter(char::is_ascii_digit).count();
let exact = format!("{:.*e}", k - 1, a);
let (mantissa, e10) = exact
.split_once('e')
.expect("{:e} on a finite f64 always emits an exponent");
let e10: i64 = e10
.parse()
.expect("{:e}'s exponent is a plain decimal integer");
let ds: String = mantissa.chars().filter(|c| *c != '.').collect();
let decpt = e10 + 1;
if decpt > 16 || decpt <= -4 {
let mantissa = if ds.len() > 1 {
format!("{}.{}", &ds[..1], &ds[1..])
} else {
ds
};
let sgn = if e10 < 0 { '-' } else { '+' };
return format!("{}{}e{}{:02}", sign, mantissa, sgn, e10.abs());
}
if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), ds)
} else if decpt as usize >= ds.len() {
format!(
"{}{}{}.0",
sign,
ds,
"0".repeat(decpt as usize - ds.len())
)
} else {
format!("{}{}.{}", sign, &ds[..decpt as usize], &ds[decpt as usize..])
}
}
fn decimal_str(d: &BigDecimal) -> String {
let (mantissa, scale) = d.as_bigint_and_exponent();
let sign = if mantissa.is_negative() { "-" } else { "" };
let ds = mantissa.abs().to_string();
let exp = -scale;
let leftdigits = exp + ds.len() as i64;
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), ds),
)
} else if dotplace as usize >= ds.len() {
(
format!("{}{}", ds, "0".repeat(dotplace as usize - ds.len())),
String::new(),
)
} else {
(
ds[..dotplace as usize].to_string(),
format!(".{}", &ds[dotplace as usize..]),
)
};
let e = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
format!("{}{}{}{}", sign, intpart, fracpart, e)
}
fn cardinal_from_str(n: &str) -> Result<String> {
let n = n.trim();
let (n, mut ret) = match n.strip_prefix('-') {
Some(rest) => (rest, NEGWORD.to_string()),
None => (n, String::new()),
};
match n.split_once('.') {
Some((left, right)) => {
ret.push_str(&int_to_word(&py_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
for (i, d) in right.chars().enumerate() {
if i > 0 {
ret.push(' ');
}
let mut buf = [0u8; 4];
ret.push_str(&int_to_word(&py_int(d.encode_utf8(&mut buf))?));
}
Ok(ret)
}
None => {
ret.push_str(&int_to_word(&py_int(n)?));
Ok(ret)
}
}
}
fn split_currency(val: &CurrencyValue) -> (BigInt, BigInt) {
match val {
CurrencyValue::Int(i) => (i.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => {
let d = d.abs();
let (mantissa, exp) = d.as_bigint_and_exponent();
if exp <= 0 {
let scale = BigInt::from(10u32).pow((-exp) as u32);
(mantissa * scale, BigInt::zero())
} else {
let scale = BigInt::from(10u32).pow(exp as u32);
let (left, rem) = mantissa.div_rem(&scale);
let right = (rem * 100u32) / scale;
(left, right)
}
}
}
}
pub struct LangBa {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangBa {
pub fn new() -> Self {
let mut currency_forms = HashMap::new();
currency_forms.insert(
"RUB",
CurrencyForms::new(&["һум", "һум"], &["тин", "тин"]),
);
currency_forms.insert(
"USD",
CurrencyForms::new(&["доллар", "доллар"], &["цент", "цент"]),
);
currency_forms.insert(
"EUR",
CurrencyForms::new(&["евро", "евро"], &["цент", "цент"]),
);
LangBa { currency_forms }
}
}
impl Default for LangBa {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangBa {
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 {
"RUB"
}
fn default_separator(&self) -> &str {
" һәм "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
POINTWORD
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
Ok(cardinal(value))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let n = match value {
FloatValue::Float { value, .. } => float_str(*value),
FloatValue::Decimal { value, .. } => decimal_str(value),
};
cardinal_from_str(&n)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-се", cardinal(value)))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-се", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(format!("{} йыл", cardinal(value)))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!("{}-се", 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!("{} йыл", 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_BA"
}
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 == BASE_SEPARATOR {
SEPARATOR
} else {
separator
};
let forms = self.currency_forms.get(currency).unwrap_or_else(|| {
self.currency_forms
.get(FALLBACK_CURRENCY)
.expect("CURRENCY_FORMS[\"RUB\"] is the eager .get default")
});
let is_negative = val.is_negative();
let (left, right) = split_currency(val);
let left_str = int_to_word(&left);
let mut result = format!("{} {}", left_str, forms.unit[0]);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(&forms.subunit[0]);
}
Ok(if is_negative {
format!("{}{}", NEGWORD, result)
} else {
result
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cardinal_f(f: f64) -> Result<String> {
LangBa::new().to_cardinal_float(&FloatValue::Float { value: f, precision: 0 }, None)
}
fn cardinal_d(s: &str) -> Result<String> {
LangBa::new().to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
},
None,
)
}
#[test]
fn corpus_float_rows() {
for (v, want) in [
(0.0, "нуль өтөр нуль"),
(0.5, "нуль өтөр биш"),
(1.0, "бер өтөр нуль"),
(1.5, "бер өтөр биш"),
(2.25, "ике өтөр ике биш"),
(3.14, "өс өтөр бер дүрт"),
(0.01, "нуль өтөр нуль бер"),
(0.1, "нуль өтөр бер"),
(0.99, "нуль өтөр туғыҙ туғыҙ"),
(1.01, "бер өтөр нуль бер"),
(12.34, "ун ике өтөр өс дүрт"),
(99.99, "туҡһан туғыҙ өтөр туғыҙ туғыҙ"),
(100.5, "бер йөҙ өтөр биш"),
(1234.56, "бер мең ике йөҙ утыҙ дүрт өтөр биш алты"),
(-0.5, "минус нуль өтөр биш"),
(-1.5, "минус бер өтөр биш"),
(-12.34, "минус ун ике өтөр өс дүрт"),
(1.005, "бер өтөр нуль нуль биш"),
(2.675, "ике өтөр алты ете биш"),
] {
assert_eq!(cardinal_f(v).unwrap(), want, "float {:?}", v);
}
}
#[test]
fn corpus_decimal_rows() {
for (s, want) in [
("0.01", "нуль өтөр нуль бер"),
("1.10", "бер өтөр бер нуль"),
("12.345", "ун ике өтөр өс дүрт биш"),
(
"98746251323029.99",
"туҡһан һигеҙ мең ете йөҙ ҡырҡ алты миллиард ике йөҙ илле бер миллион \
өс йөҙ егерме өс мең егерме туғыҙ өтөр туғыҙ туғыҙ",
),
("0.001", "нуль өтөр нуль нуль бер"),
] {
assert_eq!(cardinal_d(s).unwrap(), want, "Decimal({:?})", s);
}
}
#[test]
fn decimal_arm_is_exact_and_scale_preserving() {
assert_eq!(cardinal_d("1.10").unwrap(), "бер өтөр бер нуль");
assert_eq!(cardinal_d("1.1").unwrap(), "бер өтөр бер");
assert_eq!(cardinal_d("5.00").unwrap(), "биш өтөр нуль нуль");
assert_eq!(cardinal_d("5").unwrap(), "биш");
assert_eq!(cardinal_f(5.0).unwrap(), "биш өтөр нуль");
}
#[test]
fn float_and_decimal_spellings_diverge() {
assert!(matches!(cardinal_f(0.00001), Err(N2WError::Value(_))));
assert_eq!(
cardinal_d("0.00001").unwrap(),
"нуль өтөр нуль нуль нуль нуль бер"
);
assert_eq!(
cardinal_d("0.000001").unwrap(),
"нуль өтөр нуль нуль нуль нуль нуль бер"
);
assert!(matches!(cardinal_d("0.0000001"), Err(N2WError::Value(_))));
}
#[test]
fn exponential_repr_raises_value_error() {
match cardinal_f(1e16) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1e+16'")
}
other => panic!("expected ValueError, got {:?}", other),
}
match cardinal_f(1.5e-5) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: 'e'")
}
other => panic!("expected ValueError, got {:?}", other),
}
match cardinal_f(-1e16) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1e+16'")
}
other => panic!("expected ValueError, got {:?}", other),
}
match cardinal_d("1E+2") {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1E+2'")
}
other => panic!("expected ValueError, got {:?}", other),
}
}
#[test]
fn plain_reprs_at_the_exponential_edges_do_not_raise() {
assert_eq!(cardinal_f(1e15).unwrap(), "бер миллион миллиард өтөр нуль");
assert_eq!(
cardinal_f(0.0001).unwrap(),
"нуль өтөр нуль нуль нуль бер"
);
assert!(matches!(cardinal_f(1e-5), Err(N2WError::Value(_))));
assert!(matches!(cardinal_f(1e16), Err(N2WError::Value(_))));
}
#[test]
fn exact_decimal_ties_round_to_even_like_cpython() {
assert_eq!(float_str(1962311374373454.25), "1962311374373454.2");
assert_eq!(float_str(2050093655521678.25), "2050093655521678.2");
assert_eq!(float_str(-145360241606786.125), "-145360241606786.12");
assert_eq!(float_str(106779538212252.625), "106779538212252.62");
assert!(cardinal_f(1962311374373454.25).unwrap().ends_with("ике"));
}
#[test]
fn float_str_matches_cpython_repr() {
for (f, want) in [
(0.0, "0.0"),
(-0.0, "-0.0"),
(1.0, "1.0"),
(0.5, "0.5"),
(0.01, "0.01"),
(100.5, "100.5"),
(1.005, "1.005"),
(2.675, "2.675"),
(1234.56, "1234.56"),
(1e15, "1000000000000000.0"),
(1e16, "1e+16"),
(1e21, "1e+21"),
(1e100, "1e+100"),
(0.0001, "0.0001"),
(0.00001, "1e-05"),
(1.5e-5, "1.5e-05"),
(5e-324, "5e-324"),
(1.7976931348623157e308, "1.7976931348623157e+308"),
(123456789012345.6, "123456789012345.6"),
] {
assert_eq!(float_str(f), want, "repr({:?})", f);
}
}
#[test]
fn decimal_str_matches_cpython() {
for (s, want) in [
("0.01", "0.01"),
("1.10", "1.10"),
("5", "5"),
("5.00", "5.00"),
("100", "100"),
("0", "0"),
("0.00", "0.00"),
("0.00001", "0.00001"),
("0.000001", "0.000001"),
("0.0000001", "1E-7"),
("1E+2", "1E+2"),
("1E+16", "1E+16"),
("-1.005", "-1.005"),
("98746251323029.99", "98746251323029.99"),
] {
assert_eq!(decimal_str(&BigDecimal::from_str(s).unwrap()), want, "Decimal({:?})", s);
}
}
#[test]
fn negative_zero_and_negative_fractions_keep_the_negword() {
assert_eq!(cardinal_f(-0.5).unwrap(), "минус нуль өтөр биш");
assert_eq!(cardinal_f(-0.0).unwrap(), "минус нуль өтөр нуль");
assert_eq!(cardinal_f(-0.01).unwrap(), "минус нуль өтөр нуль бер");
assert_eq!(cardinal_d("-0.5").unwrap(), "минус нуль өтөр биш");
}
#[test]
fn precision_override_is_ignored() {
let l = LangBa::new();
let v = FloatValue::Float { value: 2.675, precision: 3 };
let want = "ике өтөр алты ете биш";
assert_eq!(l.to_cardinal_float(&v, None).unwrap(), want);
assert_eq!(l.to_cardinal_float(&v, Some(1)).unwrap(), want);
assert_eq!(l.to_cardinal_float(&v, Some(9)).unwrap(), want);
let v = FloatValue::Float { value: 2.675, precision: 99 };
assert_eq!(l.to_cardinal_float(&v, None).unwrap(), want);
}
#[test]
fn integer_modes_are_untouched() {
let l = LangBa::new();
assert_eq!(l.to_cardinal(&BigInt::from(0)).unwrap(), "нуль");
assert_eq!(l.to_cardinal(&BigInt::from(-12)).unwrap(), "минус ун ике");
assert_eq!(l.to_ordinal(&BigInt::from(11)).unwrap(), "ун бер-се");
}
}