use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
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 = "point";
const ONES: [&str; 10] = [
"", "bir", "ikki", "uch", "to'rt", "besh", "olti", "yetti", "sakkiz", "to'qqiz",
];
const TENS: [&str; 10] = [
"", "o'n", "yigirma", "o'ttiz", "qirq", "ellik", "oltmish", "yetmish", "sakson", "to'qson",
];
const HUNDRED: &str = "yuz";
const THOUSAND: &str = "ming";
const MILLION: &str = "million";
const ZERO_WORD: &str = "zero";
const ORDINAL_SUFFIX: &str = "-chi";
const DEFAULT_SEPARATOR: &str = " ";
const BASE_DEFAULT_SEPARATOR: &str = ",";
const LANG_NAME: &str = "Num2Word_UZ";
fn digit(n: &BigInt) -> usize {
n.to_usize()
.expect("callers bound this to 0..=9 before indexing ONES/TENS")
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, int_to_word(&number.abs()));
}
let ten = BigInt::from(10u32);
let hundred = BigInt::from(100u32);
let thousand = BigInt::from(1_000u32);
let million = BigInt::from(1_000_000u32);
let billion = BigInt::from(1_000_000_000u32);
if number < &ten {
return ONES[digit(number)].to_string();
}
if number < &hundred {
let (tens_val, ones_val) = number.div_mod_floor(&ten);
if ones_val.is_zero() {
return TENS[digit(&tens_val)].to_string();
}
return format!("{} {}", TENS[digit(&tens_val)], ONES[digit(&ones_val)]);
}
if number < &thousand {
let (hundreds_val, remainder) = number.div_mod_floor(&hundred);
let mut result = format!("{} {}", ONES[digit(&hundreds_val)], HUNDRED);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
if number < &million {
let (thousands_val, remainder) = number.div_mod_floor(&thousand);
let mut result = format!("{} {}", int_to_word(&thousands_val), THOUSAND);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
if number < &billion {
let (millions_val, remainder) = number.div_mod_floor(&million);
let mut result = format!("{} {}", int_to_word(&millions_val), MILLION);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
number.to_string()
}
fn py_float_str(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f.is_sign_positive() {
"inf".to_string()
} else {
"-inf".to_string()
};
}
let a = f.abs();
if a != 0.0 && (a >= 1e16 || a < 1e-4) {
let s = format!("{:e}", f);
if let Some((m, e)) = s.split_once('e') {
let (sign, digits) = match e.strip_prefix('-') {
Some(d) => ("-", d.to_string()),
None => ("+", e.to_string()),
};
let digits = if digits.len() < 2 {
format!("0{}", digits)
} else {
digits
};
return format!("{}e{}{}", m, sign, digits);
}
s
} else if f.fract() == 0.0 {
format!("{:.1}", f)
} else {
format!("{}", f)
}
}
fn int_value_error(repr_no_dot: &str) -> N2WError {
let unsigned = repr_no_dot.strip_prefix('-').unwrap_or(repr_no_dot);
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
unsigned
))
}
pub struct LangUz {
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl LangUz {
pub fn new() -> Self {
let mut currency_forms = HashMap::new();
currency_forms.insert(
"UZS",
CurrencyForms::new(&["so'm", "so'm"], &["tiyin", "tiyin"]),
);
currency_forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
currency_forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["cent", "cents"]),
);
let fallback_forms = currency_forms
.get("UZS")
.expect("CURRENCY_FORMS[\"UZS\"] is inserted directly above")
.clone();
LangUz {
currency_forms,
fallback_forms,
}
}
}
impl Default for LangUz {
fn default() -> Self {
LangUz::new()
}
}
impl Lang for LangUz {
fn default_currency(&self) -> &str {
"UZS"
}
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, int_to_word(&magnitude))
.trim()
.to_string())
}
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_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let precision = value.precision() as usize;
let (pre, post) = float2tuple(value);
let is_negative = match value {
FloatValue::Float { value, .. } => value.is_sign_negative(),
FloatValue::Decimal { value, .. } => value.is_negative(),
};
let mut words: Vec<String> = Vec::new();
if is_negative {
words.push(NEGWORD.trim().to_string());
}
words.push(int_to_word(&pre.abs()));
if precision > 0 {
words.push(POINTWORD.to_string());
let post_digits = post.to_string();
let right = format!(
"{}{}",
"0".repeat(precision.saturating_sub(post_digits.len())),
post_digits
);
for ch in right.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("non-digit {:?} in fractional part", ch))
})?;
words.push(int_to_word(&BigInt::from(d)));
}
}
Ok(words.join(" "))
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value: f, .. } => {
let a = f.abs();
if f.is_finite() && (*f == 0.0 || (a >= 1e-4 && a < 1e16)) {
self.to_cardinal_float(value, precision_override)
} else {
Err(int_value_error(&py_float_str(*f)))
}
}
FloatValue::Decimal { value: d, .. } => {
let s = python_decimal_str(d);
if s.contains('.') {
self.to_cardinal_float(value, precision_override)
} else if s.contains('E') {
Err(int_value_error(&s))
} else {
let i = value
.as_whole_int()
.expect("dot-less, E-less Decimal repr is integral");
self.to_cardinal(&i)
}
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(format!("{}{}", cardinal, ORDINAL_SUFFIX))
}
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> {
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 {
LANG_NAME
}
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 == BASE_DEFAULT_SEPARATOR {
DEFAULT_SEPARATOR
} else {
separator
};
let is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(v) => {
if is_negative {
v.abs().to_string()
} else {
v.to_string()
}
}
CurrencyValue::Decimal { value: d, .. } => {
if is_negative {
d.abs().to_string()
} else {
d.to_string()
}
}
};
let mut parts = s.split('.');
let part0 = parts.next().unwrap_or("");
let part1 = parts.next();
let left = if part0.is_empty() {
BigInt::zero()
} else {
BigInt::from_str(part0).map_err(|e| N2WError::Value(e.to_string()))?
};
let right = match part1 {
Some(f) if !f.is_empty() => {
let mut two: String = f.chars().take(2).collect();
while two.chars().count() < 2 {
two.push('0');
}
BigInt::from_str(&two).map_err(|e| N2WError::Value(e.to_string()))?
}
_ => BigInt::zero(),
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.fallback_forms);
let cr1 = &forms.unit;
let cr2 = &forms.subunit;
let one = BigInt::one();
let mut result = format!(
"{} {}",
int_to_word(&left),
if left != one { &cr1[1] } else { &cr1[0] }
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(if right != one { &cr2[1] } else { &cr2[0] });
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use bigdecimal::BigDecimal;
fn f(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn d(s: &str) -> FloatValue {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.unsigned_abs() as u32;
FloatValue::Decimal { value, precision }
}
fn go(v: &FloatValue) -> String {
LangUz::new().to_cardinal_float(v, None).unwrap()
}
#[test]
fn corpus_cardinal_float_rows() {
for (value, precision, out) in [
(0.0, 1, "zero point zero"),
(0.5, 1, "zero point besh"),
(1.0, 1, "bir point zero"),
(1.5, 1, "bir point besh"),
(2.25, 2, "ikki point ikki besh"),
(3.14, 2, "uch point bir to'rt"),
(0.01, 2, "zero point zero bir"),
(0.1, 1, "zero point bir"),
(0.99, 2, "zero point to'qqiz to'qqiz"),
(1.01, 2, "bir point zero bir"),
(12.34, 2, "o'n ikki point uch to'rt"),
(99.99, 2, "to'qson to'qqiz point to'qqiz to'qqiz"),
(100.5, 1, "bir yuz point besh"),
(1234.56, 2, "bir ming ikki yuz o'ttiz to'rt point besh olti"),
(-0.5, 1, "minus zero point besh"),
(-1.5, 1, "minus bir point besh"),
(-12.34, 2, "minus o'n ikki point uch to'rt"),
(1.005, 3, "bir point zero zero besh"),
(2.675, 3, "ikki point olti yetti besh"),
] {
assert_eq!(go(&f(value, precision)), out, "float {}", value);
}
}
#[test]
fn corpus_cardinal_dec_rows() {
for (arg, out) in [
("0.01", "zero point zero bir"),
("1.10", "bir point bir zero"),
("12.345", "o'n ikki point uch to'rt besh"),
("98746251323029.99", "98746251323029 point to'qqiz to'qqiz"),
("0.001", "zero point zero zero bir"),
] {
assert_eq!(go(&d(arg)), out, "decimal {}", arg);
}
}
#[test]
fn traced_against_pure_python() {
assert_eq!(go(&f(-0.0, 1)), "minus zero point zero");
assert_eq!(go(&f(1234567890.5, 1)), "1234567890 point besh");
assert_eq!(
go(&f(123456789012345.67, 2)),
"123456789012345 point olti yetti"
);
}
#[test]
fn precision_override_is_ignored() {
let uz = LangUz::new();
let v = f(2.675, 3);
let expect = "ikki point olti yetti besh";
assert_eq!(uz.to_cardinal_float(&v, None).unwrap(), expect);
assert_eq!(uz.to_cardinal_float(&v, Some(2)).unwrap(), expect);
assert_eq!(uz.to_cardinal_float(&v, Some(0)).unwrap(), expect);
}
}