use crate::base::{Lang, N2WError, Result};
use crate::currency::CurrencyForms;
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;
const ZERO: &str = "нол";
const NEGWORD: &str = "минус";
const POINTWORD: &str = "вергул";
const ONES: [&str; 10] = [
"",
"бир",
"икки",
"уч",
"тўрт",
"беш",
"олти",
"етти",
"саккиз",
"тўққиз",
];
const ONES_FEMININE: [&str; 10] = [
"",
"бир",
"икки",
"уч",
"тўрт",
"беш",
"олти",
"етти",
"саккиз",
"тўққиз",
];
const TENS: [&str; 10] = [
"ўн",
"ўн бир",
"ўн икки",
"ўн уч",
"ўн тўрт",
"ўн беш",
"ўн олти",
"ўн етти",
"ўн саккиз",
"ўн тўққиз",
];
const TWENTIES: [&str; 10] = [
"",
"",
"йигирма",
"ўттиз",
"қирқ",
"эллик",
"олтмиш",
"етмиш",
"саксон",
"тўқсон",
];
const HUNDREDS: [&str; 10] = [
"",
"бир юз",
"икки юз",
"уч юз",
"тўрт юз",
"беш юз",
"олти юз",
"етти юз",
"саккиз юз",
"тўққиз юз",
];
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const TSENT: [&str; 3] = ["цент", "цент", "цент"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"UZS",
CurrencyForms::new(&["сўм", "сўм", "сўм"], &["тийин", "тийин", "тийин"]),
);
m.insert("EUR", CurrencyForms::new(&["евро", "евро", "евро"], &TSENT));
m.insert(
"USD",
CurrencyForms::new(&["доллар", "доллар", "доллар"], &TSENT),
);
m
}
fn plural_form_index(n: &BigInt) -> usize {
let m10 = n.mod_floor(&BigInt::from(10));
let m100 = n.mod_floor(&BigInt::from(100));
if m100 < BigInt::from(10) || m100 > BigInt::from(20) {
if m10 == BigInt::from(1) {
0
} else if m10 < BigInt::from(5) && m10 > BigInt::from(1) {
1
} else {
2
}
} else {
2
}
}
fn key_error(key: String) -> N2WError {
N2WError::Key(key)
}
fn parse_int(s: &str) -> Result<BigInt> {
BigInt::parse_bytes(s.as_bytes(), 10)
.ok_or_else(|| N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s)))
}
fn py_float_repr(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 a = f.abs();
if a != 0.0 && (a >= 1e16 || a < 1e-4) {
let s = format!("{:e}", f); let (mant, _) = s.split_once('e').expect("LowerExp always emits 'e'");
let p = mant.split_once('.').map_or(0, |(_, frac)| frac.len());
let s = format!("{:.*e}", p, f);
let (mant, exp) = s.split_once('e').expect("LowerExp always emits 'e'");
let exp: i32 = exp.parse().expect("LowerExp exponent is an integer");
let sign = if exp < 0 { '-' } else { '+' };
return format!("{}e{}{:02}", mant, sign, exp.unsigned_abs());
}
let p = format!("{}", f)
.split_once('.')
.map_or(0, |(_, frac)| frac.len());
format!("{:.*}", p.max(1), f)
}
fn py_decimal_str(v: &BigDecimal) -> String {
let (int_val, scale) = v.as_bigint_and_exponent();
let sign = if int_val.is_negative() { "-" } else { "" };
let digits = int_val.magnitude().to_string(); let exp = -scale; let ndigits = digits.len() as i64;
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.unsigned_abs() as usize),
digits
),
)
} else if dotplace >= ndigits {
(
format!("{}{}", digits, "0".repeat((dotplace - ndigits) as usize)),
String::new(),
)
} else {
(
digits[..dotplace as usize].to_string(),
format!(".{}", &digits[dotplace as usize..]),
)
};
let expstr = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace) };
format!("{}{}{}{}", sign, intpart, fracpart, expstr)
}
fn splitbyx(n: &str, x: usize) -> Result<Vec<BigInt>> {
let chars: Vec<char> = n.chars().collect();
let length = chars.len();
let slice = |i: usize, j: usize| -> String { chars[i..j.min(length)].iter().collect() };
let mut out: Vec<BigInt> = Vec::new();
if length > x {
let start = length % x;
if start > 0 {
out.push(parse_int(&slice(0, start))?);
}
let mut i = start;
while i < length {
out.push(parse_int(&slice(i, i + x))?);
i += x;
}
} else {
out.push(parse_int(n)?);
}
Ok(out)
}
fn get_digits(n: &BigInt) -> [usize; 3] {
let s = format!("{:0>3}", n.to_string());
let chars: Vec<char> = s.chars().collect();
let tail = &chars[chars.len() - 3..];
let mut a = [0usize; 3];
for (k, c) in tail.iter().rev().enumerate() {
a[k] = c.to_digit(10).unwrap_or(0) as usize;
}
a
}
fn char_from_end(chars: &[char], k: usize) -> Result<char> {
if chars.len() < k {
return Err(N2WError::Index("string index out of range".to_string()));
}
Ok(chars[chars.len() - k])
}
pub struct LangUzCyr {
thousands: HashMap<usize, [&'static str; 3]>,
ords: HashMap<&'static str, &'static str>,
ords_feminine: HashMap<&'static str, &'static str>,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangUzCyr {
fn default() -> Self {
Self::new()
}
}
impl LangUzCyr {
pub fn new() -> Self {
let mut thousands: HashMap<usize, [&'static str; 3]> = HashMap::new();
thousands.insert(1, ["минг", "минг", "минг"]); thousands.insert(2, ["миллион", "миллион", "миллион"]); thousands.insert(3, ["миллиард", "миллиард", "миллиард"]); thousands.insert(4, ["триллион", "триллион", "триллион"]); thousands.insert(5, ["квадриллион", "квадриллион", "квадриллион"]); thousands.insert(6, ["квинтиллион", "квинтиллион", "квинтиллион"]); thousands.insert(7, ["секстиллион", "секстиллион", "секстиллион"]); thousands.insert(8, ["септиллион", "септиллион", "септиллион"]); thousands.insert(9, ["октиллион", "октиллион", "октиллион"]); thousands.insert(10, ["нониллион", "нониллион", "нониллион"]);
let mut ords: HashMap<&'static str, &'static str> = HashMap::new();
ords.insert("ноль", "нолинчи"); ords.insert("бир", "биринчи");
ords.insert("икки", "иккинчи");
ords.insert("уч", "учинчи");
ords.insert("тўрт", "тўртинчи");
ords.insert("беш", "бешинчи");
ords.insert("олти", "олтинчи");
ords.insert("етти", "еттинчи");
ords.insert("саккиз", "саккизинчи");
ords.insert("тўққиз", "тўққизинчи");
ords.insert("юз", "юзинчи");
let mut ords_feminine: HashMap<&'static str, &'static str> = HashMap::new();
ords_feminine.insert("бир", "");
ords_feminine.insert("икки", "икки");
ords_feminine.insert("уч", "уч");
ords_feminine.insert("тўрт", "тўрт");
ords_feminine.insert("беш", "беш");
ords_feminine.insert("олти", "олти");
ords_feminine.insert("етти", "етти");
ords_feminine.insert("саккиз", "саккиз");
ords_feminine.insert("тўққиз", "тўққиз");
LangUzCyr {
thousands,
ords,
ords_feminine,
currency_forms: build_currency_forms(),
}
}
fn thousands_at(&self, i: usize) -> Result<&[&'static str; 3]> {
self.thousands
.get(&i)
.ok_or_else(|| key_error(i.to_string()))
}
fn pluralize(&self, n: &BigInt, forms: &[&'static str; 3]) -> String {
forms[plural_form_index(n)].to_string()
}
fn int2word(&self, n: &BigInt, feminine: bool) -> Result<String> {
if n.is_negative() {
return Ok(format!("{} {}", NEGWORD, self.int2word(&n.abs(), false)?));
}
if n.is_zero() {
return Ok(ZERO.to_string());
}
let mut words: Vec<String> = Vec::new();
let chunks = splitbyx(&n.to_string(), 3)?;
let mut i = chunks.len();
for x in chunks.iter() {
i -= 1;
if x.is_zero() {
continue;
}
let [n1, n2, n3] = get_digits(x);
if n3 > 0 {
words.push(HUNDREDS[n3].to_string());
}
if n2 > 1 {
words.push(TWENTIES[n2].to_string());
}
if n2 == 1 {
words.push(TENS[n1].to_string());
} else if n1 > 0 {
let ones: &[&str; 10] = if i == 1 || (feminine && i == 0) {
&ONES_FEMININE
} else {
&ONES
};
words.push(ones[n1].to_string());
}
if i > 0 {
let forms = self.thousands_at(i)?;
words.push(self.pluralize(x, forms));
}
}
Ok(words.join(" "))
}
fn ordinal_fallback(&self, lastword: &str) -> Result<String> {
let c: Vec<char> = lastword.chars().collect();
let len = c.len();
let head: String = c[..len.saturating_sub(3)].iter().collect();
let tail5: String = c[len.saturating_sub(5)..].iter().collect();
if let Some(v) = self.ords_feminine.get(head.as_str()) {
return Ok(format!("{}юзинчи", v));
}
if char_from_end(&c, 1)? == 'а' || char_from_end(&c, 2)? == 'м' {
return Ok(format!("{}нчи", lastword));
}
if char_from_end(&c, 1)? == 'к' {
return Ok(format!("{}инчи", lastword));
}
if char_from_end(&c, 1)? == 'қ' {
return Ok(format!("{}инчи", lastword));
}
if tail5 == "ўн" {
return Ok(format!("{}инчи", lastword));
}
if char_from_end(&c, 2)? == 'ш' || char_from_end(&c, 1)? == 'ш' {
let mut w = lastword.to_string();
if char_from_end(&c, 2)? == 'ш' {
let mut t: Vec<char> = w.chars().collect();
t.pop();
w = t.into_iter().collect::<String>() + "инчи";
}
let wc: Vec<char> = w.chars().collect();
if char_from_end(&wc, 1)? == 'ш' {
w = format!("{}инчи", w);
}
return Ok(w);
}
if char_from_end(&c, 1)? == 'н' || char_from_end(&c, 2)? == 'н' {
return Ok(format!("{}инчи", lastword));
}
if char_from_end(&c, 1)? == 'з' || char_from_end(&c, 2)? == 'з' {
return Ok(format!("{}инчи", lastword));
}
Ok(lastword.to_string())
}
fn ordinalize(&self, cardinal: &str) -> Result<String> {
let mut outwords: Vec<String> = cardinal.split(' ').map(|s| s.to_string()).collect();
let n = outwords.len();
let mut lastword = outwords[n - 1].to_lowercase();
if n > 1 {
let w2 = outwords[n - 2].clone();
if let Some(v) = self.ords_feminine.get(w2.as_str()) {
outwords[n - 2] = v.to_string();
} else if w2 == "ўн" {
let mut t: Vec<char> = w2.chars().collect();
t.pop();
outwords[n - 2] = t.into_iter().collect::<String>() + "н";
}
}
if n == 3 {
if outwords[n - 3] == "бир" {
outwords[n - 3] = String::new();
}
}
let ord_hit = self.ords.get(lastword.as_str()).map(|v| v.to_string());
lastword = match ord_hit {
Some(v) => v,
None => self.ordinal_fallback(&lastword)?,
};
outwords[n - 1] = self.title(&lastword);
Ok(outwords.join(" ").trim().to_string())
}
fn verify_ordinal_float(&self, value: &FloatValue) -> Result<()> {
let py_str = || match value {
FloatValue::Float { value, .. } => py_float_repr(*value),
FloatValue::Decimal { value, .. } => py_decimal_str(value),
};
let whole = match value {
FloatValue::Float { value: f, .. } => f.fract() == 0.0 && f.is_finite(),
FloatValue::Decimal { value: d, .. } => d.is_integer(),
};
if !whole {
return Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
py_str()
)));
}
let negative = match value {
FloatValue::Float { value: f, .. } => *f < 0.0,
FloatValue::Decimal { value: d, .. } => d.is_negative(),
};
if negative {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
py_str()
)));
}
Ok(())
}
}
impl Lang for LangUzCyr {
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 {
","
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"вергул"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
self.int2word(value, false)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
let cardinal = self.to_cardinal(value)?;
self.ordinalize(&cardinal)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
self.verify_ordinal_float(value)?;
let cardinal = self.cardinal_float_entry(value, None)?;
self.ordinalize(&cardinal)
}
fn lang_name(&self) -> &str {
"Num2Word_UZ_CYRILLIC"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
forms
.get(plural_form_index(n))
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".to_string()))
}
fn cents_verbose(&self, number: &BigInt, currency: &str) -> Result<String> {
self.int2word(number, currency == "UZS")
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let n = match value {
FloatValue::Float { value, .. } => py_float_repr(*value),
FloatValue::Decimal { value, .. } => py_decimal_str(value),
};
match n.split_once('.') {
Some((left, right)) => {
let l = parse_int(left)?;
let left_words = self.int2word(&l, false)?;
let r = parse_int(right)?;
let right_words = self.int2word(&r, false)?;
Ok(format!("{} {} {}", left_words, POINTWORD, right_words))
}
None => self.int2word(&parse_int(&n)?, false),
}
}
fn cardinal_from_decimal(&self, value: &BigDecimal) -> Result<String> {
let f = value
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", value)))?;
let repr = py_float_repr(f);
let precision = match repr.split_once('.') {
Some((_, frac)) if !frac.contains('e') => frac.len() as u32,
_ => 0,
};
self.to_cardinal_float(
&FloatValue::Float {
value: f,
precision,
},
None,
)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { negative } => Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
if negative { "-Infinity" } else { "Infinity" }
))),
other => Ok(other),
}
}
}