use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const NEGWORD: &str = "минус ";
const POINTWORD: &str = "үтүр";
const ZERO_WORD: &str = "нөл";
const DEFAULT_ORDINAL_SUFFIX: Suffix = Suffix {
after_consonant: "инчи",
after_vowel: "нчи",
};
struct Suffix {
after_consonant: &'static str,
after_vowel: &'static str,
}
const SUFFIX_BACK_UNROUNDED: Suffix = Suffix {
after_consonant: "ынчы",
after_vowel: "нчы",
};
const SUFFIX_FRONT_UNROUNDED: Suffix = Suffix {
after_consonant: "инчи",
after_vowel: "нчи",
};
const SUFFIX_BACK_ROUNDED: Suffix = Suffix {
after_consonant: "унчу",
after_vowel: "нчу",
};
const SUFFIX_FRONT_ROUNDED: Suffix = Suffix {
after_consonant: "үнчү",
after_vowel: "нчү",
};
fn vowel_class(c: char) -> Option<&'static Suffix> {
match c {
'а' | 'ы' | 'я' => Some(&SUFFIX_BACK_UNROUNDED),
'е' | 'и' | 'э' => Some(&SUFFIX_FRONT_UNROUNDED),
'о' | 'у' | 'ё' | 'ю' => Some(&SUFFIX_BACK_ROUNDED),
'ө' | 'ү' => Some(&SUFFIX_FRONT_ROUNDED),
_ => None,
}
}
fn ordinal_suffix(cardinal: &str) -> &'static str {
harmonise(cardinal).0
}
fn harmonise(cardinal: &str) -> (&'static str, bool) {
let stem = match cardinal.split_whitespace().next_back() {
Some(w) => w,
None => return (DEFAULT_ORDINAL_SUFFIX.after_consonant, false),
};
let mut class: Option<&'static Suffix> = None;
let mut last_is_vowel = false;
for c in stem.chars() {
match vowel_class(c) {
Some(k) => {
class = Some(k);
last_is_vowel = true;
}
None => last_is_vowel = false,
}
}
match class {
Some(k) if last_is_vowel => (k.after_vowel, true),
Some(k) => (k.after_consonant, true),
None => (DEFAULT_ORDINAL_SUFFIX.after_consonant, false),
}
}
fn join_ordinal(cardinal: &str) -> String {
let (suffix, is_word) = harmonise(cardinal);
if is_word {
format!("{}{}", cardinal, suffix)
} else {
format!("{}-{}", cardinal, suffix)
}
}
const ONES: [&str; 10] = [
"", "бир", "эки", "үч", "төрт", "беш", "алты", "жети", "сегиз", "тогуз",
];
const TENS: [&str; 10] = [
"", "он", "жыйырма", "отуз", "кырк", "элүү", "алтымыш", "жетимиш", "сексен", "токсон",
];
const HUNDRED: &str = "жүз";
const THOUSAND: &str = "миң";
const MILLION: &str = "миллион";
fn one_e9() -> BigInt {
BigInt::from(1_000_000_000u64)
}
const FALLBACK_CURRENCY: &str = "KGS";
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("KGS", CurrencyForms::new(&["сом", "сом"], &["тыйын", "тыйын"]));
m.insert("USD", CurrencyForms::new(&["доллар", "доллар"], &["сент", "сент"]));
m.insert("EUR", CurrencyForms::new(&["евро", "евро"], &["сент", "сент"]));
m.insert("RUB", CurrencyForms::new(&["рубль", "рубль"], &["копейка", "копейка"]));
m
}
fn split_currency_parts(value: &BigDecimal) -> Result<(BigInt, BigInt)> {
let (int_val, scale) = value.as_bigint_and_exponent();
let int_val = int_val.abs();
if scale < 0 {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
value
)));
}
if scale == 0 {
return Ok((int_val, BigInt::zero()));
}
let digits = int_val.to_string();
let s = scale as usize;
let padded = if digits.len() <= s {
format!("{}{}", "0".repeat(s - digits.len() + 1), digits)
} else {
digits
};
let split = padded.len() - s;
let left: BigInt = padded[..split].parse().expect("digits only");
let mut two: String = padded[split..].chars().take(2).collect();
while two.len() < 2 {
two.push('0');
}
let right: BigInt = two.parse().expect("digits only");
Ok((left, right))
}
fn decompose_float(v: &FloatValue) -> (BigInt, String, bool) {
match v {
FloatValue::Decimal { value, .. } => {
let (int_val, scale) = value.as_bigint_and_exponent();
let abs = int_val.abs();
if scale <= 0 {
return (abs, String::new(), false);
}
let digits = abs.to_string();
let s = scale as usize;
let padded = if digits.len() <= s {
format!("{}{}", "0".repeat(s - digits.len() + 1), digits)
} else {
digits
};
let split = padded.len() - s;
let left: BigInt = padded[..split].parse().expect("digits only");
let right = padded[split..].to_string();
(left, right, true)
}
FloatValue::Float { value, precision } => {
let s = format!("{:.*}", *precision as usize, value.abs());
match s.split_once('.') {
Some((l, r)) => (l.parse().expect("digits only"), r.to_string(), true),
None => (s.parse().expect("digits only"), String::new(), false),
}
}
}
}
fn int_value_error(s: &str) -> N2WError {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
}
fn py_float_sci_repr(a: f64) -> String {
let sci = format!("{:e}", a); let (mant, exp_str) = sci.split_once('e').expect("{:e} always emits an 'e'");
let exp: i64 = exp_str.parse().expect("{:e} exponent is a base-10 integer");
format!(
"{}e{}{:02}",
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
)
}
enum StrRoute {
Point,
WholeDigits(BigInt),
}
fn route_by_str(v: &FloatValue) -> Result<StrRoute> {
match v {
FloatValue::Float { value, .. } => {
let a = value.abs();
if !a.is_finite() {
return Err(int_value_error(if a.is_nan() { "nan" } else { "inf" }));
}
if a >= 1e16 || (a > 0.0 && a < 1e-4) {
return Err(int_value_error(&py_float_sci_repr(a)));
}
Ok(StrRoute::Point)
}
FloatValue::Decimal { value, .. } => {
let s = python_decimal_str(&value.abs());
if s.contains('.') {
Ok(StrRoute::Point)
} else if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {
Ok(StrRoute::WholeDigits(s.parse().expect("digits only")))
} else {
Err(int_value_error(&s))
}
}
}
}
pub struct LangKy {
exclude_title: Vec<String>,
forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangKy {
fn default() -> Self {
Self::new()
}
}
impl LangKy {
pub fn new() -> Self {
LangKy {
exclude_title: vec![NEGWORD.trim().to_string(), POINTWORD.to_string()],
forms: build_currency_forms(),
}
}
fn int_to_word(&self, number: &BigInt) -> Result<String> {
if number.is_zero() {
return Ok(ZERO_WORD.to_string());
}
if number.is_negative() {
let wrapped = number + BigInt::from(10);
return match wrapped.to_usize() {
Some(i) if i < ONES.len() => Ok(ONES[i].to_string()),
_ => Err(N2WError::Index("list index out of range".to_string())),
};
}
if number >= &one_e9() {
return Ok(number.to_string());
}
let n = number
.to_u64()
.expect("0 < number < 10^9 was just proven, so u64 conversion cannot fail");
Ok(self.int_to_word_small(n))
}
fn int_to_word_small(&self, n: u64) -> String {
if n == 0 {
return ZERO_WORD.to_string();
}
if n < 10 {
return ONES[n as usize].to_string();
}
if n < 100 {
let (t, o) = (n / 10, n % 10);
let mut s = TENS[t as usize].to_string();
if o != 0 {
s.push(' ');
s.push_str(ONES[o as usize]);
}
return s;
}
if n < 1_000 {
let (h, r) = (n / 100, n % 100);
let mut s = String::new();
if h > 1 {
s.push_str(ONES[h as usize]);
s.push(' ');
}
s.push_str(HUNDRED);
if r != 0 {
s.push(' ');
s.push_str(&self.int_to_word_small(r));
}
return s;
}
if n < 1_000_000 {
let (t, r) = (n / 1_000, n % 1_000);
let mut s = String::new();
if t > 1 {
s.push_str(&self.int_to_word_small(t));
s.push(' ');
}
s.push_str(THOUSAND);
if r != 0 {
s.push(' ');
s.push_str(&self.int_to_word_small(r));
}
return s;
}
let (m, r) = (n / 1_000_000, n % 1_000_000);
let mut s = self.int_to_word_small(m);
s.push(' ');
s.push_str(MILLION);
if r != 0 {
s.push(' ');
s.push_str(&self.int_to_word_small(r));
}
s
}
}
impl Lang for LangKy {
fn default_currency(&self) -> &str {
"KGS"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"үтүр"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = self.to_cardinal(&-value)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
self.int_to_word(value)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
let cardinal = self.to_cardinal(value)?;
Ok(join_ordinal(&cardinal))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
let suffix = match self.to_cardinal(value) {
Ok(cardinal) => ordinal_suffix(&cardinal),
Err(_) => DEFAULT_ORDINAL_SUFFIX.after_consonant,
};
Ok(format!("{}-{}", value, suffix))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let is_negative = value.is_negative();
let (left, right, has_dot) = decompose_float(value);
let mut body = if has_dot {
let mut s = format!("{} {}", self.int_to_word(&left)?, POINTWORD);
for ch in right.chars() {
let d = ch.to_digit(10).expect("fractional part is digits only") as usize;
s.push(' ');
s.push_str(if ONES[d].is_empty() { ZERO_WORD } else { ONES[d] });
}
s
} else {
self.int_to_word(&left)?
};
if is_negative {
body = format!("{}{}", NEGWORD, body);
}
Ok(body.trim().to_string())
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
match route_by_str(value)? {
StrRoute::Point => self.to_cardinal_float(value, precision_override),
StrRoute::WholeDigits(abs_int) => {
let body = self.int_to_word(&abs_int)?;
if value.is_negative() {
Ok(format!("{}{}", NEGWORD, body).trim().to_string())
} else {
Ok(body)
}
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(join_ordinal(&cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!(
"{}-{}",
repr_str, DEFAULT_ORDINAL_SUFFIX.after_consonant
))
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
python_decimal_parse(s)
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok(format!(
"{}-{}",
if negative { "-Infinity" } else { "Infinity" },
DEFAULT_ORDINAL_SUFFIX.after_consonant
)),
_ => Err(int_value_error("Infinity")),
}
}
fn nan_result(&self, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok(format!(
"NaN-{}",
DEFAULT_ORDINAL_SUFFIX.after_consonant
)),
_ => Err(int_value_error("NaN")),
}
}
fn lang_name(&self) -> &str {
"Num2Word_KY"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
if forms.is_empty() {
return Ok(String::new());
}
Ok(if n.is_one() {
forms[0].clone()
} else {
forms[forms.len() - 1].clone()
})
}
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_negative = val.is_negative();
let (left, right) = match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value, .. } => split_currency_parts(value)?,
};
let forms = self
.forms
.get(currency)
.or_else(|| self.forms.get(FALLBACK_CURRENCY))
.expect("KGS is always present");
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let unit = index_form(cr1, &left)?;
let mut result = format!("{} {}", self.int_to_word(&left)?, unit);
if cents && !right.is_zero() {
let sub = index_form(cr2, &right)?;
result.push_str(separator);
result.push_str(&self.int_to_word(&right)?);
result.push(' ');
result.push_str(sub);
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
fn index_form<'a>(forms: &'a [String], n: &BigInt) -> Result<&'a str> {
let i = if n.is_one() { 0 } else { 1 };
forms
.get(i)
.map(|s| s.as_str())
.ok_or_else(|| N2WError::Index("tuple index out of range".to_string()))
}
#[cfg(test)]
mod currency_tests {
use super::*;
use std::str::FromStr;
fn cur(arg: &str, is_int: bool, code: &str) -> String {
let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
LangKy::new()
.to_currency(&v, code, true, None, false)
.unwrap()
}
fn cheque(arg: &str, code: &str) -> Result<String> {
LangKy::new().to_cheque(&BigDecimal::from_str(arg).unwrap(), code)
}
#[test]
fn corpus_currency_known_codes() {
assert_eq!(cur("0", true, "EUR"), "нөл евро");
assert_eq!(cur("1", true, "EUR"), "бир евро");
assert_eq!(cur("2", true, "EUR"), "эки евро");
assert_eq!(cur("100", true, "EUR"), "жүз евро");
assert_eq!(cur("1000000", true, "EUR"), "бир миллион евро");
assert_eq!(cur("12.34", false, "EUR"), "он эки евро отуз төрт сент");
assert_eq!(cur("0.01", false, "EUR"), "нөл евро бир сент");
assert_eq!(cur("0.5", false, "EUR"), "нөл евро элүү сент");
assert_eq!(
cur("99.99", false, "EUR"),
"токсон тогуз евро токсон тогуз сент"
);
assert_eq!(
cur("1234.56", false, "EUR"),
"миң эки жүз отуз төрт евро элүү алты сент"
);
assert_eq!(cur("-12.34", false, "EUR"), "минус он эки евро отуз төрт сент");
assert_eq!(cur("0", true, "USD"), "нөл доллар");
assert_eq!(cur("1", true, "USD"), "бир доллар");
assert_eq!(cur("2", true, "USD"), "эки доллар");
assert_eq!(cur("100", true, "USD"), "жүз доллар");
assert_eq!(cur("1000000", true, "USD"), "бир миллион доллар");
assert_eq!(cur("12.34", false, "USD"), "он эки доллар отуз төрт сент");
assert_eq!(cur("0.01", false, "USD"), "нөл доллар бир сент");
assert_eq!(cur("0.5", false, "USD"), "нөл доллар элүү сент");
assert_eq!(
cur("99.99", false, "USD"),
"токсон тогуз доллар токсон тогуз сент"
);
assert_eq!(
cur("1234.56", false, "USD"),
"миң эки жүз отуз төрт доллар элүү алты сент"
);
assert_eq!(
cur("-12.34", false, "USD"),
"минус он эки доллар отуз төрт сент"
);
}
#[test]
fn corpus_currency_float_with_zero_cents() {
assert_eq!(cur("1.0", false, "EUR"), "бир евро");
assert_eq!(cur("1.0", false, "USD"), "бир доллар");
assert_eq!(cur("1.0", false, "GBP"), "бир сом");
assert_eq!(cur("1", true, "EUR"), "бир евро");
}
#[test]
fn corpus_currency_unknown_code_falls_back_to_kgs() {
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
assert_eq!(cur("0", true, code), "нөл сом");
assert_eq!(cur("1", true, code), "бир сом");
assert_eq!(cur("2", true, code), "эки сом");
assert_eq!(cur("100", true, code), "жүз сом");
assert_eq!(cur("1000000", true, code), "бир миллион сом");
assert_eq!(cur("12.34", false, code), "он эки сом отуз төрт тыйын");
assert_eq!(cur("0.01", false, code), "нөл сом бир тыйын");
assert_eq!(cur("1.0", false, code), "бир сом");
assert_eq!(cur("0.5", false, code), "нөл сом элүү тыйын");
assert_eq!(
cur("99.99", false, code),
"токсон тогуз сом токсон тогуз тыйын"
);
assert_eq!(
cur("1234.56", false, code),
"миң эки жүз отуз төрт сом элүү алты тыйын"
);
assert_eq!(
cur("-12.34", false, code),
"минус он эки сом отуз төрт тыйын"
);
}
}
#[test]
fn currency_quirks_verified_against_python() {
let ky = LangKy::new();
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
assert_eq!(ky.to_currency(&v, "EUR", false, None, false).unwrap(), "он эки евро");
assert_eq!(
ky.to_currency(&v, "EUR", true, None, true).unwrap(),
"он эки евро отуз төрт сент"
);
assert_eq!(
ky.to_currency(&v, "EUR", true, Some(","), false).unwrap(),
"он эки евро,отуз төрт сент"
);
let v = CurrencyValue::parse("12.345", false, true, true).unwrap();
assert_eq!(
ky.to_currency(&v, "EUR", true, None, false).unwrap(),
"он эки евро отуз төрт сент"
);
let v = CurrencyValue::parse("5.00", false, true, true).unwrap();
assert_eq!(ky.to_currency(&v, "EUR", true, None, false).unwrap(), "беш евро");
let v = CurrencyValue::parse("100.0", false, true, true).unwrap();
assert_eq!(ky.to_currency(&v, "EUR", true, None, false).unwrap(), "жүз евро");
let v = CurrencyValue::parse("0.0001", false, true, true).unwrap();
assert_eq!(ky.to_currency(&v, "EUR", true, None, false).unwrap(), "нөл евро");
let v = CurrencyValue::parse("2.05", false, true, true).unwrap();
assert_eq!(
ky.to_currency(&v, "RUB", true, None, false).unwrap(),
"эки рубль беш копейка"
);
let v = CurrencyValue::parse("1000000000", true, false, false).unwrap();
assert_eq!(ky.to_currency(&v, "EUR", true, None, false).unwrap(), "1000000000 евро");
}
#[test]
fn currency_exponential_repr_is_valueerror() {
for arg in ["1e+21", "1e+16", "1E+2"] {
let v = CurrencyValue::parse(arg, false, true, true).unwrap();
match LangKy::new().to_currency(&v, "EUR", true, None, false) {
Err(N2WError::Value(_)) => {}
other => panic!("{} expected ValueError, got {:?}", arg, other),
}
}
assert_eq!(cur("1e-05", false, "EUR"), "нөл евро");
}
#[test]
fn corpus_cheque() {
assert_eq!(
cheque("1234.56", "EUR").unwrap(),
"МИҢ ЭКИ ЖҮЗ ОТУЗ ТӨРТ AND 56/100 ЕВРО"
);
assert_eq!(
cheque("1234.56", "USD").unwrap(),
"МИҢ ЭКИ ЖҮЗ ОТУЗ ТӨРТ AND 56/100 ДОЛЛАР"
);
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
match cheque("1234.56", code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!(
"Currency code \"{}\" not implemented for \"Num2Word_KY\"",
code
)
),
other => panic!("{} expected NotImplementedError, got {:?}", code, other),
}
}
}
#[test]
fn cheque_quirks_verified_against_python() {
assert_eq!(
cheque("1234.56", "KGS").unwrap(),
"МИҢ ЭКИ ЖҮЗ ОТУЗ ТӨРТ AND 56/100 СОМ"
);
assert_eq!(cheque("-1.0", "USD").unwrap(), "MINUS БИР AND 00/100 ДОЛЛАР");
}
#[test]
fn pluralize_matches_python() {
let ky = LangKy::new();
let forms: Vec<String> = vec!["сом".into(), "сом".into()];
assert_eq!(ky.pluralize(&BigInt::from(1), &forms).unwrap(), "сом");
assert_eq!(ky.pluralize(&BigInt::from(2), &forms).unwrap(), "сом");
let three: Vec<String> = vec!["a".into(), "b".into(), "c".into()];
assert_eq!(ky.pluralize(&BigInt::from(2), &three).unwrap(), "c");
assert_eq!(ky.pluralize(&BigInt::from(2), &[]).unwrap(), "");
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use std::str::FromStr;
fn f(value: f64, precision: u32) -> String {
LangKy::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(s: &str) -> String {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.max(0) as u32;
LangKy::new()
.to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
.unwrap()
}
#[test]
fn corpus_cardinal_float() {
assert_eq!(f(0.0, 1), "нөл үтүр нөл");
assert_eq!(f(0.5, 1), "нөл үтүр беш");
assert_eq!(f(1.0, 1), "бир үтүр нөл");
assert_eq!(f(1.5, 1), "бир үтүр беш");
assert_eq!(f(2.25, 2), "эки үтүр эки беш");
assert_eq!(f(3.14, 2), "үч үтүр бир төрт");
assert_eq!(f(0.01, 2), "нөл үтүр нөл бир");
assert_eq!(f(0.1, 1), "нөл үтүр бир");
assert_eq!(f(0.99, 2), "нөл үтүр тогуз тогуз");
assert_eq!(f(1.01, 2), "бир үтүр нөл бир");
assert_eq!(f(12.34, 2), "он эки үтүр үч төрт");
assert_eq!(f(99.99, 2), "токсон тогуз үтүр тогуз тогуз");
assert_eq!(f(100.5, 1), "жүз үтүр беш");
assert_eq!(f(1234.56, 2), "миң эки жүз отуз төрт үтүр беш алты");
assert_eq!(f(-0.5, 1), "минус нөл үтүр беш");
assert_eq!(f(-1.5, 1), "минус бир үтүр беш");
assert_eq!(f(-12.34, 2), "минус он эки үтүр үч төрт");
}
#[test]
fn corpus_cardinal_float_artefacts() {
assert_eq!(f(1.005, 3), "бир үтүр нөл нөл беш");
assert_eq!(f(2.675, 3), "эки үтүр алты жети беш");
}
#[test]
fn precision_override_is_ignored() {
let ky = LangKy::new();
let v = FloatValue::Float { value: 2.675, precision: 3 };
assert_eq!(ky.to_cardinal_float(&v, Some(1)).unwrap(), "эки үтүр алты жети беш");
assert_eq!(ky.to_cardinal_float(&v, Some(5)).unwrap(), "эки үтүр алты жети беш");
}
#[test]
fn corpus_cardinal_dec() {
assert_eq!(d("0.01"), "нөл үтүр нөл бир");
assert_eq!(d("1.10"), "бир үтүр бир нөл");
assert_eq!(d("12.345"), "он эки үтүр үч төрт беш");
assert_eq!(d("98746251323029.99"), "98746251323029 үтүр тогуз тогуз");
assert_eq!(d("0.001"), "нөл үтүр нөл нөл бир");
}
#[test]
fn cardinal_dec_quirks_verified_against_python() {
assert_eq!(d("5.00"), "беш үтүр нөл нөл");
}
}
#[cfg(test)]
mod entry_routing_tests {
use super::*;
use std::str::FromStr;
fn fv(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn dv(s: &str) -> FloatValue {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.max(0) as u32;
FloatValue::Decimal { value, precision }
}
#[test]
fn corpus_cardinal_entry() {
let ky = LangKy::new();
assert_eq!(ky.cardinal_float_entry(&fv(5.0, 1), None).unwrap(), "беш үтүр нөл");
assert_eq!(ky.cardinal_float_entry(&fv(0.0, 1), None).unwrap(), "нөл үтүр нөл");
assert_eq!(
ky.cardinal_float_entry(&fv(-1000000.0, 1), None).unwrap(),
"минус бир миллион үтүр нөл"
);
assert_eq!(
ky.cardinal_float_entry(&fv(-0.0, 1), None).unwrap(),
"минус нөл үтүр нөл"
);
assert_eq!(
ky.cardinal_float_entry(&fv(1e9, 1), None).unwrap(),
"1000000000 үтүр нөл"
);
assert_eq!(ky.cardinal_float_entry(&dv("5.00"), None).unwrap(), "беш үтүр нөл нөл");
assert_eq!(
ky.cardinal_float_entry(&dv("12345.000"), None).unwrap(),
"он эки миң үч жүз кырк беш үтүр нөл нөл нөл"
);
assert_eq!(ky.cardinal_float_entry(&dv("0"), None).unwrap(), "нөл");
assert_eq!(ky.cardinal_float_entry(&dv("100"), None).unwrap(), "жүз");
}
#[test]
fn corpus_cardinal_entry_scientific_is_valueerror() {
let ky = LangKy::new();
for (v, repr) in [(1e16, "1e+16"), (1e20, "1e+20"), (1e-5, "1e-05")] {
match ky.cardinal_float_entry(&fv(v, 0), None) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", repr)
),
other => panic!("{} expected ValueError, got {:?}", v, other),
}
}
match ky.cardinal_float_entry(&fv(-1e16, 0), None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1e+16'")
}
other => panic!("expected ValueError, got {:?}", other),
}
for (s, repr) in [("1E+2", "1E+2"), ("1E+20", "1E+20")] {
match ky.cardinal_float_entry(&dv(s), None) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", repr)
),
other => panic!("{} expected ValueError, got {:?}", s, other),
}
}
}
#[test]
fn corpus_ordinal_entry() {
let ky = LangKy::new();
assert_eq!(
ky.ordinal_float_entry(&fv(5.0, 1)).unwrap(),
"беш үтүр нөлүнчү"
);
assert_eq!(
ky.ordinal_float_entry(&fv(-0.0, 1)).unwrap(),
"минус нөл үтүр нөлүнчү"
);
assert_eq!(
ky.ordinal_float_entry(&fv(3.25, 2)).unwrap(),
"үч үтүр эки бешинчи"
);
assert_eq!(ky.ordinal_float_entry(&dv("0")).unwrap(), "нөлүнчү");
assert_eq!(
ky.ordinal_float_entry(&dv("5.00")).unwrap(),
"беш үтүр нөл нөлүнчү"
);
assert!(matches!(
ky.ordinal_float_entry(&fv(1e16, 0)),
Err(N2WError::Value(_))
));
}
#[test]
fn corpus_ordinal_num_entry() {
let ky = LangKy::new();
assert_eq!(
ky.ordinal_num_float_entry(&fv(5.0, 1), "5.0").unwrap(),
"5.0-инчи"
);
assert_eq!(
ky.ordinal_num_float_entry(&fv(-0.0, 1), "-0.0").unwrap(),
"-0.0-инчи"
);
assert_eq!(
ky.ordinal_num_float_entry(&fv(1e16, 0), "1e+16").unwrap(),
"1e+16-инчи"
);
assert_eq!(
ky.ordinal_num_float_entry(&dv("1E+2"), "1E+2").unwrap(),
"1E+2-инчи"
);
assert_eq!(
ky.ordinal_num_float_entry(&dv("5.00"), "5.00").unwrap(),
"5.00-инчи"
);
}
#[test]
fn corpus_year_entry() {
let ky = LangKy::new();
assert_eq!(ky.year_float_entry(&fv(5.0, 1)).unwrap(), "беш үтүр нөл");
assert_eq!(
ky.year_float_entry(&fv(-0.0, 1)).unwrap(),
"минус нөл үтүр нөл"
);
assert!(matches!(
ky.year_float_entry(&fv(1e20, 0)),
Err(N2WError::Value(_))
));
}
#[test]
fn str_to_number_inf_nan_falls_through() {
let ky = LangKy::new();
for s in ["Infinity", "-Infinity", "NaN"] {
assert!(ky.str_to_number(s).is_ok());
}
assert!(matches!(ky.str_to_number("5"), Ok(ParsedNumber::Dec(_))));
assert!(matches!(ky.str_to_number("1e3"), Ok(ParsedNumber::Dec(_))));
}
#[test]
fn corpus_strings_scientific_decimal_is_valueerror() {
let ky = LangKy::new();
match ky.cardinal_float_entry(&dv("1E+3"), None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1E+3'")
}
other => panic!("expected ValueError, got {:?}", other),
}
}
}