use crate::base::{Lang, N2WError, Result};
use crate::currency::{parse_currency_parts, CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const NEGWORD: &str = "минус ";
const POINTWORD: &str = "точка";
const ONES: [&str; 10] = [
"нула", "едно", "две", "три", "четири", "пет", "шест", "седем", "осем", "девет",
];
fn tens_word(n: u32) -> Option<&'static str> {
Some(match n {
10 => "десет",
11 => "единадесет",
12 => "дванадесет",
13 => "тринадесет",
14 => "четиринадесет",
15 => "петнадесет",
16 => "шестнадесет",
17 => "седемнадесет",
18 => "осемнадесет",
19 => "деветнадесет",
20 => "двадесет",
30 => "тридесет",
40 => "четиридесет",
50 => "петдесет",
60 => "шестдесет",
70 => "седемдесет",
80 => "осемдесет",
90 => "деветдесет",
_ => return None,
})
}
fn ordinal_word(n: u32) -> Option<&'static str> {
Some(match n {
1 => "първи",
2 => "втори",
3 => "трети",
4 => "четвърти",
5 => "пети",
6 => "шести",
7 => "седми",
8 => "осми",
9 => "девети",
10 => "десети",
11 => "единадесети",
12 => "дванадесети",
13 => "тринадесети",
14 => "четиринадесети",
15 => "петнадесети",
16 => "шестнадесети",
17 => "седемнадесети",
18 => "осемнадесети",
19 => "деветнадесети",
20 => "двадесети",
30 => "тридесети",
40 => "четиридесети",
50 => "петдесети",
60 => "шестдесети",
70 => "седемдесети",
80 => "осемдесети",
90 => "деветдесети",
100 => "стотен",
1000 => "хиляден",
_ => return None,
})
}
fn ones_form(n: u32, masculine: bool, feminine: bool) -> &'static str {
if feminine {
match n {
1 => return "една",
2 => return "две",
_ => {}
}
}
if masculine {
match n {
1 => return "един",
2 => return "два",
_ => {}
}
}
ONES[n as usize]
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const CENTS: [&str; 2] = ["цент", "цента"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"BGN",
CurrencyForms::new(&["лев", "лева"], &["стотинка", "стотинки"]),
);
m.insert("EUR", CurrencyForms::new(&["евро", "евро"], &CENTS));
m.insert("USD", CurrencyForms::new(&["долар", "долара"], &CENTS));
m.insert("GBP", CurrencyForms::new(&["паунд", "паунда"], &["пени", "пенса"]));
m.insert("JPY", CurrencyForms::new(&["йена", "йени"], &["сен", "сена"]));
m
}
fn index_form(forms: &[String], singular: bool) -> Result<&str> {
forms
.get(if singular { 0 } else { 1 })
.map(String::as_str)
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn python_int_of_float(value: f64) -> Result<BigInt> {
if value.is_nan() {
return Err(N2WError::Value("cannot convert float NaN to integer".into()));
}
if value.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
Ok(BigInt::from_f64(value.trunc()).expect("finite f64 truncates exactly"))
}
fn base_float2tuple(value: f64, precision: u32) -> Result<(BigInt, BigInt)> {
let pre = python_int_of_float(value)?;
let scale = match BigInt::from(10).pow(precision).to_f64() {
Some(s) if s.is_finite() => s,
_ => {
return Err(N2WError::Overflow(
"int too large to convert to float".into(),
))
}
};
let post = (value - value.trunc()).abs() * scale;
let rounded = post.round_ties_even();
let out = if (rounded - post).abs() < 0.01 {
rounded
} else {
post.floor()
};
Ok((
pre,
BigInt::from_f64(out).expect("finite scale keeps `out` finite"),
))
}
fn repr_precision(f: f64) -> u32 {
let s = format!("{}", f);
match s.split_once('.') {
Some((_, frac)) if !frac.contains('e') => frac.len() as u32,
_ => 0,
}
}
pub struct LangBg {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangBg {
pub fn new() -> Self {
LangBg {
currency_forms: build_currency_forms(),
}
}
fn int_to_word(&self, n: &BigInt, masculine: bool, feminine: bool) -> String {
if n.is_zero() {
return ONES[0].to_string();
}
let mut parts: Vec<String> = Vec::new();
let mut n = n.clone();
let two = BigInt::from(2);
let billion = BigInt::from(1_000_000_000u64);
let million = BigInt::from(1_000_000u64);
let thousand = BigInt::from(1000u32);
let hundred = BigInt::from(100u32);
if n >= billion {
let billions = &n / &billion;
if billions.is_one() {
parts.push("един милиард".to_string());
} else if billions == two {
parts.push("два милиарда".to_string());
} else {
parts.push(format!(
"{} милиарда",
self.int_to_word(&billions, false, false)
));
}
n %= &billion;
}
if n >= million {
let millions = &n / &million;
if millions.is_one() {
parts.push("един милион".to_string());
} else if millions == two {
parts.push("два милиона".to_string());
} else {
parts.push(format!(
"{} милиона",
self.int_to_word(&millions, false, false)
));
}
n %= &million;
}
if n >= thousand {
let thousands = &n / &thousand;
if thousands.is_one() {
parts.push("хиляда".to_string());
} else if thousands == two {
parts.push("две хиляди".to_string());
} else {
parts.push(format!(
"{} хиляди",
self.int_to_word(&thousands, false, false)
));
}
n %= &thousand;
}
if n >= hundred {
let hundreds = (&n / &hundred).to_u32().expect("hundreds is 1..=9");
match hundreds {
1 => parts.push("сто".to_string()),
2 => parts.push("двеста".to_string()),
3 => parts.push("триста".to_string()),
_ => parts.push(format!("{}стотин", ONES[hundreds as usize])),
}
n %= &hundred;
}
let small = n.to_u32().expect("n < 100 after the hundreds step");
if small >= 20 {
let tens = (small / 10) * 10;
parts.push(tens_word(tens).expect("multiple of ten in 20..=90").to_string());
let unit = small % 10;
if unit > 0 {
parts.push("и".to_string());
parts.push(ones_form(unit, masculine, feminine).to_string());
}
} else if small >= 10 {
parts.push(tens_word(small).expect("10..=19").to_string());
} else if small > 0 {
parts.push(ones_form(small, masculine, feminine).to_string());
}
parts.join(" ")
}
fn int_to_cardinal(&self, n: &BigInt) -> String {
if n.is_zero() {
return ONES[0].to_string();
}
if n.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&(-n), false, false));
}
self.int_to_word(n, true, false)
}
fn ordinal_numeric(&self, value: &FloatValue) -> String {
let trunc = match value {
FloatValue::Float { value, .. } => {
BigInt::from_f64(value.trunc()).unwrap_or_else(BigInt::zero)
}
FloatValue::Decimal { value, .. } => value.with_scale(0).as_bigint_and_exponent().0,
};
self.int_to_ordinal(&trunc)
}
fn cardinal_year_numeric(&self, value: &FloatValue) -> Result<String> {
let (neg, frac, whole) = decompose_numeric(value);
if whole.is_zero() && !frac {
return Ok(ONES[0].to_string());
}
if frac {
return Err(N2WError::Key(frac_key_msg(value)));
}
if neg {
return Ok(format!("{}{}", NEGWORD, self.int_to_word(&whole, false, false)));
}
Ok(self.int_to_word(&whole, true, false))
}
fn int_to_ordinal(&self, n: &BigInt) -> String {
if let Some(word) = n.to_u32().and_then(ordinal_word) {
return word.to_string();
}
let cardinal = self.int_to_cardinal(n);
if let Some(stem) = cardinal.strip_suffix("един") {
format!("{}първи", stem)
} else if let Some(stem) = cardinal.strip_suffix("два") {
format!("{}втори", stem)
} else if let Some(stem) = cardinal.strip_suffix("три") {
format!("{}трети", stem)
} else if cardinal.ends_with('т') {
format!("{}и", cardinal)
} else {
format!("{}ти", cardinal)
}
}
fn to_cardinal_f64(&self, n: f64, precision: u32) -> Result<String> {
let pre_int = python_int_of_float(n)?;
if n != n.trunc() {
let (pre, post) = match base_float2tuple(n, precision) {
Ok(t) => t,
Err(_) => return Ok(self.int_to_cardinal(&pre_int)),
};
let (mut result, pre) = if n < 0.0 {
(NEGWORD.to_string(), pre.abs())
} else {
(String::new(), pre)
};
result.push_str(&self.int_to_cardinal(&pre));
if precision > 0 {
result.push(' ');
result.push_str(POINTWORD);
let mut post_str = post.to_string();
let len = post_str.chars().count();
if (precision as usize) > len {
post_str = format!("{}{}", "0".repeat(precision as usize - len), post_str);
}
for ch in post_str.chars() {
let d = ch
.to_digit(10)
.expect("float2tuple yields a non-negative decimal integer");
result.push(' ');
result.push_str(ONES[d as usize]);
}
}
return Ok(result.trim().to_string());
}
if n < 0.0 {
return Ok(format!("{}{}", NEGWORD, self.to_cardinal_f64(-n, precision)?));
}
Ok(self.int_to_cardinal(&pre_int))
}
fn to_cardinal_bigdecimal(&self, n: &BigDecimal) -> String {
if n.is_negative() {
return format!("{}{}", NEGWORD, self.to_cardinal_bigdecimal(&-n.clone()));
}
self.int_to_cardinal(&n.with_scale(0).as_bigint_and_exponent().0)
}
}
impl Default for LangBg {
fn default() -> Self {
Self::new()
}
}
fn decompose_numeric(value: &FloatValue) -> (bool, bool, BigInt) {
match value {
FloatValue::Float { value: v, .. } => {
let neg = *v < 0.0;
let frac = v.fract() != 0.0;
let whole = BigInt::from_f64(v.trunc())
.unwrap_or_else(BigInt::zero)
.abs();
(neg, frac, whole)
}
FloatValue::Decimal { value: d, .. } => {
let neg = d.is_negative();
let frac = !d.is_integer();
let whole = d.abs().with_scale(0).as_bigint_and_exponent().0;
(neg, frac, whole)
}
}
}
fn frac_key_msg(value: &FloatValue) -> String {
match value {
FloatValue::Float { value, .. } => format!("{}", value),
FloatValue::Decimal { value, .. } => format!("{}", value),
}
}
impl Lang for LangBg {
fn default_currency(&self) -> &str {
"BGN"
}
fn default_separator(&self) -> &str {
" и"
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
POINTWORD
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
return Ok(format!("{}{}", NEGWORD, self.int_to_cardinal(&(-value))));
}
Ok(self.int_to_cardinal(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(self.int_to_ordinal(value))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
let m100 = value.mod_floor(&BigInt::from(100)).to_u32().unwrap_or(0);
if (11..=19).contains(&m100) {
return Ok(format!("{}-ти", value));
}
let m10 = value.mod_floor(&BigInt::from(10)).to_u32().unwrap_or(0);
let suffix = match m10 {
1 => "-ви",
2 => "-ри",
7 | 8 => "-ми",
_ => "-ти",
};
Ok(format!("{}{}", value, suffix))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
let thousand = BigInt::from(1000u32);
let two_thousand = BigInt::from(2000u32);
if value < &thousand {
return Ok(self.int_to_cardinal(value));
}
if value < &two_thousand {
let thousands = value / &thousand;
let remainder = value % &thousand;
let mut result = if thousands.is_one() {
"хиляда".to_string()
} else {
format!("{} хиляди", self.int_to_cardinal(&thousands))
};
if remainder.is_positive() {
result.push(' ');
result.push_str(&self.int_to_cardinal(&remainder));
}
return Ok(result);
}
Ok(self.int_to_cardinal(value))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(self.ordinal_numeric(value))
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
let suffix = match value {
FloatValue::Float { value: v, .. } => {
if v.fract() != 0.0 || !v.is_finite() {
"-ти"
} else {
let i = BigInt::from_f64(*v).unwrap_or_else(BigInt::zero);
let m100 = i.mod_floor(&BigInt::from(100)).to_u32().unwrap_or(0);
let m10 = i.mod_floor(&BigInt::from(10)).to_u32().unwrap_or(0);
if (11..=19).contains(&m100) {
"-ти"
} else {
match m10 {
1 => "-ви",
2 => "-ри",
7 | 8 => "-ми",
_ => "-ти",
}
}
}
}
FloatValue::Decimal { value: d, .. } => {
if !d.is_integer() {
"-ти"
} else {
let i = d.with_scale(0).as_bigint_and_exponent().0;
let m100 = &i % BigInt::from(100);
let m10 = &i % BigInt::from(10);
if m100 >= BigInt::from(11) && m100 <= BigInt::from(19) {
"-ти"
} else if m10 == BigInt::one() {
"-ви"
} else if m10 == BigInt::from(2) {
"-ри"
} else if m10 == BigInt::from(7) || m10 == BigInt::from(8) {
"-ми"
} else {
"-ти"
}
}
}
};
Ok(format!("{}{}", repr_str, suffix))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
let (neg, frac, whole) = decompose_numeric(value);
if neg || whole < BigInt::from(1000) {
return self.cardinal_year_numeric(value);
}
if whole < BigInt::from(2000) {
let mut result = "хиляда".to_string();
let remainder = &whole - BigInt::from(1000);
if remainder.is_positive() || frac {
if frac {
return Err(N2WError::Key(frac_key_msg(value)));
}
result.push(' ');
result.push_str(&self.int_to_cardinal(&remainder));
}
return Ok(result);
}
self.cardinal_year_numeric(value)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value, precision } => self.to_cardinal_f64(*value, *precision),
FloatValue::Decimal { value, .. } => Ok(self.to_cardinal_bigdecimal(value)),
}
}
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.to_cardinal_f64(f, repr_precision(f))
}
fn lang_name(&self) -> &str {
"Num2Word_BG"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
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 is_integer_input = matches!(val, CurrencyValue::Int(_));
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 \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let cr1 = &forms.unit;
let cr2 = &forms.subunit;
let minus_str = if is_negative { "минус " } else { "" };
let money_str = self.int_to_cardinal(&left);
let currency_str = index_form(cr1, left.is_one())?;
if is_integer_input {
return Ok(format!("{}{} {}", minus_str, money_str, currency_str));
}
let right_int = right.as_bigint_and_exponent().0;
let cents_str = if cents {
if has_fractional_cents {
if right.is_positive() {
self.cardinal_from_decimal(&right)?
} else {
ONES[0].to_string()
}
} else if right_int.is_positive() {
self.int_to_cardinal(&right_int)
} else {
ONES[0].to_string()
}
} else {
if has_fractional_cents {
let f = right
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", right)))?;
format!("{}", f)
} else {
right_int.to_string()
}
};
let cents_currency = index_form(cr2, right == BigDecimal::one())?;
Ok(format!(
"{}{} {}{} {} {}",
minus_str, money_str, currency_str, separator, cents_str, cents_currency
))
}
}
#[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()
}
#[test]
fn currency_corpus_rows() {
let bg = LangBg::new();
let rows: Vec<(&str, CurrencyValue, &str)> = vec![
("EUR", int("0"), "нула евро"),
("EUR", int("1"), "един евро"),
("EUR", int("2"), "два евро"),
("EUR", int("100"), "сто евро"),
("EUR", float("12.34"), "дванадесет евро и тридесет и четири цента"),
("EUR", float("0.01"), "нула евро и един цент"),
("EUR", float("1.0"), "един евро и нула цента"),
("EUR", float("99.99"), "деветдесет и девет евро и деветдесет и девет цента"),
("EUR", float("1234.56"), "хиляда двеста тридесет и четири евро и петдесет и шест цента"),
("EUR", float("-12.34"), "минус дванадесет евро и тридесет и четири цента"),
("EUR", int("1000000"), "един милион евро"),
("EUR", float("0.5"), "нула евро и петдесет цента"),
("USD", int("0"), "нула долара"),
("USD", int("1"), "един долар"),
("USD", int("2"), "два долара"),
("USD", int("100"), "сто долара"),
("USD", float("12.34"), "дванадесет долара и тридесет и четири цента"),
("USD", float("0.01"), "нула долара и един цент"),
("USD", float("1.0"), "един долар и нула цента"),
("USD", float("99.99"), "деветдесет и девет долара и деветдесет и девет цента"),
("USD", float("1234.56"), "хиляда двеста тридесет и четири долара и петдесет и шест цента"),
("USD", float("-12.34"), "минус дванадесет долара и тридесет и четири цента"),
("USD", int("1000000"), "един милион долара"),
("USD", float("0.5"), "нула долара и петдесет цента"),
("GBP", int("0"), "нула паунда"),
("GBP", int("1"), "един паунд"),
("GBP", int("2"), "два паунда"),
("GBP", int("100"), "сто паунда"),
("GBP", float("12.34"), "дванадесет паунда и тридесет и четири пенса"),
("GBP", float("0.01"), "нула паунда и един пени"),
("GBP", float("1.0"), "един паунд и нула пенса"),
("GBP", float("99.99"), "деветдесет и девет паунда и деветдесет и девет пенса"),
("GBP", float("1234.56"), "хиляда двеста тридесет и четири паунда и петдесет и шест пенса"),
("GBP", float("-12.34"), "минус дванадесет паунда и тридесет и четири пенса"),
("GBP", int("1000000"), "един милион паунда"),
("GBP", float("0.5"), "нула паунда и петдесет пенса"),
("JPY", int("0"), "нула йени"),
("JPY", int("1"), "един йена"),
("JPY", int("2"), "два йени"),
("JPY", int("100"), "сто йени"),
("JPY", float("12.34"), "дванадесет йени и тридесет и четири сена"),
("JPY", float("0.01"), "нула йени и един сен"),
("JPY", float("1.0"), "един йена и нула сена"),
("JPY", float("99.99"), "деветдесет и девет йени и деветдесет и девет сена"),
("JPY", float("1234.56"), "хиляда двеста тридесет и четири йени и петдесет и шест сена"),
("JPY", float("-12.34"), "минус дванадесет йени и тридесет и четири сена"),
("JPY", int("1000000"), "един милион йени"),
("JPY", float("0.5"), "нула йени и петдесет сена"),
];
for (code, v, want) in rows {
let got = bg.to_currency(&v, code, true, None, false).unwrap();
assert_eq!(got, want, "currency:{} {:?}", code, v);
}
}
#[test]
fn currency_bgn_default() {
let bg = LangBg::new();
let d = bg.default_currency();
assert_eq!(d, "BGN");
assert_eq!(bg.to_currency(&int("1"), d, true, None, false).unwrap(), "един лев");
assert_eq!(bg.to_currency(&int("2"), d, true, None, false).unwrap(), "два лева");
assert_eq!(
bg.to_currency(&float("1.0"), d, true, None, false).unwrap(),
"един лев и нула стотинки"
);
assert_eq!(
bg.to_currency(&float("0.01"), d, true, None, false).unwrap(),
"нула лева и един стотинка"
);
}
#[test]
fn currency_unknown_code_uses_bg_message() {
let bg = LangBg::new();
for code in ["KWD", "BHD", "INR", "CNY", "CHF"] {
for v in [int("0"), int("1000000"), float("12.34"), float("-12.34")] {
match bg.to_currency(&v, code, true, None, false) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency \"{}\" not implemented for \"Num2Word_BG\"", code)
),
other => panic!("currency:{} {:?}: expected NotImplemented, got {:?}", code, v, other),
}
}
}
}
#[test]
fn cheque_corpus_rows() {
let bg = LangBg::new();
let val = BigDecimal::from_str("1234.56").unwrap();
for (code, want) in [
("EUR", "ХИЛЯДА ДВЕСТА ТРИДЕСЕТ И ЧЕТИРИ AND 56/100 ЕВРО"),
("USD", "ХИЛЯДА ДВЕСТА ТРИДЕСЕТ И ЧЕТИРИ AND 56/100 ДОЛАРА"),
("GBP", "ХИЛЯДА ДВЕСТА ТРИДЕСЕТ И ЧЕТИРИ AND 56/100 ПАУНДА"),
("JPY", "ХИЛЯДА ДВЕСТА ТРИДЕСЕТ И ЧЕТИРИ AND 56/100 ЙЕНИ"),
] {
assert_eq!(bg.to_cheque(&val, code).unwrap(), want, "cheque:{}", code);
}
}
#[test]
fn cheque_unknown_code_uses_base_message() {
let bg = LangBg::new();
let val = BigDecimal::from_str("1234.56").unwrap();
for code in ["KWD", "BHD", "INR", "CNY", "CHF"] {
match bg.to_cheque(&val, code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{}\" not implemented for \"Num2Word_BG\"", code)
),
other => panic!("cheque:{}: expected NotImplemented, got {:?}", code, other),
}
}
}
#[test]
fn adjective_is_ignored() {
let bg = LangBg::new();
let v = float("12.34");
assert_eq!(
bg.to_currency(&v, "USD", true, None, true).unwrap(),
bg.to_currency(&v, "USD", true, None, false).unwrap(),
);
assert!(bg.currency_adjective("USD").is_none());
}
#[test]
fn float_ignores_has_decimal() {
let bg = LangBg::new();
let with = CurrencyValue::parse("1.0", false, true, true).unwrap();
let without = CurrencyValue::parse("1.0", false, false, false).unwrap();
assert_eq!(
bg.to_currency(&with, "EUR", true, None, false).unwrap(),
"един евро и нула цента"
);
assert_eq!(
bg.to_currency(&without, "EUR", true, None, false).unwrap(),
"един евро и нула цента"
);
}
#[test]
fn cents_terse_path_is_unpadded_str() {
let bg = LangBg::new();
assert_eq!(
bg.to_currency(&float("12.34"), "USD", false, None, false).unwrap(),
"дванадесет долара и 34 цента"
);
assert_eq!(
bg.to_currency(&float("0.05"), "USD", false, None, false).unwrap(),
"нула долара и 5 цента"
);
}
#[test]
fn explicit_separator_overrides_default() {
let bg = LangBg::new();
assert_eq!(bg.default_separator(), " и");
assert_eq!(
bg.to_currency(&float("12.34"), "USD", true, Some(","), false).unwrap(),
"дванадесет долара, тридесет и четири цента"
);
}
#[test]
fn pluralize_is_unreachable_and_still_raises() {
let bg = LangBg::new();
let forms = vec!["лев".to_string(), "лева".to_string()];
assert!(matches!(
bg.pluralize(&BigInt::from(1), &forms),
Err(N2WError::NotImplemented(_))
));
}
fn f(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 bg = LangBg::new();
let rows: Vec<(f64, u32, &str)> = vec![
(0.0, 1, "нула"),
(0.5, 1, "нула точка пет"),
(1.0, 1, "един"),
(1.5, 1, "един точка пет"),
(2.25, 2, "два точка две пет"),
(3.14, 2, "три точка едно четири"),
(0.01, 2, "нула точка нула едно"),
(0.1, 1, "нула точка едно"),
(0.99, 2, "нула точка девет девет"),
(1.01, 2, "един точка нула едно"),
(12.34, 2, "дванадесет точка три четири"),
(99.99, 2, "деветдесет и девет точка девет девет"),
(100.5, 1, "сто точка пет"),
(1234.56, 2, "хиляда двеста тридесет и четири точка пет шест"),
(-0.5, 1, "минус нула точка пет"),
(-1.5, 1, "минус един точка пет"),
(-12.34, 2, "минус дванадесет точка три четири"),
(1.005, 3, "един точка нула нула пет"),
(2.675, 3, "два точка шест седем пет"),
];
for (v, p, want) in rows {
assert_eq!(bg.to_cardinal_float(&f(v, p), None).unwrap(), want, "{}", v);
}
}
#[test]
fn cardinal_decimal_corpus_rows() {
let bg = LangBg::new();
let rows: Vec<(&str, u32, &str)> = vec![
("0.01", 2, "нула"),
("1.10", 2, "един"),
("12.345", 3, "дванадесет"),
(
"98746251323029.99",
2,
"деветдесет и осем хиляди седемстотин четиридесет и шест милиарда \
двеста петдесет и едно милиона триста двадесет и три хиляди двадесет и девет",
),
("0.001", 3, "нула"),
];
for (s, p, want) in rows {
assert_eq!(bg.to_cardinal_float(&dec(s, p), None).unwrap(), want, "{}", s);
}
}
#[test]
fn cardinal_decimal_negatives() {
let bg = LangBg::new();
for (s, p, want) in [
("-1.5", 1, "минус един"),
("-0.5", 1, "минус нула"),
("-12.34", 2, "минус дванадесет"),
("2.675", 3, "два"),
("-0.0", 1, "нула"),
("0.0", 1, "нула"),
] {
assert_eq!(bg.to_cardinal_float(&dec(s, p), None).unwrap(), want, "{}", s);
}
}
#[test]
fn cardinal_integral_floats() {
let bg = LangBg::new();
for (v, p, want) in [
(0.0, 1, "нула"),
(-0.0, 1, "нула"),
(1.0, 1, "един"),
(2.0, 1, "два"),
(-1.0, 1, "минус един"),
(-3.0, 1, "минус три"),
(1e21, 21, "хиляда милиарда милиарда"),
] {
assert_eq!(bg.to_cardinal_float(&f(v, p), None).unwrap(), want, "{}", v);
}
}
#[test]
fn cardinal_float_live_rows() {
let bg = LangBg::new();
for (v, p, want) in [
(1.1, 1, "един точка едно"),
(2.2, 1, "два точка две"),
(1.2, 1, "един точка две"),
(0.02, 2, "нула точка нула две"),
(9.99, 2, "девет точка девет девет"),
(-0.01, 2, "минус нула точка нула едно"),
(-0.001, 3, "минус нула точка нула нула едно"),
(0.001, 3, "нула точка нула нула едно"),
(-2.25, 2, "минус два точка две пет"),
(-100.5, 1, "минус сто точка пет"),
(-1234.56, 2, "минус хиляда двеста тридесет и четири точка пет шест"),
(1000000.5, 1, "един милион точка пет"),
(1e-7, 7, "нула точка нула нула нула нула нула нула едно"),
(
0.5000000001,
10,
"нула точка пет нула нула нула нула нула нула нула нула едно",
),
(
123456789.123,
3,
"сто двадесет и три милиона четиристотин петдесет и шест хиляди \
седемстотин осемдесет и девет точка едно две три",
),
] {
assert_eq!(bg.to_cardinal_float(&f(v, p), None).unwrap(), want, "{}", v);
}
}
#[test]
fn precision_override_is_ignored() {
let bg = LangBg::new();
for p in [None, Some(1), Some(3), Some(5), Some(0)] {
assert_eq!(
bg.to_cardinal_float(&f(1.005, 3), p).unwrap(),
"един точка нула нула пет"
);
assert_eq!(
bg.to_cardinal_float(&f(2.675, 3), p).unwrap(),
"два точка шест седем пет"
);
assert_eq!(bg.to_cardinal_float(&dec("12.345", 3), p).unwrap(), "дванадесет");
}
}
#[test]
fn float2tuple_overflow_is_swallowed() {
let bg = LangBg::new();
assert_eq!(bg.to_cardinal_float(&f(5e-324, 324), None).unwrap(), "нула");
assert_eq!(bg.to_cardinal_float(&f(-5e-324, 324), None).unwrap(), "нула");
assert_eq!(bg.to_cardinal_float(&f(1e-310, 310), None).unwrap(), "нула");
assert_eq!(bg.to_cardinal_float(&f(-1e-310, 310), None).unwrap(), "нула");
let got = bg.to_cardinal_float(&f(-1e-308, 308), None).unwrap();
assert!(got.starts_with("минус нула точка нула"), "{}", got);
assert!(got.ends_with(" едно"), "{}", got);
assert_eq!(got.split(' ').count(), 3 + 308);
}
#[test]
fn non_finite_floats_raise_pythons_types() {
let bg = LangBg::new();
for v in [f64::INFINITY, f64::NEG_INFINITY] {
match bg.to_cardinal_float(&f(v, 0), None) {
Err(N2WError::Overflow(m)) => {
assert_eq!(m, "cannot convert float infinity to integer")
}
other => panic!("{}: expected Overflow, got {:?}", v, other),
}
}
match bg.to_cardinal_float(&f(f64::NAN, 0), None) {
Err(N2WError::Value(m)) => assert_eq!(m, "cannot convert float NaN to integer"),
other => panic!("nan: expected Value, got {:?}", other),
}
}
#[test]
fn currency_fractional_cents() {
let bg = LangBg::new();
for (arg, code, want) in [
("1.011", "USD", "един долар и един точка едно цента"),
("1.021", "USD", "един долар и два точка едно цента"),
("0.005", "USD", "нула долара и нула точка пет цента"),
("2.567", "USD", "два долара и петдесет и шест точка седем цента"),
("3.0125", "USD", "три долара и един точка две пет цента"),
("0.0075", "USD", "нула долара и нула точка седем пет цента"),
("-1.011", "USD", "минус един долар и един точка едно цента"),
("12.345", "USD", "дванадесет долара и тридесет и четири точка пет цента"),
("0.001", "USD", "нула долара и нула точка едно цента"),
("9.999", "USD", "девет долара и деветдесет и девет точка девет цента"),
("1.011", "BGN", "един лев и един точка едно стотинки"),
("2.567", "BGN", "два лева и петдесет и шест точка седем стотинки"),
] {
assert_eq!(
bg.to_currency(&float(arg), code, true, None, false).unwrap(),
want,
"{} {}",
arg,
code
);
}
}
}