use crate::base::{Kwargs, KwVal, 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::{FromPrimitive, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
#[derive(Clone)]
enum G {
S(String),
A(String, String),
}
#[derive(Clone)]
enum C {
S(String),
G([G; 4]),
}
type Classifier = [C; 6];
#[derive(Clone, Copy)]
enum GenderArg<'a> {
Idx(usize),
Bad(&'a KwVal),
}
#[derive(Clone, Copy)]
struct Opts<'a> {
case: usize,
plural: bool,
gender: GenderArg<'a>,
animate: bool,
}
impl Default for Opts<'_> {
fn default() -> Self {
Opts { case: 0, plural: false, gender: GenderArg::Idx(0), animate: true }
}
}
const CASE_N: usize = 0;
const CASE_G: usize = 1;
const GENDER_M: usize = 0;
const GENDER_F: usize = 1;
const GENDER_NEUT: usize = 2;
fn case_classifier_element(cl: &Classifier, o: Opts<'_>) -> Result<String> {
Ok(match &cl[o.case] {
C::S(text) => text.clone(),
C::G(genders) => {
let idx = if o.plural {
3
} else {
match o.gender {
GenderArg::Idx(i) => i,
GenderArg::Bad(kv) => return Err(dict_lookup_error(kv)),
}
};
match &genders[idx] {
G::S(text) => text.clone(),
G::A(animate, inanimate) => {
if o.animate {
animate.clone()
} else {
inanimate.clone()
}
}
}
}
})
}
fn dict_lookup_error(kv: &KwVal) -> N2WError {
match kv {
KwVal::Str(s) => N2WError::Key(s.clone()),
KwVal::None => N2WError::Key("None".into()),
KwVal::Bool(b) => N2WError::Key(if *b { "True" } else { "False" }.into()),
KwVal::Int(i) => N2WError::Key(i.to_string()),
KwVal::List(_) => N2WError::Type("unhashable type: 'list'".into()),
}
}
struct Table(Vec<Option<Classifier>>);
impl Table {
fn build(entries: Vec<(usize, Classifier)>) -> Table {
let max = entries.iter().map(|(k, _)| *k).max().unwrap_or(0);
let mut v: Vec<Option<Classifier>> = (0..=max).map(|_| None).collect();
for (k, c) in entries {
v[k] = Some(c);
}
Table(v)
}
}
fn get_num_element(t: &Table, num: usize, o: Opts<'_>) -> Result<String> {
match t.0.get(num).and_then(|x| x.as_ref()) {
Some(cl) => case_classifier_element(cl, o),
None => Err(N2WError::Key(num.to_string())),
}
}
fn case_index(s: &str) -> Option<usize> {
Some(match s {
"n" | "nominative" | "и" | "именительный" => 0,
"g" | "genitive" | "р" | "родительный" => 1,
"d" | "dative" | "д" | "дательный" => 2,
"a" | "accusative" | "в" | "винительный" => 3,
"i" | "instrumental" | "т" | "творительный" => 4,
"p" | "prepositional" | "п" | "предложный" => 5,
_ => return None,
})
}
fn gender_index(s: &str) -> Option<usize> {
Some(match s {
"m" | "masculine" | "м" | "мужской" => 0,
"f" | "feminine" | "ж" | "женский" => 1,
"n" | "neuter" | "с" | "средний" => 2,
"p" | "plural" => 3,
_ => return None,
})
}
fn kw_truthy(kv: Option<&KwVal>, default: bool) -> bool {
match kv {
None => default,
Some(KwVal::Bool(b)) => *b,
Some(KwVal::Int(i)) => *i != 0,
Some(KwVal::Str(s)) => !s.is_empty(),
Some(KwVal::List(l)) => !l.is_empty(),
Some(KwVal::None) => false,
}
}
fn all6(w: [&str; 6]) -> Classifier {
[
C::S(w[0].into()),
C::S(w[1].into()),
C::S(w[2].into()),
C::S(w[3].into()),
C::S(w[4].into()),
C::S(w[5].into()),
]
}
fn g4(m: &str, f: &str, n: &str, p: &str) -> C {
C::G([G::S(m.into()), G::S(f.into()), G::S(n.into()), G::S(p.into())])
}
fn get_cases(prefix: &str, post_group: usize) -> Classifier {
let p = |suf: &str| format!("{}{}", prefix, suf);
let d = |a: &str, b: &str| if post_group == 0 { p(a) } else { p(b) };
[
C::G([G::S(d("ой", "ый")), G::S(p("ая")), G::S(p("ое")), G::S(p("ые"))]),
C::G([G::S(p("ого")), G::S(p("ой")), G::S(p("ого")), G::S(p("ых"))]),
C::G([G::S(p("ому")), G::S(p("ой")), G::S(p("ому")), G::S(p("ым"))]),
C::G([
G::A(p("ого"), d("ой", "ый")),
G::S(p("ую")),
G::S(p("ое")),
G::A(p("ых"), p("ые")),
]),
C::G([G::S(p("ым")), G::S(p("ой")), G::S(p("ым")), G::S(p("ыми"))]),
C::G([G::S(p("ом")), G::S(p("ой")), G::S(p("ом")), G::S(p("ых"))]),
]
}
fn ord_classifier(prefixes: &[(usize, &str, usize)]) -> Vec<(usize, Classifier)> {
prefixes.iter().map(|(n, p, pg)| (*n, get_cases(p, *pg))).collect()
}
const HUNDREDS_RAW: [(usize, [&str; 6]); 9] = [
(1, ["сто", "ста", "ста", "сто", "ста", "ста"]),
(2, ["двести", "двухсот", "двумстам", "двести", "двумястами", "двухстах"]),
(3, ["триста", "трёхсот", "трёмстам", "триста", "тремястами", "трёхстах"]),
(
4,
["четыреста", "четырёхсот", "четырёмстам", "четыреста", "четырьмястами", "четырёхстах"],
),
(5, ["пятьсот", "пятисот", "пятистам", "пятьсот", "пятьюстами", "пятистах"]),
(6, ["шестьсот", "шестисот", "шестистам", "шестьсот", "шестьюстами", "шестистах"]),
(7, ["семьсот", "семисот", "семистам", "семьсот", "семьюстами", "семистах"]),
(
8,
["восемьсот", "восьмисот", "восьмистам", "восемьсот", "восемьюстами", "восьмистах"],
),
(
9,
["девятьсот", "девятисот", "девятистам", "девятьсот", "девятьюстами", "девятистах"],
),
];
const TENS_PREFIXES: [(usize, &str); 9] = [
(1, "один"),
(2, "две"),
(3, "три"),
(4, "четыр"),
(5, "пят"),
(6, "шест"),
(7, "сем"),
(8, "восем"),
(9, "девят"),
];
const TENS_POSTFIXES: [&str; 6] =
["надцать", "надцати", "надцати", "надцать", "надцатью", "надцати"];
const THOUSANDS_PREFIXES: [(usize, &str); 9] = [
(2, "миллион"),
(3, "миллиард"),
(4, "триллион"),
(5, "квадриллион"),
(6, "квинтиллион"),
(7, "секстиллион"),
(8, "септиллион"),
(9, "октиллион"),
(10, "нониллион"),
];
const THOUSANDS_POSTFIXES: [[&str; 3]; 6] = [
["", "а", "ов"],
["а", "ов", "ов"],
["у", "ам", "ам"],
["", "а", "ов"],
["ом", "ами", "ами"],
["е", "ах", "ах"],
];
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const KOPEKS: [&str; 3] = ["копейка", "копейки", "копеек"];
const CENTS: [&str; 3] = ["цент", "цента", "центов"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("RUB", CurrencyForms::new(&["рубль", "рубля", "рублей"], &KOPEKS));
m.insert("EUR", CurrencyForms::new(&["евро", "евро", "евро"], &CENTS));
m.insert("USD", CurrencyForms::new(&["доллар", "доллара", "долларов"], &CENTS));
m.insert("UAH", CurrencyForms::new(&["гривна", "гривны", "гривен"], &KOPEKS));
m.insert(
"KZT",
CurrencyForms::new(&["тенге", "тенге", "тенге"], &["тиын", "тиына", "тиынов"]),
);
m.insert(
"BYN",
CurrencyForms::new(
&["белорусский рубль", "белорусских рубля", "белорусских рублей"],
&KOPEKS,
),
);
m.insert(
"UZS",
CurrencyForms::new(&["сум", "сума", "сумов"], &["тийин", "тийина", "тийинов"]),
);
m.insert(
"PLN",
CurrencyForms::new(
&["польский злотый", "польских слотых", "польских злотых"],
&["грош", "гроша", "грошей"],
),
);
m.insert(
"GBP",
CurrencyForms::new(
&["фунт стерлингов", "фунта стерлингов", "фунтов стерлингов"],
&["пенни", "пенни", "пенни"],
),
);
m
}
pub struct LangRu {
ones: Table,
ones_ord: Table,
tens: Table,
tens_ord: Table,
twenties: Table,
twenties_ord: Table,
hundreds: Table,
hundreds_ord: Table,
thousands: Vec<Option<[[String; 3]; 6]>>,
thousands_ord: Table,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangRu {
fn default() -> Self {
Self::new()
}
}
impl LangRu {
pub fn new() -> Self {
let ones = Table::build(vec![
(0, all6(["ноль", "ноля", "нолю", "ноль", "нолём", "ноле"])),
(
1,
[
g4("один", "одна", "одно", "одни"),
g4("одного", "одной", "одного", "одних"),
g4("одному", "одной", "одному", "одним"),
C::G([
G::A("одного".into(), "один".into()),
G::S("одну".into()),
G::S("одно".into()),
G::A("одних".into(), "одни".into()),
]),
g4("одним", "одной", "одним", "одними"),
g4("одном", "одной", "одном", "одних"),
],
),
(
2,
[
g4("два", "две", "два", "двое"),
g4("двух", "двух", "двух", "двоих"),
g4("двум", "двум", "двум", "двоим"),
C::G([
G::A("двух".into(), "два".into()),
G::A("двух".into(), "две".into()),
G::S("два".into()),
G::S("двоих".into()),
]),
g4("двумя", "двумя", "двумя", "двоими"),
g4("двух", "двух", "двух", "двоих"),
],
),
(
3,
[
g4("три", "три", "три", "трое"),
g4("трёх", "трёх", "трёх", "троих"),
g4("трём", "трём", "трём", "троим"),
C::G([
G::A("трёх".into(), "три".into()),
G::A("трёх".into(), "три".into()),
G::S("три".into()),
G::S("троих".into()),
]),
g4("тремя", "тремя", "тремя", "троими"),
g4("трёх", "трёх", "трёх", "троих"),
],
),
(
4,
[
g4("четыре", "четыре", "четыре", "четверо"),
g4("четырёх", "четырёх", "четырёх", "четверых"),
g4("четырём", "четырём", "четырём", "четверым"),
C::G([
G::A("четырёх".into(), "четыре".into()),
G::A("четырёх".into(), "четыре".into()),
G::S("четыре".into()),
G::S("четверых".into()),
]),
g4("четырьмя", "четырьмя", "четырьмя", "четверыми"),
g4("четырёх", "четырёх", "четырёх", "четверых"),
],
),
(5, all6(["пять", "пяти", "пяти", "пять", "пятью", "пяти"])),
(6, all6(["шесть", "шести", "шести", "шесть", "шестью", "шести"])),
(7, all6(["семь", "семи", "семи", "семь", "семью", "семи"])),
(8, all6(["восемь", "восьми", "восьми", "восемь", "восемью", "восьми"])),
(9, all6(["девять", "девяти", "девяти", "девять", "девятью", "девяти"])),
]);
let mut ones_ord_entries = vec![(
3,
[
g4("третий", "третья", "третье", "третьи"),
g4("третьего", "третьей", "третьего", "третьих"),
g4("третьему", "третьей", "третьему", "третьим"),
C::G([
G::A("третьего".into(), "третий".into()),
G::S("третью".into()),
G::S("третье".into()),
G::A("третьих".into(), "третьи".into()),
]),
g4("третьим", "третьей", "третьим", "третьими"),
g4("третьем", "третьей", "третьем", "третьих"),
],
)];
ones_ord_entries.extend(ord_classifier(&[
(0, "нулев", 0),
(1, "перв", 1),
(2, "втор", 0),
(4, "четвёрт", 1),
(5, "пят", 1),
(6, "шест", 0),
(7, "седьм", 0),
(8, "восьм", 0),
(9, "девят", 1),
]));
let ones_ord = Table::build(ones_ord_entries);
let mut tens_entries =
vec![(0, all6(["десять", "десяти", "десяти", "десять", "десятью", "десяти"]))];
for (num, prefix) in TENS_PREFIXES {
let w: Vec<String> =
TENS_POSTFIXES.iter().map(|s| format!("{}{}", prefix, s)).collect();
tens_entries.push((
num,
all6([&w[0], &w[1], &w[2], &w[3], &w[4], &w[5]]),
));
}
let tens = Table::build(tens_entries);
let mut tens_ord_prefixes: Vec<(usize, String, usize)> = vec![(0, "десят".into(), 1)];
tens_ord_prefixes
.extend(TENS_PREFIXES.iter().map(|(n, p)| (*n, format!("{}надцат", p), 1)));
let tens_ord = Table::build(
tens_ord_prefixes.iter().map(|(n, p, pg)| (*n, get_cases(p, *pg))).collect(),
);
let twenties = Table::build(vec![
(2, all6(["двадцать", "двадцати", "двадцати", "двадцать", "двадцатью", "двадцати"])),
(3, all6(["тридцать", "тридцати", "тридцати", "тридцать", "тридцатью", "тридцати"])),
(4, all6(["сорок", "сорока", "сорока", "сорок", "сорока", "сорока"])),
(
5,
all6([
"пятьдесят",
"пятидесяти",
"пятидесяти",
"пятьдесят",
"пятьюдесятью",
"пятидесяти",
]),
),
(
6,
all6([
"шестьдесят",
"шестидесяти",
"шестидесяти",
"шестьдесят",
"шестьюдесятью",
"шестидесяти",
]),
),
(
7,
all6([
"семьдесят",
"семидесяти",
"семидесяти",
"семьдесят",
"семьюдесятью",
"семидесяти",
]),
),
(
8,
all6([
"восемьдесят",
"восьмидесяти",
"восьмидесяти",
"восемьдесят",
"восемьюдесятью",
"восьмидесяти",
]),
),
(
9,
all6(["девяносто", "девяноста", "девяноста", "девяносто", "девяноста", "девяноста"]),
),
]);
let twenties_ord = Table::build(ord_classifier(&[
(2, "двадцат", 1),
(3, "тридцат", 1),
(4, "сороков", 0),
(5, "пятидесят", 1),
(6, "шестидесят", 1),
(7, "семидесят", 1),
(8, "восьмидесят", 1),
(9, "девяност", 1),
]));
let hundreds =
Table::build(HUNDREDS_RAW.iter().map(|(n, w)| (*n, all6(*w))).collect());
let hundreds_ord = Table::build(
HUNDREDS_RAW
.iter()
.map(|(n, w)| {
let prefix = if *n != 1 { w[1] } else { "сот" };
(*n, get_cases(prefix, 1))
})
.collect(),
);
let mut thousands: Vec<Option<[[String; 3]; 6]>> = (0..=10).map(|_| None).collect();
thousands[1] = Some([
["тысяча".into(), "тысячи".into(), "тысяч".into()],
["тысячи".into(), "тысяч".into(), "тысяч".into()],
["тысяче".into(), "тысячам".into(), "тысячам".into()],
["тысячу".into(), "тысячи".into(), "тысяч".into()],
["тысячей".into(), "тысячами".into(), "тысячами".into()],
["тысяче".into(), "тысячах".into(), "тысячах".into()],
]);
for (num, prefix) in THOUSANDS_PREFIXES {
let mk = |case: usize| -> [String; 3] {
let c = THOUSANDS_POSTFIXES[case];
[
format!("{}{}", prefix, c[0]),
format!("{}{}", prefix, c[1]),
format!("{}{}", prefix, c[2]),
]
};
thousands[num] = Some([mk(0), mk(1), mk(2), mk(3), mk(4), mk(5)]);
}
let mut thousands_ord_prefixes: Vec<(usize, String, usize)> =
vec![(1, "тысячн".into(), 1)];
thousands_ord_prefixes
.extend(THOUSANDS_PREFIXES.iter().map(|(n, p)| (*n, format!("{}н", p), 1)));
let thousands_ord = Table::build(
thousands_ord_prefixes.iter().map(|(n, p, pg)| (*n, get_cases(p, *pg))).collect(),
);
LangRu {
ones,
ones_ord,
tens,
tens_ord,
twenties,
twenties_ord,
hundreds,
hundreds_ord,
thousands,
thousands_ord,
currency_forms: build_currency_forms(),
}
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
fn opts_from_kwargs<'a>(&self, kw: &'a Kwargs) -> Result<Opts<'a>> {
let case = match kw.get("case") {
None => 0,
Some(KwVal::Str(s)) => match case_index(s) {
Some(i) => i,
None => return Err(dict_lookup_error(kw.get("case").unwrap())),
},
Some(other) => return Err(dict_lookup_error(other)),
};
let gender = match kw.get("gender") {
None => GenderArg::Idx(0),
Some(KwVal::Str(s)) => match gender_index(s) {
Some(i) => GenderArg::Idx(i),
None => GenderArg::Bad(kw.get("gender").unwrap()),
},
Some(other) => GenderArg::Bad(other),
};
Ok(Opts {
case,
plural: kw_truthy(kw.get("plural"), false),
gender,
animate: kw_truthy(kw.get("animate"), true),
})
}
fn thousands_elements(&self, num: usize, case: usize) -> Result<&[String; 3]> {
match self.thousands.get(num).and_then(|x| x.as_ref()) {
Some(cases) => Ok(&cases[case]),
None => Err(N2WError::Key(num.to_string())),
}
}
fn pluralize(n: u32, forms: &[String; 3]) -> String {
if matches!(n % 100, 11 | 12 | 13 | 14) {
return forms[2].clone();
}
if n % 10 == 1 {
return forms[0].clone();
}
if matches!(n % 10, 2 | 3 | 4) {
return forms[1].clone();
}
forms[2].clone()
}
fn int2word(&self, n: &BigInt, cardinal: bool, o: Opts<'_>) -> Result<String> {
if n.is_negative() {
let inner = self.int2word(&n.abs(), cardinal, o)?;
return Ok(format!("{} {}", self.negword(), inner));
}
if n.is_zero() {
let t = if cardinal { &self.ones } else { &self.ones_ord };
return get_num_element(t, 0, o);
}
let digits = n.to_string();
let chunks = splitbyx(&digits, 3);
let ord_join = *chunks.last().unwrap() == 0;
let len = chunks.len();
let rightmost_nonzero = chunks.iter().rposition(|&e| e != 0).unwrap();
let rightest_nonzero_chunk_i = len - 1 - rightmost_nonzero;
let mut words: Vec<String> = Vec::new();
for (j, &x) in chunks.iter().enumerate() {
let i = len - 1 - j;
if x == 0 {
continue;
}
let (n1, n2, n3) = get_digits(x);
let mut chunk_words: Vec<String>;
if cardinal {
chunk_words = self.chunk_cardinal(n3, n2, n1, i, o)?;
if i > 0 {
let forms = self.thousands_elements(i, o.case)?;
chunk_words.push(Self::pluralize(x, forms));
}
} else if !(ord_join && rightest_nonzero_chunk_i == i) {
chunk_words = self.chunk_ordinal(n3, n2, n1, i, o)?;
if i > 0 {
let t_case = if rightest_nonzero_chunk_i == i { o.case } else { CASE_N };
let forms = self.thousands_elements(i, t_case)?;
chunk_words.push(Self::pluralize(x, forms));
}
} else {
chunk_words = self.chunk_ordinal_join(n3, n2, n1, i, o)?;
if i > 0 {
chunk_words.push(get_num_element(&self.thousands_ord, i, o)?);
}
chunk_words = vec![chunk_words.concat()];
}
words.extend(chunk_words);
}
Ok(words.join(" "))
}
fn chunk_cardinal(
&self,
hundreds: u32,
tens: u32,
ones: u32,
chunk_num: usize,
o: Opts<'_>,
) -> Result<Vec<String>> {
let mut words = Vec::new();
if hundreds > 0 {
words.push(get_num_element(&self.hundreds, hundreds as usize, o)?);
}
if tens > 1 {
words.push(get_num_element(&self.twenties, tens as usize, o)?);
}
if tens == 1 {
words.push(get_num_element(&self.tens, ones as usize, o)?);
} else if ones > 0 {
let w_ones = if chunk_num == 1 {
let mut f = o;
f.gender = GenderArg::Idx(GENDER_F);
get_num_element(&self.ones, ones as usize, f)?
} else {
get_num_element(&self.ones, ones as usize, o)?
};
words.push(w_ones);
}
Ok(words)
}
fn chunk_ordinal(
&self,
hundreds: u32,
tens: u32,
ones: u32,
chunk_num: usize,
o: Opts<'_>,
) -> Result<Vec<String>> {
let d = Opts::default();
let mut words = Vec::new();
if hundreds > 0 {
if tens == 0 && ones == 0 {
words.push(get_num_element(&self.hundreds_ord, hundreds as usize, o)?);
} else {
words.push(get_num_element(&self.hundreds, hundreds as usize, d)?);
}
}
if tens > 1 {
if ones == 0 {
words.push(get_num_element(&self.twenties_ord, tens as usize, o)?);
} else {
words.push(get_num_element(&self.twenties, tens as usize, d)?);
}
}
if tens == 1 {
words.push(get_num_element(&self.tens_ord, ones as usize, o)?);
} else if ones > 0 {
let w_ones: Option<String> = if chunk_num == 0 {
Some(get_num_element(&self.ones_ord, ones as usize, o)?)
} else if chunk_num > 0 && ones == 1 && hundreds == 0 && tens == 0 {
None
} else if chunk_num == 1 {
let f = Opts { gender: GenderArg::Idx(GENDER_F), ..Opts::default() };
Some(get_num_element(&self.ones, ones as usize, f)?)
} else {
Some(get_num_element(&self.ones, ones as usize, d)?)
};
if let Some(w) = w_ones {
if !w.is_empty() {
words.push(w);
}
}
}
Ok(words)
}
fn chunk_ordinal_join(
&self,
hundreds: u32,
tens: u32,
ones: u32,
chunk_num: usize,
o: Opts<'_>,
) -> Result<Vec<String>> {
let d = Opts::default();
let g = Opts { case: CASE_G, ..Opts::default() };
let mut words = Vec::new();
if hundreds > 1 {
words.push(get_num_element(&self.hundreds, hundreds as usize, g)?);
} else if hundreds == 1 {
words.push(get_num_element(&self.hundreds, hundreds as usize, d)?);
}
if tens == 9 {
words.push(get_num_element(&self.twenties, tens as usize, d)?);
} else if tens > 1 {
words.push(get_num_element(&self.twenties, tens as usize, g)?);
}
if tens == 1 {
words.push(get_num_element(&self.tens, ones as usize, g)?);
} else if ones > 0 {
let w_ones: Option<String> = if chunk_num == 0 {
Some(get_num_element(&self.ones_ord, ones as usize, o)?)
} else if chunk_num > 0 && ones == 1 && tens != 1 {
if tens == 0 && hundreds == 0 {
None
} else {
let neut = Opts { gender: GenderArg::Idx(GENDER_NEUT), ..Opts::default() };
Some(get_num_element(&self.ones, 1, neut)?)
}
} else {
Some(get_num_element(&self.ones, ones as usize, g)?)
};
if let Some(w) = w_ones {
if !w.is_empty() {
words.push(w);
}
}
}
Ok(words)
}
fn cardinal_from_str(&self, number: &str) -> Result<String> {
let n = number.replace(',', ".");
if let Some(_dot) = n.find('.') {
let is_negative = n.starts_with('-');
let abs_n: &str = if is_negative { &n[1..] } else { &n };
let dotpos = abs_n.find('.').unwrap();
let left = &abs_n[..dotpos];
let right = &abs_n[dotpos + 1..];
let int_left = parse_int(left)?;
let int_right = parse_int(right)?;
let fem = Opts { gender: GenderArg::Idx(GENDER_F), ..Opts::default() };
let left_word = self.int2word(&int_left, true, fem)?;
let pointword: [String; 3] =
["целая".into(), "целых".into(), "целых".into()];
let celaya = self.pluralize(&int_left, &pointword)?;
let decimal_part = self.int2word(&int_right, true, fem)?;
let bitness = self.decimal_bitness(right)?;
let result =
format!("{} {} {} {}", left_word, celaya, decimal_part, bitness);
if is_negative {
Ok(format!("{} {}", self.negword(), result))
} else {
Ok(result)
}
} else {
if n.to_lowercase().contains('e') {
let f: f64 = n.parse().map_err(|_| {
N2WError::Value(format!(
"could not convert string to float: '{}'",
n
))
})?;
let bi = BigInt::from_f64(f.trunc()).ok_or_else(|| {
N2WError::Value(format!(
"cannot convert float {} to integer",
f
))
})?;
self.int2word(&bi, true, Opts::default())
} else {
self.int2word(&parse_int(&n)?, true, Opts::default())
}
}
}
fn decimal_bitness(&self, right: &str) -> Result<String> {
let chars: Vec<char> = right.chars().collect();
let last = *chars.last().ok_or_else(|| {
N2WError::Index("string index out of range".into())
})?;
let last2: String = if chars.len() >= 2 {
chars[chars.len() - 2..].iter().collect()
} else {
right.to_string()
};
let ten_pow = BigInt::from(10).pow(chars.len() as u32);
if last == '1' && last2 != "11" {
self.int2word(&ten_pow, false, Opts { gender: GenderArg::Idx(GENDER_F), ..Opts::default() })
} else {
self.int2word(
&ten_pow,
false,
Opts { case: CASE_G, plural: true, ..Opts::default() },
)
}
}
}
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 shortest_digits(a: f64) -> (String, i32) {
let sci = format!("{:e}", a);
let (mant, exp) = sci
.split_once('e')
.expect("{:e} on a finite f64 always emits an exponent");
let mut digits: Vec<u8> = mant.bytes().filter(|c| *c != b'.').collect();
let mut decpt: i32 = exp.parse::<i32>().expect("{:e} exponent is an integer") + 1;
let bits = a.to_bits();
let biased = ((bits >> 52) & 0x7ff) as i32;
let frac = bits & ((1u64 << 52) - 1);
let (mut m, mut e) = if biased == 0 {
(frac, -1074i32) } else {
(frac | (1u64 << 52), biased - 1075)
};
if m == 0 {
return (String::from_utf8(digits).expect("ASCII digits"), decpt);
}
let z = m.trailing_zeros() as i32;
m >>= z;
e += z;
let q = digits.len() as i32 - decpt;
let mut tie = e + q + 1 == 0;
if tie && q < 0 {
let r = -q as u32;
tie = r <= 22 && m % 5u64.pow(r) == 0;
}
if !tie {
return (String::from_utf8(digits).expect("ASCII digits"), decpt);
}
let last = digits[digits.len() - 1] - b'0';
if last % 2 == 1 {
if m % 4 == 1 {
*digits.last_mut().expect("non-empty") -= 1;
} else {
let mut i = digits.len();
loop {
if i == 0 {
digits.insert(0, b'1');
decpt += 1;
break;
}
i -= 1;
if digits[i] == b'9' {
digits[i] = b'0';
} else {
digits[i] += 1;
break;
}
}
}
while digits.len() > 1 && *digits.last().expect("non-empty") == b'0' {
digits.pop();
}
}
(String::from_utf8(digits).expect("ASCII digits"), decpt)
}
fn py_float_repr(value: f64) -> String {
if value.is_nan() {
return "nan".to_string();
}
if value.is_infinite() {
return if value > 0.0 { "inf" } else { "-inf" }.to_string();
}
let sign = if value.is_sign_negative() { "-" } else { "" };
let (digits, decpt) = shortest_digits(value.abs());
let ndigits = digits.len() as i32;
if decpt <= -4 || decpt > 16 {
let exp = decpt - 1;
let mut mant = String::from(&digits[..1]);
if digits.len() > 1 {
mant.push('.');
mant.push_str(&digits[1..]);
}
format!(
"{}{}e{}{:02}",
sign,
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
)
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat(-decpt as usize), digits)
} else if decpt >= ndigits {
format!("{}{}{}.0", sign, digits, "0".repeat((decpt - ndigits) as usize))
} else {
let d = decpt as usize;
format!("{}{}.{}", sign, &digits[..d], &digits[d..])
}
}
fn float_value_str(value: &FloatValue) -> String {
match value {
FloatValue::Float { value, .. } => py_float_repr(*value),
FloatValue::Decimal { value, .. } => py_decimal_str(value),
}
}
fn py_decimal_str(value: &BigDecimal) -> String {
let (int_val, scale) = value.as_bigint_and_exponent();
let exp = -(scale as i128);
let sign = if int_val.is_negative() { "-" } else { "" };
let int_digits = int_val.abs().to_string();
let len = int_digits.len() as i128;
let leftdigits = exp + len;
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 >= len {
(
format!("{}{}", int_digits, "0".repeat((dotplace - len) as usize)),
String::new(),
)
} else {
let d = dotplace as usize;
(int_digits[..d].to_string(), format!(".{}", &int_digits[d..]))
};
let expstr = if leftdigits == dotplace {
String::new()
} else {
let d = leftdigits - dotplace;
format!("E{}{}", if d < 0 { '-' } else { '+' }, d.abs())
};
format!("{}{}{}{}", sign, intpart, fracpart, expstr)
}
fn splitbyx(s: &str, x: usize) -> Vec<u32> {
let length = s.len();
let mut out = Vec::new();
if length > x {
let start = length % x;
if start > 0 {
out.push(s[..start].parse().unwrap());
}
let mut i = start;
while i < length {
out.push(s[i..i + x].parse().unwrap());
i += x;
}
} else {
out.push(s.parse().unwrap());
}
out
}
fn get_digits(x: u32) -> (u32, u32, u32) {
(x % 10, (x / 10) % 10, (x / 100) % 10)
}
impl Lang for LangRu {
fn default_currency(&self) -> &str {
"RUB"
}
fn default_separator(&self) -> &str {
","
}
fn negword(&self) -> &str {
"минус"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
self.int2word(value, true, Opts::default())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
self.int2word(value, false, Opts::default())
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
Ok(value.to_string())
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["case", "plural", "gender", "animate"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let o = self.opts_from_kwargs(kw)?;
self.int2word(value, true, o)
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["case", "plural", "gender", "animate"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
self.verify_ordinal(value)?;
let o = self.opts_from_kwargs(kw)?;
self.int2word(value, false, o)
}
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.as_whole_int() {
None => Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
float_value_str(value)
))),
Some(i) => {
if i.is_negative() {
Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
float_value_str(value)
)))
} else {
self.int2word(&i, false, Opts::default())
}
}
}
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
match value.as_whole_int() {
None => Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
repr_str
))),
Some(i) => {
if i.is_negative() {
Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
repr_str
)))
} else {
Ok(i.to_string())
}
}
}
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.cardinal_float_entry(value, None)
}
fn neg_zero_decimal(&self, to: &str) -> Option<Result<String>> {
match to {
"cardinal" | "year" => Some(self.cardinal_from_str("-0.0")),
_ => 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),
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
let _ = precision_override;
let n = match value {
FloatValue::Float { value: f, .. } => {
if f.abs() < 0.01 {
return self.int2word(&BigInt::zero(), true, Opts::default());
}
py_float_repr(*f)
}
FloatValue::Decimal { value: d, .. } => py_decimal_str(d),
};
self.cardinal_from_str(&n)
}
fn lang_name(&self) -> &str {
"Num2Word_RU"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let m100 = n.mod_floor(&BigInt::from(100)).to_u32().unwrap_or(0);
let m10 = n.mod_floor(&BigInt::from(10)).to_u32().unwrap_or(0);
let i = if matches!(m100, 11..=14) {
2
} else if m10 == 1 {
0
} else if matches!(m10, 2..=4) {
1
} else {
2
};
forms
.get(i)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn money_verbose(&self, number: &BigInt, currency: &str) -> Result<String> {
let gender = if currency == "UAH" { GENDER_F } else { GENDER_M };
self.int2word(number, true, Opts { gender: GenderArg::Idx(gender), ..Opts::default() })
}
fn cents_verbose(&self, number: &BigInt, currency: &str) -> Result<String> {
let gender = if matches!(currency, "UAH" | "RUB" | "BYN") { GENDER_F } else { GENDER_M };
self.int2word(number, true, Opts { gender: GenderArg::Idx(gender), ..Opts::default() })
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
) -> Result<String> {
let val = match val {
CurrencyValue::Decimal { value, .. } if value.is_integer() => {
CurrencyValue::Int(value.with_scale(0).as_bigint_and_exponent().0)
}
other => other.clone(),
};
if let CurrencyValue::Int(v) = &val {
if let Some(forms) = self.currency_forms.get(currency) {
let minus = if v.is_negative() { self.negword() } else { "" };
let abs = v.abs();
let money = self.to_cardinal(&abs)?;
let unit = self.pluralize(&abs, &forms.unit)?;
return Ok(format!("{} {} {}", minus, money, unit).trim().to_string());
}
}
crate::currency::default_to_currency(
self,
&val,
currency,
cents,
separator.unwrap_or(self.default_separator()),
adjective,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(n: i64) -> String {
LangRu::new().to_cardinal(&BigInt::from(n)).unwrap()
}
fn o(n: i64) -> String {
LangRu::new().to_ordinal(&BigInt::from(n)).unwrap()
}
#[test]
fn cardinals() {
assert_eq!(c(0), "ноль");
assert_eq!(c(1), "один");
assert_eq!(c(11), "одиннадцать");
assert_eq!(c(20), "двадцать");
assert_eq!(c(42), "сорок два");
assert_eq!(c(100), "сто");
assert_eq!(c(101), "сто один");
assert_eq!(c(1000), "одна тысяча");
assert_eq!(c(2000), "две тысячи");
assert_eq!(c(1000000), "один миллион");
assert_eq!(c(-42), "минус сорок два");
assert_eq!(c(-1000), "минус одна тысяча");
}
#[test]
fn ordinals() {
assert_eq!(o(0), "нулевой");
assert_eq!(o(3), "третий");
assert_eq!(o(100), "сотый");
assert_eq!(o(200), "двухсотый");
assert_eq!(o(1000), "тысячный");
assert_eq!(o(1001), "тысяча первый");
assert_eq!(o(2000), "двухтысячный");
assert_eq!(o(10000), "десятитысячный");
assert_eq!(o(100000), "стотысячный");
assert_eq!(o(1000000), "миллионный");
assert_eq!(o(10001), "десятый тысяч первый");
assert_eq!(o(100001), "сотый тысяч первый");
}
#[test]
fn big() {
let l = LangRu::new();
let sextillion: BigInt = BigInt::from(10).pow(21);
assert_eq!(l.to_cardinal(&sextillion).unwrap(), "один секстиллион");
assert_eq!(l.to_ordinal(&sextillion).unwrap(), "секстиллионный");
}
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn f(s: &str) -> CurrencyValue {
CurrencyValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
has_decimal: true,
is_float: true,
}
}
fn i(n: i64) -> CurrencyValue {
CurrencyValue::Int(BigInt::from(n))
}
fn cur(v: CurrencyValue, code: &str) -> String {
LangRu::new().to_currency(&v, code, true, None, false).unwrap()
}
#[test]
fn currency_ints_skip_cents() {
assert_eq!(cur(i(0), "EUR"), "ноль евро");
assert_eq!(cur(i(1), "EUR"), "один евро");
assert_eq!(cur(i(2), "EUR"), "два евро");
assert_eq!(cur(i(100), "EUR"), "сто евро");
assert_eq!(cur(i(1000000), "EUR"), "один миллион евро");
assert_eq!(cur(i(0), "USD"), "ноль долларов");
assert_eq!(cur(i(1), "USD"), "один доллар");
assert_eq!(cur(i(2), "USD"), "два доллара");
assert_eq!(cur(i(1), "GBP"), "один фунт стерлингов");
assert_eq!(cur(i(-12), "USD"), "минус двенадцать долларов");
}
#[test]
fn currency_whole_float_demotes_to_int() {
assert_eq!(cur(f("1.0"), "EUR"), "один евро");
assert_eq!(cur(f("1.0"), "USD"), "один доллар");
assert_eq!(cur(f("1.0"), "GBP"), "один фунт стерлингов");
assert_eq!(cur(f("-1.0"), "USD"), "минус один доллар");
assert_eq!(cur(f("1e+18"), "EUR"), "один квинтиллион евро");
}
#[test]
fn currency_floats_render_cents() {
assert_eq!(cur(f("12.34"), "EUR"), "двенадцать евро, тридцать четыре цента");
assert_eq!(cur(f("0.01"), "EUR"), "ноль евро, один цент");
assert_eq!(cur(f("0.5"), "EUR"), "ноль евро, пятьдесят центов");
assert_eq!(cur(f("-12.34"), "EUR"), "минус двенадцать евро, тридцать четыре цента");
assert_eq!(
cur(f("99.99"), "EUR"),
"девяносто девять евро, девяносто девять центов"
);
assert_eq!(
cur(f("1234.56"), "USD"),
"одна тысяча двести тридцать четыре доллара, пятьдесят шесть центов"
);
assert_eq!(
cur(f("1234.56"), "GBP"),
"одна тысяча двести тридцать четыре фунта стерлингов, пятьдесят шесть пенни"
);
}
#[test]
fn currency_cents_gender() {
assert_eq!(cur(f("1.01"), "RUB"), "один рубль, одна копейка");
assert_eq!(cur(f("1.01"), "BYN"), "один белорусский рубль, одна копейка");
assert_eq!(cur(f("1.01"), "USD"), "один доллар, один цент");
}
#[test]
fn currency_uah_gender_bug() {
assert_eq!(cur(i(1), "UAH"), "один гривна");
assert_eq!(cur(f("21.0"), "UAH"), "двадцать один гривна");
assert_eq!(cur(f("1.5"), "UAH"), "одна гривна, пятьдесят копеек");
assert_eq!(cur(f("21.5"), "UAH"), "двадцать одна гривна, пятьдесят копеек");
}
#[test]
fn currency_unknown_code_not_implemented() {
let l = LangRu::new();
for code in ["JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
for v in [i(1), f("12.34"), f("1.0")] {
match l.to_currency(&v, code, true, None, false) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{}\" not implemented for \"Num2Word_RU\"", code)
),
other => panic!("{} {:?}: expected NotImplemented, got {:?}", code, v, other),
}
}
}
}
#[test]
fn cheque() {
let l = LangRu::new();
let v = BigDecimal::from_str("1234.56").unwrap();
assert_eq!(
l.to_cheque(&v, "EUR").unwrap(),
"ОДНА ТЫСЯЧА ДВЕСТИ ТРИДЦАТЬ ЧЕТЫРЕ AND 56/100 ЕВРО"
);
assert_eq!(
l.to_cheque(&v, "USD").unwrap(),
"ОДНА ТЫСЯЧА ДВЕСТИ ТРИДЦАТЬ ЧЕТЫРЕ AND 56/100 ДОЛЛАРОВ"
);
assert_eq!(
l.to_cheque(&v, "GBP").unwrap(),
"ОДНА ТЫСЯЧА ДВЕСТИ ТРИДЦАТЬ ЧЕТЫРЕ AND 56/100 ФУНТОВ СТЕРЛИНГОВ"
);
assert!(matches!(
l.to_cheque(&v, "JPY"),
Err(N2WError::NotImplemented(_))
));
}
#[test]
fn currency_terse_cents() {
let l = LangRu::new();
assert_eq!(
l.to_currency(&f("12.34"), "EUR", false, None, false).unwrap(),
"двенадцать евро, 34 цента"
);
assert_eq!(
l.to_currency(&f("12.05"), "EUR", false, None, false).unwrap(),
"двенадцать евро, 05 центов"
);
}
#[test]
fn currency_pluralize_boundaries() {
for (n, want) in [
(1, "доллар"),
(2, "доллара"),
(4, "доллара"),
(5, "долларов"),
(11, "долларов"),
(12, "долларов"),
(14, "долларов"),
(21, "доллар"),
(22, "доллара"),
(25, "долларов"),
(100, "долларов"),
(111, "долларов"),
(121, "доллар"),
] {
let got = cur(i(n), "USD");
assert!(got.ends_with(want), "{}: {:?} should end with {:?}", n, got, want);
}
}
fn ff(v: f64) -> String {
LangRu::new()
.to_cardinal_float(&FloatValue::Float { value: v, precision: 0 }, None)
.unwrap()
}
fn dd(s: &str, p: u32) -> String {
LangRu::new()
.to_cardinal_float(
&FloatValue::Decimal { value: BigDecimal::from_str(s).unwrap(), precision: p },
None,
)
.unwrap()
}
#[test]
fn cardinal_floats() {
assert_eq!(ff(0.0), "ноль");
assert_eq!(ff(0.5), "ноль целых пять десятых");
assert_eq!(ff(1.0), "одна целая ноль десятых");
assert_eq!(ff(1.5), "одна целая пять десятых");
assert_eq!(ff(2.25), "две целых двадцать пять сотых");
assert_eq!(ff(3.14), "три целых четырнадцать сотых");
assert_eq!(ff(0.01), "ноль целых одна сотая");
assert_eq!(ff(0.1), "ноль целых одна десятая");
assert_eq!(ff(0.99), "ноль целых девяносто девять сотых");
assert_eq!(ff(1.01), "одна целая одна сотая");
assert_eq!(ff(12.34), "двенадцать целых тридцать четыре сотых");
assert_eq!(ff(99.99), "девяносто девять целых девяносто девять сотых");
assert_eq!(ff(100.5), "сто целых пять десятых");
assert_eq!(
ff(1234.56),
"одна тысяча двести тридцать четыре целых пятьдесят шесть сотых"
);
assert_eq!(ff(-0.5), "минус ноль целых пять десятых");
assert_eq!(ff(-1.5), "минус одна целая пять десятых");
assert_eq!(ff(-12.34), "минус двенадцать целых тридцать четыре сотых");
assert_eq!(ff(1.005), "одна целая пять тысячных");
assert_eq!(ff(2.675), "две целых шестьсот семьдесят пять тысячных");
}
#[test]
fn cardinal_float_below_hundredth_is_zero() {
assert_eq!(ff(0.001), "ноль");
assert_eq!(ff(0.005), "ноль");
assert_eq!(ff(-0.005), "ноль");
}
#[test]
fn cardinal_float_exponent_is_integer() {
assert_eq!(ff(1e16), "десять квадриллионов");
assert_eq!(ff(1e18), "один квинтиллион");
assert_eq!(
ff(123456789.5),
"сто двадцать три миллиона четыреста пятьдесят шесть тысяч \
семьсот восемьдесят девять целых пять десятых"
);
}
#[test]
fn cardinal_decimals() {
assert_eq!(dd("0.01", 2), "ноль целых одна сотая");
assert_eq!(dd("1.10", 2), "одна целая десять сотых");
assert_eq!(dd("12.345", 3), "двенадцать целых триста сорок пять тысячных");
assert_eq!(dd("0.001", 3), "ноль целых одна тысячная");
assert_eq!(
dd("98746251323029.99", 2),
"девяносто восемь триллионов семьсот сорок шесть миллиардов \
двести пятьдесят одна миллион триста двадцать три тысячи \
двадцать девять целых девяносто девять сотых"
);
assert_eq!(dd("5", 0), "пять");
assert_eq!(dd("-5", 0), "минус пять");
}
fn fentry(v: f64) -> String {
LangRu::new()
.cardinal_float_entry(&FloatValue::Float { value: v, precision: 1 }, None)
.unwrap()
}
#[test]
fn cardinal_float_entry_keeps_decimal_tail() {
assert_eq!(fentry(1.0), "одна целая ноль десятых");
assert_eq!(fentry(2.0), "две целых ноль десятых");
assert_eq!(fentry(-21.0), "минус двадцать одна целая ноль десятых");
assert_eq!(fentry(1000000.0), "одна миллион целых ноль десятых");
assert_eq!(
LangRu::new()
.cardinal_float_entry(&FloatValue::Float { value: -0.0, precision: 1 }, None)
.unwrap(),
"ноль"
);
let l = LangRu::new();
let d = FloatValue::Decimal {
value: BigDecimal::from_str("5.00").unwrap(),
precision: 2,
};
assert_eq!(
l.cardinal_float_entry(&d, None).unwrap(),
"пять целых ноль сотых"
);
assert_eq!(l.year_float_entry(&d).unwrap(), "пять целых ноль сотых");
}
#[test]
fn ordinal_float_entry_verifies() {
let l = LangRu::new();
let f = |v: f64| FloatValue::Float { value: v, precision: 1 };
assert_eq!(l.ordinal_float_entry(&f(5.0)).unwrap(), "пятый");
assert_eq!(l.ordinal_float_entry(&f(-0.0)).unwrap(), "нулевой");
assert_eq!(l.ordinal_float_entry(&f(1e16)).unwrap(), "десятиквадриллионный");
match l.ordinal_float_entry(&f(2.5)) {
Err(N2WError::Type(m)) => assert_eq!(m, "Cannot treat float 2.5 as ordinal."),
other => panic!("expected TypeError, got {:?}", other),
}
match l.ordinal_float_entry(&f(-3.0)) {
Err(N2WError::Type(m)) => {
assert_eq!(m, "Cannot treat negative num -3.0 as ordinal.")
}
other => panic!("expected TypeError, got {:?}", other),
}
let d = |s: &str| FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 2,
};
assert_eq!(l.ordinal_float_entry(&d("5.00")).unwrap(), "пятый");
assert_eq!(l.ordinal_float_entry(&d("1E+2")).unwrap(), "сотый");
assert!(matches!(l.ordinal_float_entry(&d("1.50")), Err(N2WError::Type(_))));
}
#[test]
fn ordinal_num_float_entry_verifies() {
let l = LangRu::new();
let f = |v: f64| FloatValue::Float { value: v, precision: 1 };
assert_eq!(l.ordinal_num_float_entry(&f(5.0), "5.0").unwrap(), "5");
assert_eq!(l.ordinal_num_float_entry(&f(-0.0), "-0.0").unwrap(), "0");
assert_eq!(
l.ordinal_num_float_entry(&f(1e20), "1e+20").unwrap(),
"100000000000000000000"
);
assert!(matches!(
l.ordinal_num_float_entry(&f(2.5), "2.5"),
Err(N2WError::Type(_))
));
assert!(matches!(
l.ordinal_num_float_entry(&f(-1.0), "-1.0"),
Err(N2WError::Type(_))
));
}
#[test]
fn infinity_string_is_value_error() {
let l = LangRu::new();
match l.str_to_number("Infinity") {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: 'Infinity'")
}
other => panic!("expected ValueError, got {:?}", other),
}
assert!(matches!(l.str_to_number("-Infinity"), Err(N2WError::Value(_))));
assert!(matches!(l.str_to_number("nan"), Ok(ParsedNumber::NaN)));
assert!(matches!(l.str_to_number("5"), Ok(ParsedNumber::Dec(_))));
}
use crate::base::{KwVal, Kwargs};
fn kw(items: &[(&str, KwVal)]) -> Kwargs {
Kwargs(items.iter().map(|(k, v)| (k.to_string(), v.clone())).collect())
}
fn s(v: &str) -> KwVal {
KwVal::Str(v.to_string())
}
#[test]
fn cardinal_kwargs() {
let l = LangRu::new();
let ck = |n: i64, items: &[(&str, KwVal)]| {
l.to_cardinal_kw(&BigInt::from(n), &kw(items)).unwrap()
};
assert_eq!(ck(1234, &[("case", s("g"))]), "одной тысячи двухсот тридцати четырёх");
assert_eq!(ck(1234, &[("case", s("a"))]), "одну тысячу двести тридцать четырёх");
assert_eq!(
ck(1234, &[("case", s("i"))]),
"одной тысячей двумястами тридцатью четырьмя"
);
assert_eq!(ck(-5, &[("case", s("g"))]), "минус пяти");
assert_eq!(ck(21, &[("case", s("родительный"))]), "двадцати одного");
assert_eq!(ck(21, &[("case", s("n")), ("gender", s("f"))]), "двадцать одна");
assert_eq!(ck(21, &[("case", s("n")), ("gender", s("ж"))]), "двадцать одна");
assert_eq!(
ck(2, &[("case", s("n")), ("plural", KwVal::Bool(true))]),
"двое"
);
assert_eq!(
ck(
1,
&[
("case", s("a")),
("plural", KwVal::Bool(false)),
("animate", KwVal::Bool(false))
]
),
"один"
);
assert!(matches!(
l.to_cardinal_kw(&BigInt::from(0), &kw(&[("case", s("xx"))])),
Err(N2WError::Key(_))
));
assert_eq!(ck(5, &[("case", s("n")), ("gender", s("xx"))]), "пять");
assert!(matches!(
l.to_cardinal_kw(&BigInt::from(1), &kw(&[("gender", s("xx"))])),
Err(N2WError::Key(_))
));
assert!(matches!(
l.to_cardinal_kw(&BigInt::from(1), &kw(&[("foo", s("bar"))])),
Err(N2WError::Fallback(_))
));
}
#[test]
fn ordinal_kwargs() {
let l = LangRu::new();
let ok = |n: i64, items: &[(&str, KwVal)]| {
l.to_ordinal_kw(&BigInt::from(n), &kw(items)).unwrap()
};
assert_eq!(ok(1234, &[("case", s("g"))]), "тысяча двести тридцать четвёртого");
assert_eq!(ok(21, &[("case", s("i"))]), "двадцать первым");
assert_eq!(ok(3, &[("case", s("d"))]), "третьему");
assert_eq!(ok(100, &[("case", s("p"))]), "сотом");
assert_eq!(
ok(1, &[("case", s("n")), ("plural", KwVal::Bool(true))]),
"первые"
);
assert!(matches!(
l.to_ordinal_kw(&BigInt::from(-5), &kw(&[("case", s("xx"))])),
Err(N2WError::Type(_))
));
assert!(matches!(
l.to_ordinal_kw(&BigInt::from(5), &kw(&[("case", s("xx"))])),
Err(N2WError::Key(_))
));
}
#[test]
fn negative_ordinal_is_type_error() {
let l = LangRu::new();
assert!(matches!(
l.to_ordinal(&BigInt::from(-1)),
Err(N2WError::Type(_))
));
assert!(matches!(
l.to_ordinal_num(&BigInt::from(-1)),
Err(N2WError::Type(_))
));
}
}