use crate::base::{Lang, N2WError, Result};
use crate::currency::{parse_currency_parts, CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::ParsedNumber;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{FromPrimitive, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const ZERO: &str = "nulis";
const NEGWORD: &str = "minus";
const POINTWORD: &str = "kablelis";
const ONES_FEMININE: [&str; 10] = [
"", "viena", "dvi", "trys", "keturios", "penkios", "šešios", "septynios", "aštuonios",
"devynios",
];
const ONES: [&str; 10] = [
"", "vienas", "du", "trys", "keturi", "penki", "šeši", "septyni", "aštuoni", "devyni",
];
const TENS: [&str; 10] = [
"dešimt",
"vienuolika",
"dvylika",
"trylika",
"keturiolika",
"penkiolika",
"šešiolika",
"septyniolika",
"aštuoniolika",
"devyniolika",
];
const TWENTIES: [&str; 10] = [
"",
"",
"dvidešimt",
"trisdešimt",
"keturiasdešimt",
"penkiasdešimt",
"šešiasdešimt",
"septyniasdešimt",
"aštuoniasdešimt",
"devyniasdešimt",
];
const HUNDRED: [&str; 2] = ["šimtas", "šimtai"];
static THOUSANDS: [[&str; 3]; 11] = [
["", "", ""], ["tūkstantis", "tūkstančiai", "tūkstančių"],
["milijonas", "milijonai", "milijonų"],
["milijardas", "milijardai", "milijardų"],
["trilijonas", "trilijonai", "trilijonų"],
["kvadrilijonas", "kvadrilijonai", "kvadrilijonų"],
["kvintilijonas", "kvintilijonai", "kvintilijonų"],
["sikstilijonas", "sikstilijonai", "sikstilijonų"],
["septilijonas", "septilijonai", "septilijonų"],
["oktilijonas", "oktilijonai", "oktilijonų"],
["naintilijonas", "naintilijonai", "naintilijonų"],
];
fn key_error(key: String) -> N2WError {
N2WError::Key(key)
}
fn thousands_at(i: usize) -> Result<&'static [&'static str; 3]> {
THOUSANDS
.get(i)
.filter(|_| i >= 1)
.ok_or_else(|| key_error(i.to_string()))
}
fn splitbyx(n: &str) -> Vec<u32> {
const X: usize = 3;
let chars: Vec<char> = n.chars().collect();
let length = chars.len();
let parse = |s: &[char]| -> u32 {
s.iter()
.collect::<String>()
.parse::<u32>()
.expect("splitbyx slice is always <= 3 ASCII digits")
};
let mut out: Vec<u32> = Vec::new();
if length > X {
let start = length % X;
if start > 0 {
out.push(parse(&chars[0..start]));
}
let mut i = start;
while i < length {
out.push(parse(&chars[i..(i + X).min(length)]));
i += X;
}
} else {
out.push(parse(&chars));
}
out
}
fn get_digits(n: u32) -> [usize; 3] {
let s = format!("{:03}", n); 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).expect("chunk digits are ASCII") as usize;
}
a
}
fn ordinal_lookup(n: &BigInt) -> Option<&'static str> {
Some(match n.to_u32()? {
1 => "pirmas",
2 => "antras",
3 => "trečias",
4 => "ketvirtas",
5 => "penktas",
6 => "šeštas",
7 => "septintas",
8 => "aštuntas",
9 => "devintas",
10 => "dešimtas",
11 => "vienuoliktas",
12 => "dvyliktas",
13 => "tryliktas",
14 => "keturioliktas",
15 => "penkioliktas",
16 => "šešioliktas",
17 => "septynioliktas",
18 => "aštuonioliktas",
19 => "devynioliktas",
20 => "dvidešimtas",
30 => "trisdešimtas",
40 => "keturiasdešimtas",
50 => "penkiasdešimtas",
60 => "šešiasdešimtas",
70 => "septyniasdešimtas",
80 => "aštuoniasdešimtas",
90 => "devyniasdešimtas",
100 => "šimtasis",
1000 => "tūkstantas",
_ => return None,
})
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const GENERIC_CENTS: [&str; 3] = ["centas", "centai", "centų"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"LTL",
CurrencyForms::new(&["litas", "litai", "litų"], &GENERIC_CENTS),
);
m.insert(
"EUR",
CurrencyForms::new(&["euras", "eurai", "eurų"], &GENERIC_CENTS),
);
m.insert(
"USD",
CurrencyForms::new(&["doleris", "doleriai", "dolerių"], &GENERIC_CENTS),
);
m.insert(
"GBP",
CurrencyForms::new(
&["svaras sterlingų", "svarai sterlingų", "svarų sterlingų"],
&["pensas", "pensai", "pensų"],
),
);
m.insert(
"PLN",
CurrencyForms::new(
&["zlotas", "zlotai", "zlotų"],
&["grašis", "grašiai", "grašių"],
),
);
m.insert(
"RUB",
CurrencyForms::new(
&["rublis", "rubliai", "rublių"],
&["kapeika", "kapeikos", "kapeikų"],
),
);
m
}
enum Cents {
Whole(BigInt),
Fractional(BigDecimal),
}
fn get_digits_big(n: &BigInt) -> Result<[usize; 3]> {
let digits = n.abs().to_string();
let s = if n.is_negative() {
format!("-{:0>2}", digits)
} else {
format!("{:0>3}", digits)
};
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).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", c))
})? as usize;
}
Ok(a)
}
fn trunc_to_bigint(v: &FloatValue) -> Option<BigInt> {
match v {
FloatValue::Float { value, .. } => {
if !value.is_finite() {
return None;
}
BigInt::from_f64(value.trunc())
}
FloatValue::Decimal { value, .. } => {
Some(value.with_scale(0).as_bigint_and_exponent().0)
}
}
}
fn int_value_error(literal: &str) -> N2WError {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
literal
))
}
fn float_no_point_str(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 s = format!("{:e}", f);
match s.split_once('e') {
Some((m, e)) if !e.starts_with('-') => format!("{}e+{:0>2}", m, e),
Some((m, e)) => format!("{}e-{:0>2}", m, &e[1..]),
None => s,
}
}
fn reconstructed_str(v: &FloatValue) -> String {
match v {
FloatValue::Float { value, precision } => {
format!("{:.*}", *precision as usize, value)
}
FloatValue::Decimal { value, precision } => {
let neg = value.is_negative();
let abs = value.abs();
let body = if *precision == 0 {
abs.with_scale(0).as_bigint_and_exponent().0.to_string()
} else {
let sc = *precision as usize;
let coeff = abs
.with_scale(*precision as i64)
.as_bigint_and_exponent()
.0;
let digits = coeff.to_string();
let padded = if digits.len() < sc + 1 {
format!("{}{}", "0".repeat(sc + 1 - digits.len()), digits)
} else {
digits
};
let cut = padded.len() - sc;
format!("{}.{}", &padded[..cut], &padded[cut..])
};
if neg {
format!("-{}", body)
} else {
body
}
}
}
}
pub struct LangLt {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangLt {
fn default() -> Self {
Self::new()
}
}
impl LangLt {
pub fn new() -> Self {
LangLt {
currency_forms: build_currency_forms(),
}
}
fn pluralize(&self, n: u32, forms: &[&str; 3]) -> String {
let [n1, n2, _n3] = get_digits(n);
if n2 == 1 || n1 == 0 || n == 0 {
forms[2].to_string()
} else if n1 == 1 {
forms[0].to_string()
} else {
forms[1].to_string()
}
}
fn int2word(&self, n: &BigInt, feminine: bool) -> Result<String> {
if n.is_zero() {
return Ok(ZERO.to_string());
}
let mut words: Vec<String> = Vec::new();
let chunks = splitbyx(&n.to_string());
let mut i = chunks.len();
let thousand = BigInt::from(1000);
for x in chunks {
i -= 1;
if x == 0 {
continue;
}
let [n1, n2, n3] = get_digits(x);
if n3 > 0 {
words.push(ONES[n3].to_string());
if n3 > 1 {
words.push(HUNDRED[1].to_string());
} else {
words.push(HUNDRED[0].to_string());
}
}
if n2 > 1 {
words.push(TWENTIES[n2].to_string());
}
if n2 == 1 {
words.push(TENS[n1].to_string());
} else if n1 > 0 {
if (i == 1 || (feminine && i == 0)) && *n < thousand {
words.push(ONES_FEMININE[n1].to_string());
} else {
words.push(ONES[n1].to_string());
}
}
if i > 0 {
let forms = thousands_at(i)?;
words.push(self.pluralize(x, forms));
}
}
Ok(words.join(" "))
}
}
impl Lang for LangLt {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
""
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"kablelis"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let words = self.int2word(&value.abs(), false)?;
Ok(format!("{} {}", NEGWORD, words))
} else {
self.int2word(value, false)
}
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if let Some(word) = ordinal_lookup(value) {
return Ok(word.to_string());
}
Ok(format!("{}as", self.to_cardinal(value)?))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let s = reconstructed_str(value);
let (base_str, n) = match s.strip_prefix('-') {
Some(rest) => (format!("{} ", NEGWORD), rest.to_string()),
None => (String::new(), s),
};
let to_int = |lit: &str| -> Result<BigInt> {
BigInt::parse_bytes(lit.as_bytes(), 10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", lit))
})
};
if let Some(dot) = n.find('.') {
let left = &n[..dot];
let right = &n[dot + 1..];
let leading_zero_count = right.len() - right.trim_start_matches('0').len();
let decimal_part = format!(
"{}{}",
format!("{} ", ZERO).repeat(leading_zero_count),
self.int2word(&to_int(right)?, false)?
);
Ok(format!(
"{}{} {} {}",
base_str,
self.int2word(&to_int(left)?, false)?,
POINTWORD,
decimal_part
))
} else {
Ok(format!("{}{}", base_str, self.int2word(&to_int(&n)?, false)?))
}
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
if value.has_visible_point() {
return self.to_cardinal_float(value, precision_override);
}
match value {
FloatValue::Float { value, .. } => Err(int_value_error(&float_no_point_str(*value))),
FloatValue::Decimal { value, .. } => {
let s = crate::strnum::python_decimal_str(value);
match crate::strnum::python_int_parse(&s) {
Some(i) => self.to_cardinal(&i),
None => Err(int_value_error(&s)),
}
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if let FloatValue::Float { value: f, .. } = value {
if f.is_nan() {
return Ok("nan".to_string());
}
if f.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
}
let num = trunc_to_bigint(value).expect("finite after the guards above");
self.to_ordinal(&num)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match crate::strnum::python_decimal_parse(s)? {
ParsedNumber::Inf { negative } => Err(int_value_error(if negative {
"-Infinity"
} else {
"Infinity"
})),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_LT"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let [n1, n2, _n3] = get_digits_big(n)?;
let idx = if n2 == 1 || n1 == 0 || n.is_zero() {
2
} else if n1 == 1 {
0
} else {
1
};
forms
.get(idx)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let separator = separator.unwrap_or(self.default_separator());
let (left, right, is_negative, is_float) = match val {
CurrencyValue::Int(v) => (v.abs(), Cents::Whole(BigInt::zero()), v.is_negative(), false),
CurrencyValue::Decimal { value, .. } => {
let scaled = value * BigDecimal::from(100);
let has_fractional_cents = &scaled - scaled.with_scale(0) != BigDecimal::zero();
let (l, r, neg) = parse_currency_parts(val, false, has_fractional_cents, 100);
let r = if has_fractional_cents {
Cents::Fractional(r)
} else {
Cents::Whole(r.as_bigint_and_exponent().0)
};
(l, r, neg, true)
}
};
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let cr1 = &forms.unit;
let cr2 = &forms.subunit;
let minus_str = if is_negative {
format!("{} ", NEGWORD)
} else {
String::new()
};
let money_str = self.to_cardinal(&left)?;
let right_is_positive = match &right {
Cents::Whole(r) => r.is_positive(),
Cents::Fractional(r) => r.is_positive(),
};
if !(right_is_positive || is_float) {
return Ok(format!(
"{}{} {}",
minus_str,
money_str,
Lang::pluralize(self, &left, cr1)?
));
}
let right_is_zero = match &right {
Cents::Whole(r) => r.is_zero(),
Cents::Fractional(r) => r.is_zero(),
};
let cents_str = if right_is_zero {
if cents {
self.to_cardinal(&BigInt::zero())?
} else {
"0".to_string()
}
} else {
match &right {
Cents::Fractional(r) => {
if cents {
self.cardinal_from_decimal(r)?
} else {
let f = r.to_f64().ok_or_else(|| {
N2WError::Value(format!("cannot represent {} as f64", r))
})?;
format!("{}", f)
}
}
Cents::Whole(r) => {
if cents {
self.to_cardinal(r)?
} else {
r.to_string()
}
}
}
};
let right_trunc = match &right {
Cents::Whole(r) => r.clone(),
Cents::Fractional(r) => r.with_scale(0).as_bigint_and_exponent().0,
};
let sep = if separator.is_empty() { ", " } else { separator };
Ok(format!(
"{}{} {}{}{} {}",
minus_str,
money_str,
Lang::pluralize(self, &left, cr1)?,
sep,
cents_str,
Lang::pluralize(self, &right_trunc, cr2)?
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn int(s: &str) -> CurrencyValue {
CurrencyValue::parse(s, true, false, false).unwrap()
}
fn float(s: &str) -> CurrencyValue {
CurrencyValue::parse(s, false, true, true).unwrap()
}
fn cur(lt: &LangLt, code: &str, v: &CurrencyValue) -> Result<String> {
lt.to_currency(v, code, true, None, false)
}
#[test]
fn currency_corpus_rows() {
let lt = LangLt::new();
let rows: Vec<(&str, CurrencyValue, &str)> = vec![
("EUR", int("0"), "nulis eurų"),
("EUR", int("1"), "vienas euras"),
("EUR", int("2"), "du eurai"),
("EUR", int("100"), "vienas šimtas eurų"),
("EUR", float("12.34"), "dvylika eurų, trisdešimt keturi centai"),
("EUR", float("0.01"), "nulis eurų, vienas centas"),
("EUR", float("1.0"), "vienas euras, nulis centų"),
("EUR", float("99.99"), "devyniasdešimt devyni eurai, devyniasdešimt devyni centai"),
("EUR", float("1234.56"), "vienas tūkstantis du šimtai trisdešimt keturi eurai, penkiasdešimt šeši centai"),
("EUR", float("-12.34"), "minus dvylika eurų, trisdešimt keturi centai"),
("EUR", int("1000000"), "vienas milijonas eurų"),
("EUR", float("0.5"), "nulis eurų, penkiasdešimt centų"),
("USD", int("0"), "nulis dolerių"),
("USD", int("1"), "vienas doleris"),
("USD", int("2"), "du doleriai"),
("USD", int("100"), "vienas šimtas dolerių"),
("USD", float("12.34"), "dvylika dolerių, trisdešimt keturi centai"),
("USD", float("0.01"), "nulis dolerių, vienas centas"),
("USD", float("1.0"), "vienas doleris, nulis centų"),
("USD", float("99.99"), "devyniasdešimt devyni doleriai, devyniasdešimt devyni centai"),
("USD", float("1234.56"), "vienas tūkstantis du šimtai trisdešimt keturi doleriai, penkiasdešimt šeši centai"),
("USD", float("-12.34"), "minus dvylika dolerių, trisdešimt keturi centai"),
("USD", int("1000000"), "vienas milijonas dolerių"),
("USD", float("0.5"), "nulis dolerių, penkiasdešimt centų"),
("GBP", int("0"), "nulis svarų sterlingų"),
("GBP", int("1"), "vienas svaras sterlingų"),
("GBP", int("2"), "du svarai sterlingų"),
("GBP", int("100"), "vienas šimtas svarų sterlingų"),
("GBP", float("12.34"), "dvylika svarų sterlingų, trisdešimt keturi pensai"),
("GBP", float("0.01"), "nulis svarų sterlingų, vienas pensas"),
("GBP", float("1.0"), "vienas svaras sterlingų, nulis pensų"),
("GBP", float("99.99"), "devyniasdešimt devyni svarai sterlingų, devyniasdešimt devyni pensai"),
("GBP", float("1234.56"), "vienas tūkstantis du šimtai trisdešimt keturi svarai sterlingų, penkiasdešimt šeši pensai"),
("GBP", float("-12.34"), "minus dvylika svarų sterlingų, trisdešimt keturi pensai"),
("GBP", int("1000000"), "vienas milijonas svarų sterlingų"),
("GBP", float("0.5"), "nulis svarų sterlingų, penkiasdešimt pensų"),
];
for (code, val, want) in rows {
assert_eq!(cur(<, code, &val).unwrap(), want, "currency:{} {:?}", code, val);
}
}
#[test]
fn currency_corpus_notimplemented_rows() {
let lt = LangLt::new();
let vals = [
int("0"), int("1"), int("2"), int("100"), float("12.34"), float("0.01"),
float("1.0"), float("99.99"), float("1234.56"), float("-12.34"),
int("1000000"), float("0.5"),
];
for code in ["JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
for val in &vals {
match cur(<, code, val) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{}\" not implemented for \"Num2Word_LT\"", code)
),
other => panic!("currency:{} {:?} -> {:?}", code, val, other),
}
}
}
}
#[test]
fn cheque_corpus_rows() {
let lt = LangLt::new();
let v = BigDecimal::from_str("1234.56").unwrap();
for (code, want) in [
("EUR", "VIENAS TŪKSTANTIS DU ŠIMTAI TRISDEŠIMT KETURI AND 56/100 EURŲ"),
("USD", "VIENAS TŪKSTANTIS DU ŠIMTAI TRISDEŠIMT KETURI AND 56/100 DOLERIŲ"),
("GBP", "VIENAS TŪKSTANTIS DU ŠIMTAI TRISDEŠIMT KETURI AND 56/100 SVARŲ STERLINGŲ"),
] {
assert_eq!(lt.to_cheque(&v, code).unwrap(), want, "cheque:{}", code);
}
for code in ["JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
match lt.to_cheque(&v, code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{}\" not implemented for \"Num2Word_LT\"", code)
),
other => panic!("cheque:{} -> {:?}", code, other),
}
}
}
#[test]
fn currency_codes_absent_from_corpus() {
let lt = LangLt::new();
let rows: Vec<(&str, CurrencyValue, &str)> = vec![
("LTL", int("1"), "vienas litas"),
("LTL", int("2"), "du litai"),
("LTL", int("0"), "nulis litų"),
("LTL", float("1.0"), "vienas litas, nulis centų"),
("LTL", float("12.34"), "dvylika litų, trisdešimt keturi centai"),
("PLN", int("1"), "vienas zlotas"),
("PLN", int("2"), "du zlotai"),
("PLN", int("0"), "nulis zlotų"),
("PLN", float("1.0"), "vienas zlotas, nulis grašių"),
("PLN", float("12.34"), "dvylika zlotų, trisdešimt keturi grašiai"),
("RUB", int("1"), "vienas rublis"),
("RUB", int("2"), "du rubliai"),
("RUB", int("0"), "nulis rublių"),
("RUB", float("1.0"), "vienas rublis, nulis kapeikų"),
("RUB", float("12.34"), "dvylika rublių, trisdešimt keturi kapeikos"),
];
for (code, val, want) in rows {
assert_eq!(cur(<, code, &val).unwrap(), want, "currency:{} {:?}", code, val);
}
}
#[test]
fn decimal_without_dot_still_shows_cents() {
let lt = LangLt::new();
let d5 = CurrencyValue::parse("5", false, false, false).unwrap();
assert_eq!(cur(<, "EUR", &d5).unwrap(), "penki eurai, nulis centų");
assert_eq!(cur(<, "EUR", &int("5")).unwrap(), "penki eurai");
}
#[test]
fn fractional_cents() {
let lt = LangLt::new();
assert_eq!(
cur(<, "EUR", &float("12.345")).unwrap(),
"dvylika eurų, trisdešimt keturi kablelis penki centai"
);
assert_eq!(
cur(<, "EUR", &float("1.011")).unwrap(),
"vienas euras, vienas kablelis vienas centas"
);
assert_eq!(
cur(<, "EUR", &float("0.005")).unwrap(),
"nulis eurų, nulis kablelis penki centų"
);
}
#[test]
fn cents_false() {
let lt = LangLt::new();
let terse = |v: &CurrencyValue| lt.to_currency(v, "EUR", false, None, false).unwrap();
assert_eq!(terse(&float("12.34")), "dvylika eurų, 34 centai");
assert_eq!(terse(&float("1.0")), "vienas euras, 0 centų");
assert_eq!(terse(&float("12.345")), "dvylika eurų, 34.5 centai");
}
#[test]
fn explicit_separator_has_no_trailing_space() {
let lt = LangLt::new();
assert_eq!(
lt.to_currency(&float("12.34"), "EUR", true, Some(" ir"), false).unwrap(),
"dvylika eurų irtrisdešimt keturi centai"
);
assert_eq!(
lt.to_currency(&float("12.34"), "EUR", true, Some(","), false).unwrap(),
"dvylika eurų,trisdešimt keturi centai"
);
assert_eq!(
lt.to_currency(&float("12.34"), "EUR", true, Some(""), false).unwrap(),
"dvylika eurų, trisdešimt keturi centai"
);
}
#[test]
fn adjective_is_ignored() {
let lt = LangLt::new();
assert_eq!(
lt.to_currency(&float("12.34"), "EUR", true, None, true).unwrap(),
"dvylika eurų, trisdešimt keturi centai"
);
assert_eq!(lt.to_currency(&int("2"), "EUR", true, None, true).unwrap(), "du eurai");
}
#[test]
fn pluralize_reads_last_three_digits() {
let lt = LangLt::new();
let forms: Vec<String> = ["euras", "eurai", "eurų"].iter().map(|s| s.to_string()).collect();
let p = |n: i64| Lang::pluralize(<, &BigInt::from(n), &forms).unwrap();
assert_eq!(p(1), "euras");
assert_eq!(p(21), "euras"); assert_eq!(p(11), "eurų"); assert_eq!(p(12), "eurų");
assert_eq!(p(2), "eurai");
assert_eq!(p(1234), "eurai"); assert_eq!(p(0), "eurų");
assert_eq!(p(100), "eurų"); assert_eq!(p(1000000), "eurų");
assert_eq!(
Lang::pluralize(<, &BigInt::from_str("100000000000000000000001").unwrap(), &forms).unwrap(),
"euras"
);
}
#[test]
fn pluralize_negative_is_a_value_error() {
let lt = LangLt::new();
let forms: Vec<String> = ["euras", "eurai", "eurų"].iter().map(|s| s.to_string()).collect();
assert!(matches!(
Lang::pluralize(<, &BigInt::from(-5), &forms),
Err(N2WError::Value(_))
));
}
fn flt(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn dec(s: &str, precision: u32) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
}
}
#[test]
fn cardinal_float_corpus_rows() {
let lt = LangLt::new();
let rows: Vec<(FloatValue, &str)> = vec![
(flt(0.0, 1), "nulis kablelis nulis nulis"),
(flt(0.5, 1), "nulis kablelis penki"),
(flt(1.0, 1), "vienas kablelis nulis nulis"),
(flt(1.5, 1), "vienas kablelis penki"),
(flt(2.25, 2), "du kablelis dvidešimt penki"),
(flt(3.14, 2), "trys kablelis keturiolika"),
(flt(0.01, 2), "nulis kablelis nulis vienas"),
(flt(0.1, 1), "nulis kablelis vienas"),
(flt(0.99, 2), "nulis kablelis devyniasdešimt devyni"),
(flt(1.01, 2), "vienas kablelis nulis vienas"),
(flt(12.34, 2), "dvylika kablelis trisdešimt keturi"),
(flt(99.99, 2), "devyniasdešimt devyni kablelis devyniasdešimt devyni"),
(flt(100.5, 1), "vienas šimtas kablelis penki"),
(flt(1234.56, 2), "vienas tūkstantis du šimtai trisdešimt keturi kablelis penkiasdešimt šeši"),
(flt(-0.5, 1), "minus nulis kablelis penki"),
(flt(-1.5, 1), "minus vienas kablelis penki"),
(flt(-12.34, 2), "minus dvylika kablelis trisdešimt keturi"),
(flt(1.005, 3), "vienas kablelis nulis nulis penki"),
(flt(2.675, 3), "du kablelis šeši šimtai septyniasdešimt penki"),
];
for (v, want) in rows {
assert_eq!(lt.to_cardinal_float(&v, None).unwrap(), want, "cardinal {:?}", v);
}
}
#[test]
fn cardinal_decimal_corpus_rows() {
let lt = LangLt::new();
let rows: Vec<(FloatValue, &str)> = vec![
(dec("0.01", 2), "nulis kablelis nulis vienas"),
(dec("1.10", 2), "vienas kablelis dešimt"),
(dec("12.345", 3), "dvylika kablelis trys šimtai keturiasdešimt penki"),
(dec("98746251323029.99", 2),
"devyniasdešimt aštuoni trilijonai septyni šimtai keturiasdešimt šeši \
milijardai du šimtai penkiasdešimt vienas milijonas trys šimtai \
dvidešimt trys tūkstančiai dvidešimt devyni kablelis devyniasdešimt devyni"),
(dec("0.001", 3), "nulis kablelis nulis nulis vienas"),
];
for (v, want) in rows {
assert_eq!(lt.to_cardinal_float(&v, None).unwrap(), want, "cardinal_dec {:?}", v);
}
}
#[test]
fn cardinal_decimal_edges() {
let lt = LangLt::new();
assert_eq!(lt.to_cardinal_float(&dec("5", 0), None).unwrap(), "penki");
assert_eq!(
lt.to_cardinal_float(&dec("5.00", 2), None).unwrap(),
"penki kablelis nulis nulis nulis"
);
assert_eq!(
lt.to_cardinal_float(&dec("1.100", 3), None).unwrap(),
"vienas kablelis vienas šimtas"
);
assert_eq!(lt.to_cardinal_float(&dec("-1.5", 1), None).unwrap(), "minus vienas kablelis penki");
}
#[test]
fn precision_override_is_ignored() {
let lt = LangLt::new();
for p in [Some(0u32), Some(1), Some(5), None] {
assert_eq!(
lt.to_cardinal_float(&flt(1.5, 1), p).unwrap(),
"vienas kablelis penki",
"precision override {:?}", p
);
}
assert_eq!(
lt.to_cardinal_float(&flt(2.675, 3), Some(1)).unwrap(),
"du kablelis šeši šimtai septyniasdešimt penki"
);
}
#[test]
fn cardinal_float_messy_reprs() {
let lt = LangLt::new();
let rows: Vec<(FloatValue, &str)> = vec![
(flt(10.0, 1), "dešimt kablelis nulis nulis"),
(flt(0.25, 2), "nulis kablelis dvidešimt penki"),
(flt(123.456, 3), "vienas šimtas dvidešimt trys kablelis keturi šimtai penkiasdešimt šeši"),
(flt(1000000.001, 3), "vienas milijonas kablelis nulis nulis vienas"),
(flt(3.0, 1), "trys kablelis nulis nulis"),
(flt(0.0001, 4), "nulis kablelis nulis nulis nulis vienas"),
(flt(-0.01, 2), "minus nulis kablelis nulis vienas"),
(flt(0.1 + 0.2, 17), "nulis kablelis trisdešimt kvadrilijonų keturi"),
];
for (v, want) in rows {
assert_eq!(lt.to_cardinal_float(&v, None).unwrap(), want, "cardinal {:?}", v);
}
}
}