use crate::base::{KwVal, Kwargs, Lang, N2WError, Result};
use crate::currency::{parse_currency_parts, 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::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const MASK: &str = "д*";
const DEFAULT_CLAZZ: &str = "д";
const NEGWORD: &str = "минус";
const POINTWORD: &str = "запятая";
const DECIMALPOINT: &str = "а";
const CARDINALS: &[(u64, &[(&str, &str)])] = &[
(
0,
&[
("attr", "ноль"),
("abs", "ноль"),
("gen", "нолан"),
("dat", "нолана"),
("erg", "ноло"),
("instr", "ноланца"),
("mat", "ноланах"),
("comp", "ноланал"),
("all", "ноланга"),
],
),
(
1,
&[
("attr", "цхьа"),
("obl", "цхьана"),
("abs", "цхьаъ"),
("gen", "цхьаннан"),
("dat", "цхьанна"),
("erg", "цхьамма"),
("instr", "цхьаьнца"),
("mat", "цхьаннах"),
("comp", "цхьаннал"),
("all", "цхаьнга"),
("ORD", "цхьалгӀа"),
],
),
(
2,
&[
("attr", "ши"),
("obl", "шина"),
("abs", "шиъ"),
("gen", "шиннан"),
("dat", "шинна"),
("erg", "шимма"),
("instr", "шинца"),
("mat", "шиннах"),
("comp", "шиннал"),
("all", "шинга"),
("ORD", "шолгӀа"),
],
),
(
3,
&[
("attr", "кхо"),
("obl", "кхона"),
("abs", "кхоъ"),
("gen", "кхааннан"),
("dat", "кхаанна"),
("erg", "кхаамма"),
("instr", "кхаанца"),
("mat", "кхааннах"),
("comp", "кхааннал"),
("all", "кхаанга"),
("ORD", "кхоалгӀа"),
],
),
(
4,
&[
("attr", "д*и"),
("obl", "д*еа"),
("abs", "д*иъ"),
("gen", "д*еаннан"),
("dat", "д*еанна"),
("erg", "д*еамма"),
("instr", "д*еанца"),
("mat", "д*еаннах"),
("comp", "д*еаннал"),
("all", "д*еанга"),
("ORD", "д*оьалгӀа"),
],
),
(
5,
&[
("attr", "пхи"),
("obl", "пхеа"),
("abs", "пхиъ"),
("gen", "пхеаннан"),
("dat", "пхеанна"),
("erg", "пхеамма"),
("instr", "нхеанца"),
("mat", "пхеаннах"),
("comp", "пхеаннал"),
("all", "пхеанга"),
("ORD", "пхоьалгӀа"),
],
),
(6, &[("abs", "ялх"), ("attr", "ялх"), ("ORD", "йолхалгӀа")]),
(
7,
&[("abs", "ворхӀ"), ("attr", "ворхӀ"), ("ORD", "ворхӀалгӀа")],
),
(
8,
&[("abs", "бархӀ"), ("attr", "бархӀ"), ("ORD", "борхӀалӀа")],
),
(9, &[("abs", "исс"), ("attr", "исс"), ("ORD", "уьссалгӀа")]),
(
10,
&[
("attr", "итт"),
("abs", "итт"),
("gen", "иттаннан"),
("dat", "иттанна"),
("erg", "иттамма"),
("instr", "иттанца"),
("mat", "иттаннах"),
("comp", "иттаннал"),
("all", "иттанга"),
("ORD", "уьтталгӀа"),
],
),
(
11,
&[
("abs", "цхьайтта"),
("attr", "цхьайтта"),
("ORD", "цхьайтталгӀа"),
],
),
(
12,
&[("abs", "шийтта"), ("attr", "шийтта"), ("ORD", "шийтталга")],
),
(
13,
&[
("abs", "кхойтта"),
("attr", "кхойтта"),
("ORD", "кхойтталгӀа"),
],
),
(
14,
&[
("abs", "д*ейтта"),
("attr", "д*ейтта"),
("ORD", "д*ейтталгӀа"),
],
),
(
15,
&[
("abs", "пхийтта"),
("attr", "пхийтта"),
("ORD", "пхийтталгӀа"),
],
),
(
16,
&[
("abs", "ялхитта"),
("attr", "ялхитта"),
("ORD", "ялхитталгӀа"),
],
),
(
17,
&[
("abs", "вуьрхӀитта"),
("attr", "вуьрхӀитта"),
("ORD", "вуьрхӀитталгӀа"),
],
),
(
18,
&[
("abs", "берхӀитта"),
("attr", "берхӀитта"),
("ORD", "берхитталӀа"),
],
),
(
19,
&[
("abs", "ткъайесна"),
("attr", "ткъайесна"),
("ORD", "ткъаесналгӀа"),
],
),
(
20,
&[
("abs", "ткъа"),
("gen", "ткъаннан"),
("dat", "ткъанна"),
("erg", "ткъамма"),
("instr", "ткъанца"),
("mat", "ткъаннах"),
("comp", "ткъаннал"),
("all", "ткъанга"),
("attr", "ткъе"),
("ORD", "ткъолгӀа"),
],
),
(
40,
&[
("abs", "шовзткъа"),
("attr", "шовзткъе"),
("ORD", "шовзткъалгІа"),
],
),
(
60,
&[
("abs", "кхузткъа"),
("attr", "кхузткъе"),
("ORD", "кхузткъалгІа"),
],
),
(
80,
&[
("abs", "дезткъа"),
("attr", "дезткъе"),
("ORD", "дезткъалгІа"),
],
),
(
100,
&[
("attr", "бӀе"),
("abs", "бӀе"),
("obl", "бӀен"),
("gen", "бӀеннан"),
("dat", "бӀенна"),
("erg", "бӀемма"),
("instr", "бӀенца"),
("mat", "бӀеннах"),
("comp", "бӀеннал"),
("all", "бӀенга"),
("ORD", "бІолгІа"),
],
),
(
1000,
&[
("attr", "эзар"),
("abs", "эзар"),
("obl", "эзаран"),
("gen", "эзарнан"),
("dat", "эзарна"),
("erg", "эзарно"),
("instr", "эзарнаца"),
("mat", "эзарнах"),
("comp", "эзарнал"),
("all", "эзаранга"),
("ORD", "эзарлагІа"),
],
),
(
1000000,
&[
("attr", "миллион"),
("abs", "миллион"),
("ORD", "миллионалгІа"),
],
),
];
const CASESUFFIX_CONS: &[(&str, &str)] = &[
("gen", "аннан"),
("dat", "анна"),
("erg", "амма"),
("instr", "анца"),
("mat", "аннах"),
("comp", "аннал"),
("all", "анга"),
("obl", "ан"),
("ORD", "алгӀа"),
];
const CASESUFFIX_VOC: &[(&str, &str)] = &[
("gen", "ннан"),
("dat", "нна"),
("erg", "мма"),
("instr", "нца"),
("mat", "ннах"),
("comp", "ннал"),
("all", "нга"),
("obl", "н"),
("ORD", "лгӀа"),
];
const ILLIONS: &[(u32, &str)] = &[
(6, "миллион"),
(9, "миллиард"),
(12, "биллион"),
(15, "биллиард"),
(18, "триллион"),
(21, "триллиард"),
(24, "квадриллион"),
(27, "квадриллиард"),
(30, "квинтиллион"),
(33, "квинтиллиард"),
];
const POTS_DESC: [u32; 10] = [33, 30, 27, 24, 21, 18, 15, 12, 9, 6];
fn money_case(case: &str) -> &'static str {
if case != "abs" {
"obl"
} else {
"attr"
}
}
const MONEY_VERBOSE_ARITY_ERR: &str =
"_money_verbose() missing 1 required positional argument: 'case'";
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
[
(
"EUR",
CurrencyForms::new(&["Евро", "Евро"], &["Сент", "Сенташ"]),
),
(
"RUB",
CurrencyForms::new(&["Сом", "Сомаш"], &["Кепек", "Кепекаш"]),
),
(
"USD",
CurrencyForms::new(&["Доллар", "Доллараш"], &["Сент", "Сенташ"]),
),
(
"GBP",
CurrencyForms::new(&["Фунт", "Фунташ"], &["Пенни", "Пенни"]),
),
]
.into_iter()
.collect()
}
fn build_currency_adjectives() -> HashMap<&'static str, &'static str> {
[
("AUD", "Australian"),
("BYN", "Belarusian"),
("CAD", "Canadian"),
("EEK", "Estonian"),
("USD", "US"),
("RUB", "Russian"),
("NOK", "Norwegian"),
("MXN", "Mexican"),
("RON", "Romanian"),
("INR", "Indian"),
("HUF", "Hungarian"),
("ISK", "íslenskar"),
("UZS", "Uzbekistan"),
("SAR", "Saudi"),
("JPY", "Japanese"),
("KRW", "Korean"),
]
.into_iter()
.collect()
}
fn key_error(key: impl std::fmt::Display) -> N2WError {
N2WError::Key(format!("'{}'", key))
}
const VOWEL_A: char = 'а';
fn cardinals_entry(n: u64) -> Result<&'static [(&'static str, &'static str)]> {
CARDINALS
.iter()
.find(|(k, _)| *k == n)
.map(|(_, v)| *v)
.ok_or_else(|| key_error(n))
}
fn case_of(entry: &[(&'static str, &'static str)], case: &str) -> Option<&'static str> {
entry.iter().find(|(c, _)| *c == case).map(|(_, w)| *w)
}
fn cardinals_at(n: u64, case: &str) -> Result<&'static str> {
case_of(cardinals_entry(n)?, case).ok_or_else(|| key_error(case))
}
fn makecase(number: u64, case: &str, clazz: &str) -> Result<String> {
let entry = cardinals_entry(number)?;
if let Some(word) = case_of(entry, case) {
return Ok(word.replace(MASK, clazz));
}
let abs_form = case_of(entry, "abs").ok_or_else(|| key_error("abs"))?;
let is_vowel_stem = abs_form.chars().next_back() == Some(VOWEL_A);
let table = if is_vowel_stem {
CASESUFFIX_VOC
} else {
CASESUFFIX_CONS
};
let suffix = table
.iter()
.find(|(c, _)| *c == case)
.map(|(_, w)| *w)
.ok_or_else(|| key_error(case))?;
Ok(abs_form.replace(MASK, clazz) + suffix)
}
fn to_cardinal_small(number: u64, clazz: &str, case: &str) -> Result<String> {
if number < 20 {
return makecase(number, case, clazz);
}
if number < 100 {
let twens = number / 20;
let units = number % 20;
let base = twens * 20;
if units == 0 {
return makecase(number, case, clazz);
}
let twenties = makecase(base, "attr", clazz)?;
let rest = to_cardinal_small(units, clazz, case)?;
return Ok(twenties + " " + &rest.replace(MASK, clazz));
}
if number < 1000 {
let hundreds = number / 100;
let tens = number % 100;
let hundert = if hundreds > 1 {
cardinals_at(hundreds, "attr")?.replace(MASK, clazz) + " "
} else {
String::new()
};
if tens != 0 {
let rest = to_cardinal_small(tens, clazz, case)?;
return Ok(hundert + cardinals_at(100, "abs")? + " " + &rest);
}
return Ok(hundert + &makecase(100, case, clazz)?);
}
let thousands = number / 1000;
let hundert = number % 1000;
let tcase = if hundert > 0 { "attr" } else { case };
let tausend = if thousands > 1 {
let head = to_cardinal_small(thousands, clazz, "attr")?;
head + " " + cardinals_at(1000, tcase)?
} else {
makecase(1000, tcase, clazz)?
};
let rest = if hundert != 0 {
format!(" {}", to_cardinal_small(hundert, clazz, case)?)
} else {
String::new()
};
Ok(tausend + &rest)
}
fn pow10(n: u32) -> BigInt {
BigInt::from(10).pow(n)
}
fn python_repr(x: f64) -> String {
if x == 0.0 {
return "0.0".to_string();
}
let sci = format!("{:e}", x);
let (mant, exp_str) = match sci.split_once('e') {
Some(parts) => parts,
None => return sci.clone(),
};
let exp: i32 = exp_str.parse().unwrap_or(0);
let digits: String = mant.chars().filter(|c| c.is_ascii_digit()).collect();
let ndigits = digits.chars().count() as i32;
let decpt = exp + 1;
if decpt <= -4 || decpt > 16 {
let e = decpt - 1;
let mut it = digits.chars();
let mut s = String::new();
if let Some(first) = it.next() {
s.push(first);
}
let rest: String = it.collect();
if !rest.is_empty() {
s.push('.');
s.push_str(&rest);
}
s.push('e');
s.push(if e < 0 { '-' } else { '+' });
s.push_str(&format!("{:02}", e.abs()));
return s;
}
let prec = std::cmp::max(ndigits - decpt, 1) as usize;
format!("{:.*}", prec, x)
}
fn pow10_scale(n: i64) -> BigInt {
pow10(u32::try_from(n).unwrap_or(0))
}
fn dec_floordiv(a: &BigDecimal, b: &BigInt) -> BigDecimal {
let (coeff, scale) = a.as_bigint_and_exponent();
let (num, den) = if scale >= 0 {
(coeff, b * pow10_scale(scale))
} else {
(coeff * pow10_scale(-scale), b.clone())
};
BigDecimal::from(num / den)
}
fn dec_rem(a: &BigDecimal, b: &BigInt) -> BigDecimal {
let exact = a - BigDecimal::from(b.clone()) * dec_floordiv(a, b);
dec_fix(&exact)
}
const DECIMAL_CONTEXT_PREC: usize = 28;
fn round_half_even(n: &BigInt, drop: u32) -> BigInt {
let pow = pow10(drop);
let (q, r) = n.div_rem(&pow);
let twice: BigInt = &r * 2;
match twice.cmp(&pow) {
std::cmp::Ordering::Less => q,
std::cmp::Ordering::Greater => q + 1,
std::cmp::Ordering::Equal => {
if q.is_even() {
q
} else {
q + 1
}
}
}
}
fn dec_fix(v: &BigDecimal) -> BigDecimal {
let (coeff, scale) = v.as_bigint_and_exponent();
let ndigits = coeff.to_string().chars().count();
if ndigits <= DECIMAL_CONTEXT_PREC {
return BigDecimal::new(coeff, scale);
}
let drop = (ndigits - DECIMAL_CONTEXT_PREC) as u32;
let rounded = round_half_even(&coeff, drop);
let new_scale = scale - drop as i64;
if rounded.to_string().chars().count() > DECIMAL_CONTEXT_PREC {
return BigDecimal::new(rounded / 10, new_scale - 1);
}
BigDecimal::new(rounded, new_scale)
}
fn dec_abs(v: &BigDecimal) -> BigDecimal {
dec_fix(&v.abs())
}
fn decimal_repr(v: &BigDecimal) -> String {
let (coeff, scale) = v.as_bigint_and_exponent();
let sign = if coeff.is_negative() { "-" } else { "" };
let int_digits = coeff.abs().to_string();
let ndigits = int_digits.chars().count() as i64;
let exp = -scale;
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) as usize), int_digits),
)
} else if dotplace >= ndigits {
(
format!(
"{}{}",
int_digits,
"0".repeat((dotplace - ndigits) as usize)
),
String::new(),
)
} else {
let head: String = int_digits.chars().take(dotplace as usize).collect();
let tail: String = int_digits.chars().skip(dotplace as usize).collect();
(head, format!(".{}", tail))
};
let exp_part = if leftdigits == dotplace {
String::new()
} else {
let e = leftdigits - dotplace;
format!("E{}{}", if e < 0 { "-" } else { "+" }, e.abs())
};
format!("Decimal('{}{}{}{}')", sign, intpart, fracpart, exp_part)
}
fn cardinals_key_dec(n: &BigDecimal) -> Result<u64> {
CARDINALS
.iter()
.map(|(k, _)| *k)
.find(|k| BigDecimal::from(*k) == *n)
.ok_or_else(|| N2WError::Key(decimal_repr(n)))
}
fn makecase_dec(number: &BigDecimal, case: &str, clazz: &str) -> Result<String> {
makecase(cardinals_key_dec(number)?, case, clazz)
}
fn verify_ordinal(value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
fn kw_str<'a>(kw: &'a Kwargs, key: &str, default: &'a str) -> Result<&'a str> {
match kw.get(key) {
None => Ok(default),
Some(KwVal::Str(s)) => Ok(s.as_str()),
Some(_) => Err(N2WError::Fallback("kwargs".into())),
}
}
pub struct LangCe {
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_adjectives: HashMap<&'static str, &'static str>,
}
impl Default for LangCe {
fn default() -> Self {
Self::new()
}
}
impl LangCe {
pub fn new() -> Self {
LangCe {
currency_forms: build_currency_forms(),
currency_adjectives: build_currency_adjectives(),
}
}
fn cardinal_float(&self, number: f64) -> Result<String> {
let negative = number < 0.0;
let abs_number = number.abs();
if abs_number.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".into(),
));
}
if abs_number.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
let ipart = BigInt::from_f64(abs_number).ok_or_else(|| {
N2WError::Value(format!("cannot convert float {} to integer", abs_number))
})?;
let entires = self.cardinal(&ipart, DEFAULT_CLAZZ, "abs")?;
let repr = python_repr(abs_number);
let float_part = repr
.split_once('.')
.map(|(_, frac)| frac)
.ok_or_else(|| N2WError::Index("list index out of range".into()))?;
let mut postfix: Vec<String> = Vec::new();
for c in float_part.chars() {
let digit = c.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", c))
})?;
postfix.push(self.cardinal(&BigInt::from(digit), DEFAULT_CLAZZ, "abs")?);
}
let result = format!("{} {} {}", entires, DECIMALPOINT, postfix.join(" "));
if negative {
return Ok(format!("{} {}", NEGWORD, result));
}
Ok(result)
}
fn cardinal_decimal(&self, number: &BigDecimal, clazz: &str, case: &str) -> Result<String> {
if number.is_negative() {
let inner = self.cardinal_decimal(&dec_abs(number), clazz, case)?;
return Ok(format!("{} {}", NEGWORD, inner));
}
if *number < BigDecimal::from(20u64) {
return makecase_dec(number, case, clazz);
}
if *number < BigDecimal::from(100u64) {
let twenty = BigInt::from(20);
let twens = dec_floordiv(number, &twenty);
let units = dec_rem(number, &twenty);
let base = twens * BigDecimal::from(20u64);
if units.is_zero() {
return makecase_dec(number, case, clazz);
}
let twenties = makecase_dec(&base, "attr", clazz)?;
let rest = self.cardinal_decimal(&units, clazz, case)?;
return Ok(twenties + " " + &rest.replace(MASK, clazz));
}
if *number < BigDecimal::from(1000u64) {
let hundreds = dec_floordiv(number, &BigInt::from(100));
let tens = dec_rem(number, &BigInt::from(100));
let hundert = if hundreds > BigDecimal::one() {
cardinals_at(cardinals_key_dec(&hundreds)?, "attr")?.replace(MASK, clazz) + " "
} else {
String::new()
};
if !tens.is_zero() {
let rest = self.cardinal_decimal(&tens, clazz, case)?;
return Ok(hundert + cardinals_at(100, "abs")? + " " + &rest);
}
return Ok(hundert + &makecase(100, case, clazz)?);
}
if *number < BigDecimal::from(1_000_000u64) {
let thousands = dec_floordiv(number, &BigInt::from(1000));
let hundert = dec_rem(number, &BigInt::from(1000));
let tcase = if hundert > BigDecimal::zero() {
"attr"
} else {
case
};
let tausend = if thousands > BigDecimal::one() {
let head = self.cardinal_decimal(&thousands, clazz, "attr")?;
head + " " + cardinals_at(1000, tcase)?
} else {
makecase(1000, tcase, clazz)?
};
let rest = if !hundert.is_zero() {
format!(" {}", self.cardinal_decimal(&hundert, clazz, case)?)
} else {
String::new()
};
return Ok(tausend + &rest);
}
if *number < BigDecimal::from(pow10(34)) {
let mut out: Vec<String> = Vec::new();
for &pot in POTS_DESC.iter() {
let step = dec_rem(&dec_floordiv(number, &pow10(pot)), &BigInt::from(1000));
if step > BigDecimal::zero() {
let words = self.cardinal_decimal(&step, clazz, "attr")?;
let illion = ILLIONS
.iter()
.find(|(k, _)| *k == pot)
.map(|(_, w)| *w)
.ok_or_else(|| key_error(pot))?;
out.push(format!("{} {}", words, illion));
}
}
let rest = dec_rem(number, &pow10(6));
if !rest.is_zero() {
out.push(self.cardinal_decimal(&rest, clazz, case)?);
}
return Ok(out.join(" "));
}
Ok("NOT IMPLEMENTED".to_string())
}
fn cardinal(&self, number: &BigInt, clazz: &str, case: &str) -> Result<String> {
if number.is_negative() {
let inner = self.cardinal(&number.abs(), clazz, case)?;
return Ok(format!("{} {}", NEGWORD, inner));
}
let million = pow10(6);
if *number < million {
let n = number
.to_u64()
.ok_or_else(|| N2WError::Value("value below 10^6 must fit u64".into()))?;
return to_cardinal_small(n, clazz, case);
}
if *number < pow10(34) {
let mut out: Vec<String> = Vec::new();
for &pot in POTS_DESC.iter() {
let step = (number / pow10(pot)) % 1000u32;
if step > BigInt::zero() {
let s = step
.to_u64()
.ok_or_else(|| N2WError::Value("step below 1000 must fit u64".into()))?;
let words = to_cardinal_small(s, clazz, "attr")?;
let illion = ILLIONS
.iter()
.find(|(k, _)| *k == pot)
.map(|(_, w)| *w)
.ok_or_else(|| key_error(pot))?;
out.push(format!("{} {}", words, illion));
}
}
let rest = number % &million;
if !rest.is_zero() {
let r = rest
.to_u64()
.ok_or_else(|| N2WError::Value("remainder below 10^6 must fit u64".into()))?;
out.push(to_cardinal_small(r, clazz, case)?);
}
return Ok(out.join(" "));
}
Ok("NOT IMPLEMENTED".to_string())
}
}
impl Lang for LangCe {
fn python_maxval(&self) -> Option<num_bigint::BigInt> {
Some(num_bigint::BigInt::from(10u32).pow(606))
}
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> {
self.cardinal(value, DEFAULT_CLAZZ, "abs")
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value, .. } => self.cardinal_float(*value),
FloatValue::Decimal { value, .. } => self.cardinal_decimal(value, DEFAULT_CLAZZ, "abs"),
}
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
match value {
FloatValue::Float { value, .. } => self.cardinal_float(*value),
FloatValue::Decimal { value, .. } => self.cardinal_decimal(value, DEFAULT_CLAZZ, "ORD"),
}
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
let whole = value.as_whole_int().ok_or_else(|| {
N2WError::Type(format!("Cannot treat float {} as ordinal.", repr_str))
})?;
if whole.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
repr_str
)));
}
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> {
python_decimal_parse(s)
}
fn inf_result(&self, negative: bool, _to: &str) -> Result<String> {
if negative {
Ok(format!("{} NOT IMPLEMENTED", NEGWORD))
} else {
Ok("NOT IMPLEMENTED".to_string())
}
}
fn nan_result(&self, _to: &str) -> Result<String> {
Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.InvalidOperation'>]".into(),
})
}
fn neg_zero_decimal(&self, to: &str) -> Option<Result<String>> {
let zero = BigInt::zero();
match to {
"cardinal" | "year" => Some(self.cardinal(&zero, DEFAULT_CLAZZ, "abs")),
"ordinal" => Some(self.cardinal(&zero, DEFAULT_CLAZZ, "ORD")),
_ => None,
}
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["clazz", "case"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let clazz = kw_str(kw, "clazz", DEFAULT_CLAZZ)?;
let case = kw_str(kw, "case", "abs")?;
self.cardinal(value, clazz, case)
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["clazz"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let clazz = kw_str(kw, "clazz", DEFAULT_CLAZZ)?;
self.cardinal(value, clazz, "ORD")
}
fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["case"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let case = kw_str(kw, "case", "abs")?;
self.cardinal(value, DEFAULT_CLAZZ, case)
}
fn to_cardinal_float_kw(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
kw: &Kwargs,
) -> Result<String> {
if !kw.only(&["clazz", "case"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let clazz = kw_str(kw, "clazz", DEFAULT_CLAZZ)?;
let case = kw_str(kw, "case", "abs")?;
match value {
FloatValue::Float { value, .. } => self.cardinal_float(*value),
FloatValue::Decimal { value, .. } => self.cardinal_decimal(value, clazz, case),
}
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.cardinal(value, DEFAULT_CLAZZ, "ORD")
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
verify_ordinal(value)?;
Ok(format!("{}-й", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.cardinal(value, DEFAULT_CLAZZ, "abs")
}
fn lang_name(&self) -> &str {
"Num2Word_CE"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn currency_adjective(&self, code: &str) -> Option<&str> {
self.currency_adjectives.get(code).copied()
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let form = if n.is_one() { 0 } else { 1 };
forms
.get(form)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn money_verbose(&self, _number: &BigInt, _currency: &str) -> Result<String> {
Err(N2WError::Type(MONEY_VERBOSE_ARITY_ERR.into()))
}
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)))?;
self.cardinal_float(f)
}
fn cents_verbose(&self, _number: &BigInt, _currency: &str) -> Result<String> {
Err(N2WError::Type(
"_cents_verbose() missing 1 required positional argument: 'case'".into(),
))
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
) -> Result<String> {
self.currency_with_case(val, currency, cents, separator, adjective, "abs")
}
fn to_currency_kw(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
kw: &Kwargs,
) -> Result<String> {
if !kw.only(&["case"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let case = kw_str(kw, "case", "abs")?;
self.currency_with_case(val, currency, cents, separator, adjective, case)
}
}
impl LangCe {
fn currency_with_case(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
_adjective: bool,
case: &str,
) -> Result<String> {
let mcase = money_case(case);
let separator = separator.unwrap_or(self.default_separator());
let has_fractional_cents = match val {
CurrencyValue::Int(_) => false,
CurrencyValue::Decimal { value, .. } => {
let scaled = value * BigDecimal::from(100);
&scaled - scaled.with_scale(0) != BigDecimal::zero()
}
};
let (left, right, is_negative) =
parse_currency_parts(val, false, has_fractional_cents, 100);
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let index_err = || N2WError::Index("tuple index out of range".into());
let devise = forms.unit.first().ok_or_else(index_err)?;
let centime = forms.subunit.first().ok_or_else(index_err)?;
let minus_str = if is_negative {
format!("{} ", NEGWORD.trim())
} else {
String::new()
};
let money_str = self.cardinal(&left, DEFAULT_CLAZZ, mcase)?;
let cents_str = if has_fractional_cents {
if cents {
self.cardinal_from_decimal(&right)?
} else {
let f = right
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", right)))?;
format!("{}", f)
}
} else {
let right_int = right.as_bigint_and_exponent().0;
if cents {
self.cardinal(&right_int, DEFAULT_CLAZZ, mcase)?
} else {
self.cents_terse(&right_int, currency)?
}
};
Ok(format!(
"{}{} {}{} {} {}",
minus_str, money_str, devise, separator, cents_str, centime
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn show(r: Result<String>) -> String {
match r {
Ok(s) => s,
Err(N2WError::Key(m)) => format!("KeyError: {}", m),
Err(N2WError::Index(m)) => format!("IndexError: {}", m),
Err(N2WError::Value(m)) => format!("ValueError: {}", m),
Err(N2WError::Overflow(m)) => format!("OverflowError: {}", m),
Err(N2WError::Type(m)) => format!("TypeError: {}", m),
Err(N2WError::NotImplemented(m)) => format!("NotImplementedError: {}", m),
Err(e) => format!("{:?}", e),
}
}
enum E {
Words(&'static str),
Raises(&'static str),
}
use E::{Raises, Words};
impl E {
fn want(&self) -> &'static str {
match self {
Words(s) | Raises(s) => s,
}
}
}
#[test]
fn float_arm_matches_python() {
let cases: &[(u64, u32, E)] = &[
(0x0000000000000000_u64, 1, Words("ноль а ноль")), (0x3fe0000000000000_u64, 1, Words("ноль а пхиъ")), (0x3ff0000000000000_u64, 1, Words("цхьаъ а ноль")), (0x3ff8000000000000_u64, 1, Words("цхьаъ а пхиъ")), (0x4002000000000000_u64, 2, Words("шиъ а шиъ пхиъ")), (0x40091eb851eb851f_u64, 2, Words("кхоъ а цхьаъ диъ")), (0x3f847ae147ae147b_u64, 2, Words("ноль а ноль цхьаъ")), (0x3fb999999999999a_u64, 1, Words("ноль а цхьаъ")), (0x3fefae147ae147ae_u64, 2, Words("ноль а исс исс")), (0x3ff028f5c28f5c29_u64, 2, Words("цхьаъ а ноль цхьаъ")), (0x4028ae147ae147ae_u64, 2, Words("шийтта а кхоъ диъ")), (0x4058ff5c28f5c28f_u64, 2, Words("дезткъе ткъайесна а исс исс")), (0x4059200000000000_u64, 1, Words("бӀе а пхиъ")), (0x40934a3d70a3d70a_u64, 2, Words("эзар ши бӀе ткъе дейтта а пхиъ ялх")), (0xbfe0000000000000_u64, 1, Words("минус ноль а пхиъ")), (0xbff8000000000000_u64, 1, Words("минус цхьаъ а пхиъ")), (0xc028ae147ae147ae_u64, 2, Words("минус шийтта а кхоъ диъ")), (0x3ff0147ae147ae14_u64, 3, Words("цхьаъ а ноль ноль пхиъ")), (0x4005666666666666_u64, 3, Words("шиъ а ялх ворхӀ пхиъ")), (0x8000000000000000_u64, 1, Words("ноль а ноль")), (0xbf847ae147ae147b_u64, 2, Words("минус ноль а ноль цхьаъ")), (0x4132d687e3d70a3d_u64, 2, Words("цхьа миллион ши бӀе ткъе дейтта эзар пхи бӀе кхузткъе ворхӀ а бархӀ исс")), (0x42dc12218377de66_u64, 1, Words("бӀе ткъе кхо биллион ди бӀе шовзткъе ялхитта миллиард ворхӀ бӀе дезткъе исс миллион шийтта эзар кхо бӀе шовзткъе пхиъ а ялх")), (0x3f1a36e2eb1c432d_u64, 4, Words("ноль а ноль ноль ноль цхьаъ")), (0x430c6bf526340000_u64, 1, Words("цхьа биллиард а ноль")), (0x3fd3333333333334_u64, 17, Words("ноль а кхоъ ноль ноль ноль ноль ноль ноль ноль ноль ноль ноль ноль ноль ноль ноль ноль диъ")), (0x444b1ae4d6e2ef50_u64, 21, Raises("IndexError: list index out of range")), (0x4454542ba12a337c_u64, 20, Raises("ValueError: invalid literal for int() with base 10: 'e'")), (0x3ee4f8b588e368f1_u64, 5, Raises("IndexError: list index out of range")), (0x3eef75104d551d69_u64, 6, Raises("ValueError: invalid literal for int() with base 10: 'e'")), (0x4341c37937e08000_u64, 16, Raises("IndexError: list index out of range")), (0x0000000000000001_u64, 324, Raises("IndexError: list index out of range")), (0x46fed09bead87c03_u64, 34, Raises("IndexError: list index out of range")), (0x47071c74f0225d03_u64, 33, Raises("ValueError: invalid literal for int() with base 10: 'e'")), (0xc0934a3d70a3d70a_u64, 2, Words("минус эзар ши бӀе ткъе дейтта а пхиъ ялх")), (0x54b249ad2594c37d_u64, 100, Raises("IndexError: list index out of range")), (0x4034800000000000_u64, 1, Words("ткъа а пхиъ")), (0x4044000000000000_u64, 1, Words("шовзткъа а ноль")), (0x408f3ffdf3b645a2_u64, 3, Words("исс бӀе дезткъе ткъайесна а исс исс исс")), (0x43179085685d83c9_u64, 1, Words("цхьа биллиард ялх бӀе шовзткъе берхӀитта биллион ши бӀе ялх миллиард ворхӀ бӀе дезткъе миллион дезткъе бархӀ эзар пхи бӀе кхузткъе шиъ а шиъ")), (0x42eee708f58b7e34_u64, 2, Words("ши бӀе кхузткъе цхьайтта биллион бархӀ бӀе ткъе цхьа миллиард дезткъе шийтта миллион ворхӀ бӀе ворхӀ эзар кхо бӀе кхойтта а ялх шиъ")), ];
let l = LangCe::new();
for (bits, precision, want) in cases {
let value = f64::from_bits(*bits);
let v = FloatValue::Float {
value,
precision: *precision,
};
let got = show(l.to_cardinal_float(&v, None));
assert_eq!(got, want.want(), "float {:?} (bits {:#x})", value, bits);
}
}
#[test]
fn decimal_arm_matches_python() {
let cases: &[(&str, E)] = &[
("0.01", Raises("KeyError: Decimal('0.01')")),
("1.10", Raises("KeyError: Decimal('1.10')")),
("12.345", Raises("KeyError: Decimal('12.345')")),
("98746251323029.99", Raises("KeyError: Decimal('9.99')")),
("0.001", Raises("KeyError: Decimal('0.001')")),
("5", Words("пхиъ")),
("-12.34", Raises("KeyError: Decimal('12.34')")),
("80.00", Words("дезткъа")),
("0.0000001", Raises("KeyError: Decimal('1E-7')")),
("5.5", Raises("KeyError: Decimal('5.5')")),
("20.5", Raises("KeyError: Decimal('0.5')")),
("1E+2", Words("бӀе")),
("323029.99", Raises("KeyError: Decimal('9.99')")),
("29.99", Raises("KeyError: Decimal('9.99')")),
("-0.00", Words("ноль")),
("1e34", Words("NOT IMPLEMENTED")),
("0", Words("ноль")),
("19.999", Raises("KeyError: Decimal('19.999')")),
("1000000.5", Raises("KeyError: Decimal('0.5')")),
("999.5", Raises("KeyError: Decimal('19.5')")),
];
let l = LangCe::new();
for (s, want) in cases {
let value = BigDecimal::from_str(s).unwrap();
let v = FloatValue::Decimal {
value,
precision: 0,
};
let got = show(l.to_cardinal_float(&v, None));
assert_eq!(got, want.want(), "Decimal({:?})", s);
}
}
#[test]
fn precision_override_is_inert() {
let l = LangCe::new();
let v = FloatValue::Float {
value: 2.675,
precision: 3,
};
let base = show(l.to_cardinal_float(&v, None));
assert_eq!(base, "шиъ а ялх ворхӀ пхиъ");
for p in [0u32, 1, 3, 5, 17] {
assert_eq!(
show(l.to_cardinal_float(&v, Some(p))),
base,
"precision={}",
p
);
}
}
#[test]
fn fractional_cents_matches_python() {
let cases: &[(&str, &str, bool, E)] = &[
(
"1.011",
"USD",
true,
Words("цхьа Доллар, цхьаъ а цхьаъ Сент"),
),
(
"2.675",
"USD",
true,
Words("ши Доллар, кхузткъе ворхӀ а пхиъ Сент"),
),
("0.005", "USD", true, Words("ноль Доллар, ноль а пхиъ Сент")),
("1.011", "USD", false, Words("цхьа Доллар, 1.1 Сент")),
(
"12.34",
"USD",
true,
Words("шийтта Доллар, ткъе дейтта Сент"),
),
(
"1.0055",
"RUB",
true,
Words("цхьа Сом, ноль а пхиъ пхиъ Кепек"),
),
];
let l = LangCe::new();
for (val, code, cents, want) in cases {
let v = CurrencyValue::parse(val, false, true, true).unwrap();
let got = show(l.to_currency(&v, code, *cents, None, false));
assert_eq!(got, want.want(), "to_currency({}, {})", val, code);
}
}
#[test]
fn python_repr_matches_cpython() {
let cases: &[(f64, &str)] = &[
(0.0, "0.0"),
(0.5, "0.5"),
(2.675, "2.675"),
(1.005, "1.005"),
(100.5, "100.5"),
(1.0, "1.0"),
(0.0001, "0.0001"), (1e-5, "1e-05"), (1e15, "1000000000000000.0"), (1e16, "1e+16"), (1e21, "1e+21"),
(1.5e21, "1.5e+21"),
(1e100, "1e+100"),
(5e-324, "5e-324"),
(0.30000000000000004, "0.30000000000000004"),
(123456789012345.6, "123456789012345.6"),
(1.7976931348623157e308, "1.7976931348623157e+308"),
(1658206780088562.2, "1658206780088562.2"),
(271821092707313.62, "271821092707313.62"),
];
for (x, want) in cases {
assert_eq!(&python_repr(*x), want, "python_repr({:?})", x);
}
}
#[test]
fn decimal_repr_matches_cpython() {
let cases: &[(&str, &str)] = &[
("0.01", "Decimal('0.01')"),
("1.10", "Decimal('1.10')"),
("12.345", "Decimal('12.345')"),
("0.001", "Decimal('0.001')"),
("0.0000001", "Decimal('1E-7')"), ("0.000001", "Decimal('0.000001')"), ("1E+2", "Decimal('1E+2')"), ("5", "Decimal('5')"),
("0", "Decimal('0')"),
("-12.34", "Decimal('-12.34')"),
("80.00", "Decimal('80.00')"),
];
for (s, want) in cases {
let v = BigDecimal::from_str(s).unwrap();
assert_eq!(&decimal_repr(&v), want, "decimal_repr({:?})", s);
}
}
#[test]
fn integer_modes_unchanged() {
let l = LangCe::new();
assert_eq!(l.to_cardinal(&BigInt::from(0)).unwrap(), "ноль");
assert_eq!(
l.to_cardinal(&BigInt::from(99)).unwrap(),
"дезткъе ткъайесна"
);
assert_eq!(l.to_ordinal(&BigInt::from(100)).unwrap(), "бІолгІа");
assert_eq!(l.to_cardinal(&BigInt::from(100)).unwrap(), "бӀе");
assert_eq!(l.to_ordinal_num(&BigInt::from(7)).unwrap(), "7-й");
}
fn fl(v: f64) -> FloatValue {
FloatValue::Float {
value: v,
precision: 1,
}
}
fn dc(s: &str) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
}
}
#[test]
fn float_entry_hooks_match_corpus() {
let l = LangCe::new();
assert_eq!(show(l.cardinal_float_entry(&fl(1.0), None)), "цхьаъ а ноль");
assert_eq!(show(l.cardinal_float_entry(&fl(-0.0), None)), "ноль а ноль");
assert_eq!(
show(l.cardinal_float_entry(&fl(-1000000.0), None)),
"минус цхьа миллион а ноль"
);
assert_eq!(
show(l.cardinal_float_entry(&fl(1e16), None)),
"IndexError: list index out of range"
);
assert_eq!(show(l.ordinal_float_entry(&fl(5.0))), "пхиъ а ноль");
assert_eq!(show(l.ordinal_float_entry(&fl(-3.0))), "минус кхоъ а ноль");
assert_eq!(show(l.ordinal_float_entry(&dc("5.00"))), "пхоьалгӀа");
assert_eq!(show(l.ordinal_float_entry(&dc("0"))), "нольалгӀа");
assert_eq!(show(l.ordinal_float_entry(&dc("-0.0"))), "нольалгӀа");
assert_eq!(show(l.ordinal_float_entry(&dc("1E+2"))), "бІолгІа");
assert_eq!(show(l.ordinal_float_entry(&dc("-3.0"))), "минус кхоалгӀа");
assert_eq!(
show(l.ordinal_float_entry(&dc("12345.000"))),
"шийтта эзар кхо бӀе шовзткъе пхоьалгӀа"
);
assert_eq!(show(l.ordinal_float_entry(&dc("1E+20"))), "бӀе триллион");
assert_eq!(show(l.year_float_entry(&fl(2.5))), "шиъ а пхиъ");
assert_eq!(show(l.year_float_entry(&dc("-3.0"))), "минус кхоъ");
assert_eq!(show(l.ordinal_num_float_entry(&fl(5.0), "5.0")), "5.0-й");
assert_eq!(show(l.ordinal_num_float_entry(&fl(-0.0), "-0.0")), "-0.0-й");
assert_eq!(
show(l.ordinal_num_float_entry(&fl(1e16), "1e+16")),
"1e+16-й"
);
assert_eq!(
show(l.ordinal_num_float_entry(&fl(0.5), "0.5")),
"TypeError: Cannot treat float 0.5 as ordinal."
);
assert_eq!(
show(l.ordinal_num_float_entry(&fl(-3.0), "-3.0")),
"TypeError: Cannot treat negative num -3.0 as ordinal."
);
assert_eq!(
show(l.ordinal_num_float_entry(&dc("5.00"), "5.00")),
"5.00-й"
);
assert_eq!(
show(l.ordinal_num_float_entry(&dc("1.50"), "1.50")),
"TypeError: Cannot treat float 1.50 as ordinal."
);
}
#[test]
fn str_to_number_native_inf_nan() {
let l = LangCe::new();
assert!(matches!(
l.str_to_number("Infinity"),
Ok(ParsedNumber::Inf { negative: false })
));
assert!(matches!(
l.str_to_number("-Infinity"),
Ok(ParsedNumber::Inf { negative: true })
));
assert!(matches!(l.str_to_number("NaN"), Ok(ParsedNumber::NaN)));
assert!(matches!(l.str_to_number("5"), Ok(ParsedNumber::Dec(_))));
assert_eq!(show(l.inf_result(false, "cardinal")), "NOT IMPLEMENTED");
assert_eq!(
show(l.inf_result(true, "cardinal")),
"минус NOT IMPLEMENTED"
);
assert!(matches!(
l.nan_result("cardinal"),
Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
..
})
));
assert_eq!(show(l.neg_zero_decimal("cardinal").unwrap()), "ноль");
assert_eq!(show(l.neg_zero_decimal("ordinal").unwrap()), "нольалгӀа");
assert_eq!(show(l.neg_zero_decimal("year").unwrap()), "ноль");
assert!(l.neg_zero_decimal("ordinal_num").is_none());
}
#[test]
fn kwargs_hooks_match_corpus() {
let l = LangCe::new();
let kw = |pairs: &[(&str, &str)]| {
Kwargs(
pairs
.iter()
.map(|(k, v)| (k.to_string(), KwVal::Str(v.to_string())))
.collect(),
)
};
let n = BigInt::from(1234);
assert_eq!(
show(l.to_cardinal_kw(&n, &kw(&[("clazz", "б")]))),
"эзар ши бӀе ткъе бейтта"
);
assert_eq!(
show(l.to_cardinal_kw(&n, &kw(&[("clazz", "x")]))),
"эзар ши бӀе ткъе xейтта"
);
assert_eq!(
show(l.to_ordinal_kw(&n, &kw(&[("clazz", "й")]))),
"эзар ши бӀе ткъе йейтталгӀа"
);
assert_eq!(
show(l.to_cardinal_kw(&BigInt::from(1), &kw(&[("case", "attr")]))),
"цхьа"
);
assert_eq!(
show(l.to_cardinal_kw(&n, &kw(&[("case", "gen")]))),
"эзар ши бӀе ткъе дейттаннан"
);
assert_eq!(
show(l.to_year_kw(&BigInt::from(21), &kw(&[("case", "ORD")]))),
"ткъе цхьалгӀа"
);
assert_eq!(
show(l.to_cardinal_kw(&BigInt::from(5), &kw(&[("case", "bogus")]))),
"KeyError: 'bogus'"
);
assert!(matches!(
l.to_ordinal_kw(&n, &kw(&[("case", "gen")])),
Err(N2WError::Fallback(_))
));
assert!(matches!(
l.to_cardinal_kw(&n, &Kwargs(vec![("clazz".into(), KwVal::None)])),
Err(N2WError::Fallback(_))
));
let v = CurrencyValue::Int(BigInt::from(1234));
assert_eq!(
show(l.to_currency_kw(&v, "RUB", true, None, false, &kw(&[("case", "attr")]))),
"эзар ши бӀе ткъе дейттан Сом, нольан Кепек"
);
assert_eq!(
show(l.to_currency_kw(&v, "RUB", true, None, false, &Kwargs(vec![]))),
"эзар ши бӀе ткъе дейтта Сом, ноль Кепек"
);
assert_eq!(
show(l.to_cardinal_float_kw(&dc("21"), None, &kw(&[("case", "ORD")]))),
"ткъе цхьалгӀа"
);
assert_eq!(
show(l.to_cardinal_float_kw(&fl(1.5), None, &kw(&[("case", "ORD")]))),
"цхьаъ а пхиъ"
);
}
}