use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const NEGWORD: &str = "tsis txaus ";
const ONES: [&str; 10] = [
"", "ib", "ob", "peb", "plaub", "tsib", "rau", "xya", "yim", "cuaj",
];
const TENS: [&str; 10] = [
"",
"kaum",
"nees nkaum",
"peb caug",
"plaub caug",
"tsib caug",
"rau caum",
"xya caum",
"yim caum",
"cuaj caum",
];
const HUNDRED: &str = "puas";
const THOUSAND: &str = "txhiab";
const MILLION: &str = "tawm rau";
const ZERO_WORD: &str = "xoom";
const FALLBACK_CODE: &str = "USD";
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"USD",
CurrencyForms::new(&["nyiaj kub", "nyiaj kub"], &["xees", "xees"]),
);
m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &["xees", "xees"]));
m.insert("LAK", CurrencyForms::new(&["kib", "kib"], &["att", "att"]));
m
}
fn split_currency(val: &CurrencyValue) -> Result<(BigInt, BigInt)> {
let (d, has_decimal) = match val {
CurrencyValue::Int(v) => return Ok((v.abs(), BigInt::zero())),
CurrencyValue::Decimal { value, has_decimal, .. } => (value.abs(), *has_decimal),
};
let (digits, scale) = d.as_bigint_and_exponent();
let s = digits.to_string();
let ndigits = s.len() as i64;
let adjusted = ndigits - 1 - scale;
let scientific = if scale < 0 {
true
} else if scale == 0 {
has_decimal
} else if !has_decimal {
true
} else {
adjusted < -6
};
if scientific {
if ndigits < 3 {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
d
)));
}
let left = s[0..1].parse::<BigInt>().unwrap_or_else(|_| BigInt::zero());
let right = s[1..3].parse::<BigInt>().unwrap_or_else(|_| BigInt::zero());
return Ok((left, right));
}
if scale == 0 {
return Ok((digits, BigInt::zero()));
}
let scale = scale as usize;
let (int_part, frac_part) = if s.len() > scale {
let (a, b) = s.split_at(s.len() - scale);
(a.to_string(), b.to_string())
} else {
("0".to_string(), format!("{:0>width$}", s, width = scale))
};
let left = int_part.parse::<BigInt>().unwrap_or_else(|_| BigInt::zero());
let head: String = frac_part.chars().take(2).collect();
let right = format!("{:0<2}", head)
.parse::<BigInt>()
.unwrap_or_else(|_| BigInt::zero());
Ok((left, right))
}
pub struct LangHmn {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangHmn {
pub fn new() -> Self {
LangHmn {
currency_forms: build_currency_forms(),
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
let ten = BigInt::from(10);
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let million = BigInt::from(1_000_000);
let billion = BigInt::from(1_000_000_000);
if number < &ten {
return ONES[idx(number)].to_string();
}
if number < &hundred {
let (t, o) = number.div_mod_floor(&ten);
let mut out = TENS[idx(&t)].to_string();
if !o.is_zero() {
out.push_str(" thiab ");
out.push_str(ONES[idx(&o)]);
}
return out;
}
if number < &thousand {
let (h, r) = number.div_mod_floor(&hundred);
let mut out = format!("{} {}", ONES[idx(&h)], HUNDRED);
if !r.is_zero() {
out.push_str(" thiab ");
out.push_str(&self.int_to_word(&r));
}
return out;
}
if number < &million {
let (t, r) = number.div_mod_floor(&thousand);
let mut out = format!("{} {}", self.int_to_word(&t), THOUSAND);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
if number < &billion {
let (m, r) = number.div_mod_floor(&million);
let mut out = format!("{} {}", self.int_to_word(&m), MILLION);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
number.to_string()
}
fn cardinal_from_str(&self, s: &str) -> Result<String> {
let s = s.trim();
if let Some(rest) = s.strip_prefix('-') {
let inner = self.cardinal_from_str(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
if let Some(dot) = s.find('.') {
let left = &s[..dot];
let right = &s[dot + 1..];
let mut ret = format!("{} {}", self.int_to_word(&parse_pyint(left)?), self.pointword());
for ch in right.chars() {
let d = pydigit(ch)?;
ret.push(' ');
ret.push_str(if d == 0 { ZERO_WORD } else { ONES[d as usize] });
}
return Ok(ret.trim().to_string());
}
Ok(self.int_to_word(&parse_pyint(s)?))
}
}
impl Default for LangHmn {
fn default() -> Self {
Self::new()
}
}
fn idx(n: &BigInt) -> usize {
n.to_usize().expect("guarded to 0..=9 by the caller")
}
fn parse_pyint(s: &str) -> Result<BigInt> {
s.parse::<BigInt>().map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
})
}
fn pydigit(ch: char) -> Result<u32> {
ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})
}
fn float_repr(value: f64, precision: u32) -> String {
let neg = value.is_sign_negative();
let abs = value.abs();
let body = if abs.is_finite() && abs != 0.0 {
let es = format!("{:e}", abs);
let epos = es.find('e').expect("LowerExp of a finite nonzero has 'e'");
let exp: i32 = es[epos + 1..]
.parse()
.expect("LowerExp exponent is an integer");
if exp >= 16 || exp <= -5 {
let mantissa = &es[..epos];
let sign = if exp < 0 { "-" } else { "+" };
format!("{}e{}{:02}", mantissa, sign, exp.abs())
} else {
format!("{:.*}", precision as usize, abs)
}
} else {
format!("{:.*}", precision as usize, abs)
};
if neg {
format!("-{}", body)
} else {
body
}
}
fn decimal_repr(value: &bigdecimal::BigDecimal) -> String {
let (int_val, scale) = value.as_bigint_and_exponent();
let exp = -scale; let sign = if int_val.is_negative() { "-" } else { "" };
let digits = int_val.magnitude().to_string(); let ndigits = digits.len() as i64;
let leftdigits = exp + ndigits;
let dotplace = if exp <= 0 && leftdigits > -6 {
leftdigits
} else {
1
};
let (intpart, fracpart) = if dotplace <= 0 {
(
"0".to_string(),
format!(".{}{}", "0".repeat((-dotplace) as usize), digits),
)
} else if dotplace >= ndigits {
(
format!("{}{}", digits, "0".repeat((dotplace - ndigits) as usize)),
String::new(),
)
} else {
let dp = dotplace as usize;
(digits[..dp].to_string(), format!(".{}", &digits[dp..]))
};
let expstr = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
format!("{}{}{}{}", sign, intpart, fracpart, expstr)
}
impl Lang for LangHmn {
fn cardinal_float_entry(
&self,
value: &crate::floatpath::FloatValue,
precision_override: Option<u32>,
) -> crate::base::Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let is_one = match value {
FloatValue::Float { value: f, .. } => *f == 1.0,
FloatValue::Decimal { value: d, .. } => d == &bigdecimal::BigDecimal::from(1),
};
if is_one {
return Ok("thawj".to_string());
}
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(format!("thib {}", cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("thib {}", repr_str))
}
fn str_to_number(&self, s: &str) -> Result<crate::strnum::ParsedNumber> {
match crate::strnum::python_decimal_parse(s)? {
crate::strnum::ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
p => Ok(p),
}
}
fn default_currency(&self) -> &str {
"USD"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"lab"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = self.to_cardinal(&value.abs())?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(self.int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value.is_one() {
return Ok("thawj".to_string());
}
Ok(format!("thib {}", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("thib {}", value))
}
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 s = match value {
FloatValue::Float { value, precision } => float_repr(*value, *precision),
FloatValue::Decimal { value, .. } => decimal_repr(value),
};
self.cardinal_from_str(&s)
}
fn lang_name(&self) -> &str {
"Num2Word_HMN"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
match forms.last() {
None => Ok(String::new()),
Some(last) => Ok(if n.is_one() {
forms[0].clone()
} else {
last.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) = split_currency(val)?;
let forms = self
.currency_forms
.get(currency)
.or_else(|| self.currency_forms.get(FALLBACK_CODE))
.expect("FALLBACK_CODE is inserted by build_currency_forms");
let one = BigInt::one();
let unit = pick(&forms.unit, &left, &one)?;
let mut result = format!("{} {}", self.int_to_word(&left), unit);
if cents && !right.is_zero() {
let subunit = pick(&forms.subunit, &right, &one)?;
result.push_str(&format!(
"{}{} {}",
separator,
self.int_to_word(&right),
subunit
));
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
fn pick(forms: &[String], n: &BigInt, one: &BigInt) -> Result<String> {
let i = if n != one { 1 } else { 0 };
forms
.get(i)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
#[cfg(test)]
mod currency_tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn cur(arg: &str, code: &str) -> Result<String> {
let is_int = !arg.contains('.') && !arg.to_lowercase().contains('e');
let has_decimal = !is_int;
let v = CurrencyValue::parse(arg, is_int, has_decimal, has_decimal).unwrap();
LangHmn::new().to_currency(&v, code, true, None, false)
}
fn cheque(arg: &str, code: &str) -> Result<String> {
LangHmn::new().to_cheque(&BigDecimal::from_str(arg).unwrap(), code)
}
#[test]
fn corpus_currency() {
for (code, unit, sub) in [
("USD", "nyiaj kub", "xees"),
("EUR", "euro", "xees"),
("LAK", "kib", "att"),
] {
assert_eq!(cur("0", code).unwrap(), format!("xoom {}", unit));
assert_eq!(cur("1", code).unwrap(), format!("ib {}", unit));
assert_eq!(cur("2", code).unwrap(), format!("ob {}", unit));
assert_eq!(cur("100", code).unwrap(), format!("ib puas {}", unit));
assert_eq!(cur("1000000", code).unwrap(), format!("ib tawm rau {}", unit));
assert_eq!(
cur("12.34", code).unwrap(),
format!("kaum thiab ob {} peb caug thiab plaub {}", unit, sub)
);
assert_eq!(cur("0.01", code).unwrap(), format!("xoom {} ib {}", unit, sub));
assert_eq!(cur("1.0", code).unwrap(), format!("ib {}", unit));
assert_eq!(
cur("99.99", code).unwrap(),
format!(
"cuaj caum thiab cuaj {} cuaj caum thiab cuaj {}",
unit, sub
)
);
assert_eq!(
cur("1234.56", code).unwrap(),
format!(
"ib txhiab ob puas thiab peb caug thiab plaub {} tsib caug thiab rau {}",
unit, sub
)
);
assert_eq!(
cur("-12.34", code).unwrap(),
format!(
"tsis txaus kaum thiab ob {} peb caug thiab plaub {}",
unit, sub
)
);
assert_eq!(cur("0.5", code).unwrap(), format!("xoom {} tsib caug {}", unit, sub));
}
}
#[test]
fn corpus_currency_unknown_code_falls_back_to_usd() {
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
assert_eq!(cur("0", code).unwrap(), "xoom nyiaj kub");
assert_eq!(cur("1", code).unwrap(), "ib nyiaj kub");
assert_eq!(cur("2", code).unwrap(), "ob nyiaj kub");
assert_eq!(cur("100", code).unwrap(), "ib puas nyiaj kub");
assert_eq!(cur("1000000", code).unwrap(), "ib tawm rau nyiaj kub");
assert_eq!(
cur("12.34", code).unwrap(),
"kaum thiab ob nyiaj kub peb caug thiab plaub xees"
);
assert_eq!(cur("0.01", code).unwrap(), "xoom nyiaj kub ib xees");
assert_eq!(cur("1.0", code).unwrap(), "ib nyiaj kub");
assert_eq!(
cur("99.99", code).unwrap(),
"cuaj caum thiab cuaj nyiaj kub cuaj caum thiab cuaj xees"
);
assert_eq!(
cur("1234.56", code).unwrap(),
"ib txhiab ob puas thiab peb caug thiab plaub nyiaj kub tsib caug thiab rau xees"
);
assert_eq!(
cur("-12.34", code).unwrap(),
"tsis txaus kaum thiab ob nyiaj kub peb caug thiab plaub xees"
);
assert_eq!(cur("0.5", code).unwrap(), "xoom nyiaj kub tsib caug xees");
}
}
#[test]
fn corpus_cheque() {
assert_eq!(
cheque("1234.56", "USD").unwrap(),
"IB TXHIAB OB PUAS THIAB PEB CAUG THIAB PLAUB AND 56/100 NYIAJ KUB"
);
assert_eq!(
cheque("1234.56", "EUR").unwrap(),
"IB TXHIAB OB PUAS THIAB PEB CAUG THIAB PLAUB AND 56/100 EURO"
);
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_HMN\"",
code
)
),
other => panic!("{}: expected NotImplementedError, got {:?}", code, other),
}
}
}
#[test]
fn python_parity_edge_cases() {
assert_eq!(cur("2.675", "USD").unwrap(), "ob nyiaj kub rau caum thiab xya xees");
assert_eq!(cur("1.005", "USD").unwrap(), "ib nyiaj kub");
assert_eq!(
cur("1.999", "USD").unwrap(),
"ib nyiaj kub cuaj caum thiab cuaj xees"
);
assert_eq!(cur("0.0001", "USD").unwrap(), "xoom nyiaj kub");
assert_eq!(cur("-0.001", "USD").unwrap(), "tsis txaus xoom nyiaj kub");
assert_eq!(cur("0.009", "USD").unwrap(), "xoom nyiaj kub");
assert_eq!(cur("-0.0", "USD").unwrap(), "xoom nyiaj kub");
assert_eq!(cur("0.0", "USD").unwrap(), "xoom nyiaj kub");
assert_eq!(cur("-1", "USD").unwrap(), "tsis txaus ib nyiaj kub");
assert_eq!(cur("1.01", "USD").unwrap(), "ib nyiaj kub ib xees");
assert_eq!(cur("1.1", "USD").unwrap(), "ib nyiaj kub kaum xees");
assert_eq!(cur("0.1", "USD").unwrap(), "xoom nyiaj kub kaum xees");
let d5 = CurrencyValue::parse("5", false, false, false).unwrap();
assert_eq!(
LangHmn::new().to_currency(&d5, "USD", true, None, false).unwrap(),
"tsib nyiaj kub"
);
let d500 = CurrencyValue::parse("5.00", false, true, true).unwrap();
assert_eq!(
LangHmn::new().to_currency(&d500, "USD", true, None, false).unwrap(),
"tsib nyiaj kub"
);
assert_eq!(cur("1000000000", "USD").unwrap(), "1000000000 nyiaj kub");
assert_eq!(cur("1000000000.0", "USD").unwrap(), "1000000000 nyiaj kub");
}
#[test]
fn exponential_repr_raises_value_error() {
for (arg, has_decimal) in [
("1e+21", true), ("1e+16", true), ("-1e+16", true), ("1.5e+17", true), ("1E+3", false), ("15E+3", false), ("1E-7", false), ("0E-7", false), ("1.5E-7", true), ] {
let v = CurrencyValue::parse(arg, false, has_decimal, has_decimal).unwrap();
match LangHmn::new().to_currency(&v, "USD", true, None, false) {
Err(N2WError::Value(_)) => {}
other => panic!("{}: expected ValueError, got {:?}", arg, other),
}
}
}
#[test]
fn exponential_repr_with_long_coefficient_misreads_as_cents() {
for (arg, has_decimal, want) in [
("1.01E+7", false, "ib nyiaj kub ib xees"),
("1.10E+7", false, "ib nyiaj kub kaum xees"),
("2.23E+11", false, "ob nyiaj kub nees nkaum thiab peb xees"),
("3.09E+9", false, "peb nyiaj kub cuaj xees"),
("1.2345E+20", false, "ib nyiaj kub nees nkaum thiab peb xees"),
("1.01e+17", true, "ib nyiaj kub ib xees"),
("1.2345678901234568e+16", true, "ib nyiaj kub nees nkaum thiab peb xees"),
] {
let v = CurrencyValue::parse(arg, false, has_decimal, has_decimal).unwrap();
assert_eq!(
LangHmn::new().to_currency(&v, "USD", true, None, false).unwrap(),
want,
"{}",
arg
);
}
}
#[test]
fn plain_reprs_near_the_exponential_window_do_not_raise() {
for arg in ["0.000001", "0.00001", "0.0001", "0.0"] {
let v = CurrencyValue::parse(arg, false, true, true).unwrap();
assert_eq!(
LangHmn::new().to_currency(&v, "USD", true, None, false).unwrap(),
"xoom nyiaj kub",
"{}",
arg
);
}
for (arg, want) in [("5", "tsib nyiaj kub"), ("1000", "ib txhiab nyiaj kub")] {
let v = CurrencyValue::parse(arg, false, false, false).unwrap();
assert_eq!(
LangHmn::new().to_currency(&v, "USD", true, None, false).unwrap(),
want
);
}
}
#[test]
fn kwargs() {
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
let l = LangHmn::new();
assert_eq!(
l.to_currency(&v, "USD", false, None, false).unwrap(),
"kaum thiab ob nyiaj kub"
);
assert_eq!(
l.to_currency(&v, "USD", true, Some(","), false).unwrap(),
"kaum thiab ob nyiaj kub,peb caug thiab plaub xees"
);
assert_eq!(
l.to_currency(&v, "USD", true, None, true).unwrap(),
"kaum thiab ob nyiaj kub peb caug thiab plaub xees"
);
assert_eq!(l.default_separator(), " ");
}
#[test]
fn pluralize_is_last_form_not_second() {
let l = LangHmn::new();
let three: Vec<String> = ["a", "b", "c"].iter().map(|s| s.to_string()).collect();
assert_eq!(l.pluralize(&BigInt::from(1), &three).unwrap(), "a");
assert_eq!(l.pluralize(&BigInt::from(2), &three).unwrap(), "c");
assert_eq!(l.pluralize(&BigInt::from(2), &[]).unwrap(), "");
assert_eq!(l.pluralize(&BigInt::from(1), &[]).unwrap(), "");
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn f(value: f64, precision: u32) -> Result<String> {
LangHmn::new().to_cardinal_float(&FloatValue::Float { value, precision }, None)
}
fn d(s: &str) -> Result<String> {
LangHmn::new().to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
},
None,
)
}
#[test]
fn corpus_cardinal_float() {
assert_eq!(f(0.0, 1).unwrap(), "xoom lab xoom");
assert_eq!(f(0.5, 1).unwrap(), "xoom lab tsib");
assert_eq!(f(1.0, 1).unwrap(), "ib lab xoom");
assert_eq!(f(1.5, 1).unwrap(), "ib lab tsib");
assert_eq!(f(2.25, 2).unwrap(), "ob lab ob tsib");
assert_eq!(f(3.14, 2).unwrap(), "peb lab ib plaub");
assert_eq!(f(0.01, 2).unwrap(), "xoom lab xoom ib");
assert_eq!(f(0.1, 1).unwrap(), "xoom lab ib");
assert_eq!(f(0.99, 2).unwrap(), "xoom lab cuaj cuaj");
assert_eq!(f(1.01, 2).unwrap(), "ib lab xoom ib");
assert_eq!(f(12.34, 2).unwrap(), "kaum thiab ob lab peb plaub");
assert_eq!(f(99.99, 2).unwrap(), "cuaj caum thiab cuaj lab cuaj cuaj");
assert_eq!(f(100.5, 1).unwrap(), "ib puas lab tsib");
assert_eq!(
f(1234.56, 2).unwrap(),
"ib txhiab ob puas thiab peb caug thiab plaub lab tsib rau"
);
assert_eq!(f(-0.5, 1).unwrap(), "tsis txaus xoom lab tsib");
assert_eq!(f(-1.5, 1).unwrap(), "tsis txaus ib lab tsib");
assert_eq!(f(-12.34, 2).unwrap(), "tsis txaus kaum thiab ob lab peb plaub");
assert_eq!(f(1.005, 3).unwrap(), "ib lab xoom xoom tsib");
assert_eq!(f(2.675, 3).unwrap(), "ob lab rau xya tsib");
}
#[test]
fn corpus_cardinal_dec() {
assert_eq!(d("0.01").unwrap(), "xoom lab xoom ib");
assert_eq!(d("1.10").unwrap(), "ib lab ib xoom");
assert_eq!(d("12.345").unwrap(), "kaum thiab ob lab peb plaub tsib");
assert_eq!(d("98746251323029.99").unwrap(), "98746251323029 lab cuaj cuaj");
assert_eq!(d("0.001").unwrap(), "xoom lab xoom xoom ib");
}
#[test]
fn neg_zero_float_keeps_negword() {
assert_eq!(f(-0.0, 1).unwrap(), "tsis txaus xoom lab xoom");
assert_eq!(f(0.0, 1).unwrap(), "xoom lab xoom");
}
#[test]
fn decimal_str_parity() {
assert_eq!(d("5.00").unwrap(), "tsib lab xoom xoom");
assert_eq!(d("1.100").unwrap(), "ib lab ib xoom xoom");
assert_eq!(d("0.10").unwrap(), "xoom lab ib xoom");
assert_eq!(d("2.50").unwrap(), "ob lab tsib xoom");
assert_eq!(d("-0.001").unwrap(), "tsis txaus xoom lab xoom xoom ib");
assert_eq!(d("-12.34").unwrap(), "tsis txaus kaum thiab ob lab peb plaub");
assert_eq!(d("100").unwrap(), "ib puas");
assert_eq!(d("0.0").unwrap(), "xoom lab xoom");
assert_eq!(d("1000000000.5").unwrap(), "1000000000 lab tsib");
}
#[test]
fn decimal_plain_near_window() {
assert_eq!(d("0.000001").unwrap(), "xoom lab xoom xoom xoom xoom xoom ib");
assert_eq!(d("1E-6").unwrap(), "xoom lab xoom xoom xoom xoom xoom ib");
assert_eq!(d("1E-5").unwrap(), "xoom lab xoom xoom xoom xoom ib");
assert_eq!(
d("1.00E-5").unwrap(),
"xoom lab xoom xoom xoom xoom ib xoom xoom"
);
}
#[test]
fn scientific_float_raises_value_error() {
for (v, p) in [
(1e16, 16u32),
(1e-5, 5),
(1.5e17, 16),
(9.999e-5, 5),
(1e21, 21),
(-1e16, 16),
] {
match f(v, p) {
Err(N2WError::Value(_)) => {}
other => panic!("{:e}: expected ValueError, got {:?}", v, other),
}
}
assert_eq!(f(0.0001, 4).unwrap(), "xoom lab xoom xoom xoom ib");
assert_eq!(
f(1e15, 1).unwrap(),
"1000000000000000 lab xoom"
);
}
#[test]
fn scientific_decimal_raises_value_error() {
for s in ["1E+3", "1E-7", "1.5E+3", "0.0000001", "1E+2", "1E-8", "1.23E+11"] {
match d(s) {
Err(N2WError::Value(_)) => {}
other => panic!("{}: expected ValueError, got {:?}", s, other),
}
}
}
#[test]
fn neg_zero_decimal_loses_sign_known_divergence() {
assert_eq!(d("-0.0").unwrap(), "xoom lab xoom");
assert_eq!(d("-0").unwrap(), "xoom");
}
#[test]
fn precision_override_ignored() {
let v = FloatValue::Float {
value: 12.34,
precision: 2,
};
let l = LangHmn::new();
assert_eq!(
l.to_cardinal_float(&v, None).unwrap(),
l.to_cardinal_float(&v, Some(5)).unwrap()
);
assert_eq!(
l.to_cardinal_float(&v, Some(5)).unwrap(),
"kaum thiab ob lab peb plaub"
);
}
}