use crate::base::{KwVal, Kwargs, Lang, N2WError, Result};
use crate::currency::{default_to_currency, CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const ZERO: &str = "нула";
const NEGWORD: &str = "минус";
const ONES: [(&str, &str); 10] = [
("", ""), ("један", "једна"),
("два", "две"),
("три", "три"),
("четири", "четири"),
("пет", "пет"),
("шест", "шест"),
("седам", "седам"),
("осам", "осам"),
("девет", "девет"),
];
const TENS: [&str; 10] = [
"десет",
"једанаест",
"дванаест",
"тринаест",
"четрнаест",
"петнаест",
"шеснаест",
"седамнаест",
"осамнаест",
"деветнаест",
];
const TWENTIES: [&str; 10] = [
"", "", "двадесет",
"тридесет",
"четрдесет",
"педесет",
"шездесет",
"седамдесет",
"осамдесет",
"деведесет",
];
const HUNDREDS: [&str; 10] = [
"", "сто",
"двеста",
"триста",
"четиристо",
"петсто",
"шесто",
"седамсто",
"осамсто",
"деветсто",
];
fn scale(chunk_len: usize) -> Result<(&'static str, &'static str, &'static str, bool)> {
Ok(match chunk_len {
0 => ("", "", "", false),
1 => ("хиљада", "хиљаде", "хиљада", true), 2 => ("милион", "милиона", "милиона", false), 3 => ("милијарда", "милијарде", "милијарди", true), 4 => ("билион", "билиона", "билиона", false), 5 => ("билијарда", "билијарде", "билијарди", true), 6 => ("трилион", "трилиона", "трилиона", false), 7 => ("трилијарда", "трилијарде", "трилијарди", true), 8 => ("квадрилион", "квадрилиона", "квадрилиона", false), 9 => ("квадрилијарда", "квадрилијарде", "квадрилијарди", true), 10 => ("квинтилион", "квинтилиона", "квинтилиона", false), _ => return Err(key_error(&chunk_len.to_string())),
})
}
fn attribute_error(msg: &str) -> N2WError {
N2WError::Attribute(msg.to_string())
}
fn key_error(msg: &str) -> N2WError {
N2WError::Key(msg.to_string())
}
fn splitbyx(n: &str, x: usize) -> Vec<u32> {
let length = n.len();
let mut out = Vec::new();
if length > x {
let start = length % x;
if start > 0 {
out.push(n[..start].parse::<u32>().unwrap_or(0));
}
let mut i = start;
while i < length {
out.push(n[i..(i + x).min(length)].parse::<u32>().unwrap_or(0));
i += x;
}
} else {
out.push(n.parse::<u32>().unwrap_or(0));
}
out
}
fn get_digits(n: u32) -> (usize, usize, usize) {
let s = format!("{:03}", n);
let chars: Vec<char> = s.chars().collect();
let last3 = &chars[chars.len().saturating_sub(3)..];
let d: Vec<usize> = last3
.iter()
.rev()
.map(|c| c.to_digit(10).unwrap_or(0) as usize)
.collect();
(d[0], d[1], d[2])
}
fn parse_bigint(s: &str) -> Result<BigInt> {
s.parse::<BigInt>()
.map_err(|_| N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s)))
}
fn python_number_str(v: &FloatValue) -> String {
match v {
FloatValue::Float { value, precision } => {
if v.has_visible_point() {
format!("{:.*}", *precision as usize, value)
} else {
python_float_exp_repr(*value)
}
}
FloatValue::Decimal { value, .. } => python_decimal_str(value),
}
}
fn python_float_exp_repr(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 s = format!("{:e}", v);
match s.split_once('e') {
Some((mant, exp)) => {
let e: i64 = exp.parse().unwrap_or(0);
let sign = if e < 0 { "-" } else { "+" };
format!("{}e{}{:02}", mant, sign, e.abs())
}
None => s,
}
}
fn feminine_kwarg(kw: &Kwargs) -> Result<bool> {
if !kw.only(&["feminine"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
match kw.get("feminine") {
Option::None | Some(KwVal::None) => Ok(false),
Some(KwVal::Bool(b)) => Ok(*b),
Some(_) => Err(N2WError::Fallback("kwargs".into())),
}
}
fn pluralize(number: u32, forms: (&'static str, &'static str, &'static str, bool)) -> &'static str {
let form = if number % 100 < 10 || number % 100 > 20 {
if number % 10 == 1 {
0
} else if number % 10 > 1 && number % 10 < 5 {
1
} else {
2
}
} else {
2
};
match form {
0 => forms.0,
1 => forms.1,
_ => forms.2,
}
}
fn int2word(number: &BigInt, feminine: bool) -> Result<String> {
if number.is_negative() {
return Ok(format!("{} {}", NEGWORD, int2word(&number.abs(), false)?));
}
if number.is_zero() {
return Ok(ZERO.to_string());
}
let mut words: Vec<&'static str> = Vec::new();
let digits = number.to_string();
let chunks = splitbyx(&digits, 3);
let mut chunk_len = chunks.len();
for chunk in &chunks {
chunk_len -= 1;
let (digit_right, digit_mid, digit_left) = get_digits(*chunk);
if digit_left > 0 {
words.push(HUNDREDS[digit_left]);
}
if digit_mid > 1 {
words.push(TWENTIES[digit_mid]);
}
if digit_mid == 1 {
words.push(TENS[digit_right]);
} else if digit_right > 0 {
if !(chunk_len > 0 && *chunk == 1) {
let is_feminine = feminine || scale(chunk_len)?.3;
words.push(if is_feminine {
ONES[digit_right].1
} else {
ONES[digit_right].0
});
}
}
if chunk_len > 0 && *chunk != 0 {
words.push(pluralize(*chunk, scale(chunk_len)?));
}
}
Ok(words.join(" "))
}
fn cardinal_float_str(value: &FloatValue, feminine: bool) -> Result<String> {
let n = python_number_str(value);
if let Some((left, right)) = {
let is_negative = n.starts_with('-');
let abs_n = if is_negative { &n[1..] } else { &n[..] };
abs_n
.split_once('.')
.map(|(l, r)| (l.to_string(), r.to_string()))
} {
let is_negative = n.starts_with('-');
let leading = right.chars().take_while(|&c| c == '0').count();
let left_int = parse_bigint(&left)?;
let right_int = parse_bigint(&right)?;
let mut decimal_part = String::new();
for _ in 0..leading {
decimal_part.push_str(ZERO);
decimal_part.push(' ');
}
decimal_part.push_str(&int2word(&right_int, feminine)?);
let body = format!(
"{} {} {}",
int2word(&left_int, feminine)?,
"запета",
decimal_part
);
Ok(if is_negative {
format!("{} {}", NEGWORD, body)
} else {
body
})
} else {
int2word(&parse_bigint(&n)?, feminine)
}
}
fn ordinal_word(value: &BigInt) -> Option<&'static str> {
let n = value.to_u32()?;
Some(match n {
1 => "први",
2 => "други",
3 => "трећи",
4 => "четврти",
5 => "пети",
6 => "шести",
7 => "седми",
8 => "осми",
9 => "девети",
10 => "десети",
11 => "једанаести",
12 => "дванаести",
13 => "тринаести",
14 => "четрнаести",
15 => "петнаести",
16 => "шеснаести",
17 => "седамнаести",
18 => "осамнаести",
19 => "деветнаести",
20 => "двадесети",
30 => "тридесети",
40 => "четрдесети",
50 => "педесети",
60 => "шездесети",
70 => "седамдесети",
80 => "осамдесети",
90 => "деведесети",
100 => "стоти",
1000 => "хиљадити",
_ => return None,
})
}
fn cyrl_to_latn(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'Љ' => out.push_str("Lj"),
'Њ' => out.push_str("Nj"),
'Џ' => out.push_str("Dž"),
'љ' => out.push_str("lj"),
'њ' => out.push_str("nj"),
'џ' => out.push_str("dž"),
'А' => out.push('A'),
'а' => out.push('a'),
'Б' => out.push('B'),
'б' => out.push('b'),
'В' => out.push('V'),
'в' => out.push('v'),
'Г' => out.push('G'),
'г' => out.push('g'),
'Д' => out.push('D'),
'д' => out.push('d'),
'Ђ' => out.push('Đ'),
'ђ' => out.push('đ'),
'Е' => out.push('E'),
'е' => out.push('e'),
'Ж' => out.push('Ž'),
'ж' => out.push('ž'),
'З' => out.push('Z'),
'з' => out.push('z'),
'И' => out.push('I'),
'и' => out.push('i'),
'Ј' => out.push('J'),
'ј' => out.push('j'),
'К' => out.push('K'),
'к' => out.push('k'),
'Л' => out.push('L'),
'л' => out.push('l'),
'М' => out.push('M'),
'м' => out.push('m'),
'Н' => out.push('N'),
'н' => out.push('n'),
'О' => out.push('O'),
'о' => out.push('o'),
'П' => out.push('P'),
'п' => out.push('p'),
'Р' => out.push('R'),
'р' => out.push('r'),
'С' => out.push('S'),
'с' => out.push('s'),
'Т' => out.push('T'),
'т' => out.push('t'),
'Ћ' => out.push('Ć'),
'ћ' => out.push('ć'),
'У' => out.push('U'),
'у' => out.push('u'),
'Ф' => out.push('F'),
'ф' => out.push('f'),
'Х' => out.push('H'),
'х' => out.push('h'),
'Ц' => out.push('C'),
'ц' => out.push('c'),
'Ч' => out.push('Č'),
'ч' => out.push('č'),
'Ш' => out.push('Š'),
'ш' => out.push('š'),
other => out.push(other),
}
}
out
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"RUB",
CurrencyForms::new(
&["rublja", "rublje", "rublji", "True"],
&["kopejka", "kopejke", "kopejki", "True"],
),
);
m.insert(
"EUR",
CurrencyForms::new(
&["evro", "evra", "evra", "False"],
&["cent", "centa", "centi", "False"],
),
);
m.insert(
"RSD",
CurrencyForms::new(
&["dinar", "dinara", "dinara", "False"],
&["para", "pare", "para", "True"],
),
);
m
}
pub struct LangSrLatn {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangSrLatn {
pub fn new() -> Self {
LangSrLatn {
currency_forms: build_currency_forms(),
}
}
}
impl Default for LangSrLatn {
fn default() -> Self {
LangSrLatn::new()
}
}
impl Lang for LangSrLatn {
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"запета"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
Ok(cyrl_to_latn(&int2word(value, false)?))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if let Some(word) = ordinal_word(value) {
return Ok(cyrl_to_latn(word));
}
let cardinal = self.to_cardinal(value)?;
Ok(cyrl_to_latn(&format!("{}{}", cardinal, "и")))
}
fn to_ordinal_num(&self, _value: &BigInt) -> Result<String> {
Err(attribute_error("'int' object has no attribute 'replace'"))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(cyrl_to_latn(&self.to_cardinal(value)?))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
Ok(cyrl_to_latn(&cardinal_float_str(value, false)?))
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
Ok(cyrl_to_latn(&cardinal_float_str(value, false)?))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let num = match value {
FloatValue::Float { value: f, .. } => {
if f.is_nan() {
return Ok("nan".to_string());
}
BigInt::from_f64(f.trunc()).ok_or_else(|| {
N2WError::Overflow("cannot convert float infinity to integer".into())
})?
}
FloatValue::Decimal { value: d, .. } => d.with_scale(0).as_bigint_and_exponent().0,
};
self.to_ordinal(&num)
}
fn ordinal_num_float_entry(&self, value: &FloatValue, _repr_str: &str) -> Result<String> {
let type_name = match value {
FloatValue::Float { .. } => "float",
FloatValue::Decimal { .. } => "decimal.Decimal",
};
Err(attribute_error(&format!(
"'{}' object has no attribute 'replace'",
type_name
)))
}
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: '{}Infinity'",
if negative { "-" } else { "" }
))),
other => Ok(other),
}
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
let feminine = feminine_kwarg(kw)?;
Ok(cyrl_to_latn(&int2word(value, feminine)?))
}
fn to_cardinal_float_kw(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
kw: &Kwargs,
) -> Result<String> {
let feminine = feminine_kwarg(kw)?;
Ok(cyrl_to_latn(&cardinal_float_str(value, feminine)?))
}
fn lang_name(&self) -> &str {
"Num2Word_SR_LATN"
}
fn default_currency(&self) -> &str {
"RSD"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let ten = BigInt::from(10);
let m100 = n.mod_floor(&BigInt::from(100));
let m10 = n.mod_floor(&ten);
let form = if m100 < ten || m100 > BigInt::from(20) {
if m10.is_one() {
0
} else if m10 > BigInt::one() && m10 < BigInt::from(5) {
1
} else {
2
}
} else {
2
};
forms
.get(form)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn cents_verbose(&self, number: &BigInt, currency: &str) -> Result<String> {
let forms = self
.currency_forms
.get(currency)
.ok_or_else(|| key_error(currency))?;
let feminine = forms.subunit.last().map(|s| s == "True").unwrap_or(false);
int2word(number, feminine)
}
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());
if let CurrencyValue::Int(v) = val {
let is_negative = v.is_negative();
let left = v.abs();
let mut words: Vec<String> = Vec::new();
if is_negative {
words.push("минус".to_string());
}
words.push(self.to_cardinal(&left)?);
let one_dinar = left.mod_floor(&BigInt::from(10)).is_one()
&& left.mod_floor(&BigInt::from(100)) != BigInt::from(11);
let unit = if one_dinar {
"динар"
} else {
"динара"
};
words.push(unit.to_string());
let _ = (currency, cents, separator, adjective);
return Ok(cyrl_to_latn(&words.join(" ")));
}
let out = default_to_currency(self, val, currency, cents, separator, adjective)?;
Ok(cyrl_to_latn(&out))
}
}