use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
const ONES: [&str; 10] = [
"",
"rimwe",
"kabiri",
"gatatu",
"kane",
"gatanu",
"gatandatu",
"karindwi",
"umunani",
"icyenda",
];
const TENS: [&str; 10] = [
"",
"icumi",
"makumyabiri",
"mirongo itatu",
"mirongo ine",
"mirongo itanu",
"mirongo itandatu",
"mirongo irindwi",
"mirongo inani",
"mirongo cyenda",
];
const HUNDRED: &str = "ijana";
const THOUSAND: &str = "igihumbi";
const MILLION: &str = "miliyoni";
const NEGWORD: &str = "munsi ya ";
const POINTWORD: &str = "akadomo";
const EXCLUDE_TITLE: [&str; 4] = ["na", "akadomo", "munsi", "ya"];
const ZERU: &str = "zeru";
const BILLION: u64 = 1_000_000_000;
fn int_to_word(value: &BigInt) -> String {
match value.to_u64() {
Some(n) if n < BILLION => int_to_word_small(n),
_ => value.to_string(),
}
}
fn int_to_word_small(n: u64) -> String {
if n == 0 {
return ZERU.to_string();
}
if n < 10 {
return ONES[n as usize].to_string();
}
if n < 100 {
let (t, o) = (n / 10, n % 10);
let mut out = TENS[t as usize].to_string();
if o != 0 {
out.push_str(" na ");
out.push_str(ONES[o as usize]);
}
return out;
}
if n < 1_000 {
let (h, r) = (n / 100, n % 100);
let mut out = String::new();
if h > 1 {
out.push_str(ONES[h as usize]);
out.push(' ');
}
out.push_str(HUNDRED);
if r != 0 {
out.push_str(" na ");
out.push_str(&int_to_word_small(r));
}
return out;
}
if n < 1_000_000 {
let (t, r) = (n / 1_000, n % 1_000);
let mut out = String::new();
if t > 1 {
out.push_str(&int_to_word_small(t));
out.push(' ');
}
out.push_str(THOUSAND);
if r != 0 {
out.push_str(" na ");
out.push_str(&int_to_word_small(r));
}
return out;
}
let (m, r) = (n / 1_000_000, n % 1_000_000);
let mut out = String::new();
if m > 1 {
out.push_str(&int_to_word_small(m));
out.push(' ');
}
out.push_str(MILLION);
if r != 0 {
out.push_str(" na ");
out.push_str(&int_to_word_small(r));
}
out
}
fn cardinal(value: &BigInt) -> String {
if value.is_negative() {
let inner = cardinal(&value.abs());
return format!("{}{}", NEGWORD, inner).trim().to_string();
}
int_to_word(value)
}
fn python_float_repr(v: f64) -> String {
if v.is_nan() {
return "nan".to_string();
}
if v.is_infinite() {
return (if v.is_sign_negative() { "-inf" } else { "inf" }).to_string();
}
let sign = if v.is_sign_negative() { "-" } else { "" };
let a = v.abs();
let s = format!("{:e}", a);
let (mant, exp) = s.split_once('e').expect("LowerExp always emits an 'e'");
let exp: i32 = exp.parse().expect("LowerExp emits an integer exponent");
let mut digits: String = mant.chars().filter(|c| *c != '.').collect();
let mut decpt = exp + 1;
let frac_digits = digits.chars().count() as i32 - decpt;
if frac_digits > 0 && a != 0.0 {
let t = format!("{:.*}", frac_digits as usize, a);
let (ip, fp) = t.split_once('.').expect("frac_digits > 0 forces a point");
let all = format!("{}{}", ip, fp);
let trimmed = all.trim_start_matches('0');
if !trimmed.is_empty() {
let lead = all.chars().count() - trimmed.chars().count();
digits = trimmed.to_string();
decpt = ip.chars().count() as i32 - lead as i32;
}
}
let n = digits.chars().count() as i32;
if decpt <= -4 || decpt > 16 {
let e = decpt - 1;
let mut out = String::from(sign);
let mut it = digits.chars();
out.push(it.next().expect("a finite double has at least one digit"));
if n > 1 {
out.push('.');
out.push_str(it.as_str());
}
out.push('e');
out.push(if e < 0 { '-' } else { '+' });
out.push_str(&format!("{:02}", (e as i64).abs()));
out
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), digits)
} else if decpt >= n {
format!("{}{}{}.0", sign, digits, "0".repeat((decpt - n) as usize))
} else {
let k = decpt as usize;
format!(
"{}{}.{}",
sign,
digits.chars().take(k).collect::<String>(),
digits.chars().skip(k).collect::<String>()
)
}
}
fn python_str(v: &FloatValue) -> String {
match v {
FloatValue::Float { value, .. } => python_float_repr(*value),
FloatValue::Decimal { value, .. } => crate::strnum::python_decimal_str(value),
}
}
fn python_int(s: &str) -> Result<BigInt> {
s.parse::<BigInt>().map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
})
}
fn cardinal_from_str(n: &str) -> Result<String> {
let n = n.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = cardinal_from_str(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
if let Some((left, right)) = n.split_once('.') {
let mut ret = int_to_word(&python_int(left)?);
ret.push(' ');
ret.push_str(POINTWORD);
for ch in right.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
ch
))
})?;
ret.push(' ');
ret.push_str(if d == 0 { ZERU } else { ONES[d as usize] });
}
return Ok(ret.trim().to_string());
}
Ok(int_to_word(&python_int(n)?))
}
fn build_currency_forms() -> Vec<(&'static str, CurrencyForms)> {
const SENTIME: [&str; 2] = ["sentime", "sentime"];
vec![
(
"RWF",
CurrencyForms::new(&["amafaranga", "amafaranga"], &SENTIME),
),
("USD", CurrencyForms::new(&["idolari", "idolari"], &SENTIME)),
("EUR", CurrencyForms::new(&["iyero", "iyero"], &SENTIME)),
]
}
fn plural_form(forms: &[String], n: &BigInt) -> Result<String> {
let idx = if n.is_one() { 0 } else { 1 };
forms
.get(idx)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn split_str_parts(value: &BigDecimal) -> (BigInt, BigInt) {
let integer = value.with_scale(0);
let left = integer.as_bigint_and_exponent().0;
let frac = value - &integer;
let right = (frac * BigDecimal::from(100))
.with_scale(0)
.as_bigint_and_exponent()
.0;
(left, right)
}
pub struct LangRw {
exclude_title: Vec<String>,
currency_forms: Vec<(&'static str, CurrencyForms)>,
}
impl Default for LangRw {
fn default() -> Self {
Self::new()
}
}
impl LangRw {
pub fn new() -> Self {
LangRw {
exclude_title: EXCLUDE_TITLE.iter().map(|s| s.to_string()).collect(),
currency_forms: build_currency_forms(),
}
}
}
impl Lang for LangRw {
fn default_currency(&self) -> &str {
"RWF"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"akadomo"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
Ok(cardinal(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value.is_one() {
return Ok("mbere".to_string());
}
Ok(format!("wa {}", cardinal(value)))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("wa {}", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(cardinal(value))
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
cardinal_from_str(&python_str(value))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if value.as_whole_int().is_some_and(|i| i.is_one()) {
return Ok("mbere".to_string());
}
Ok(format!("wa {}", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("wa {}", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.cardinal_float_entry(value, None)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_RW"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms
.iter()
.find(|(c, _)| *c == code)
.map(|(_, f)| f)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
if forms.is_empty() {
return Ok(String::new());
}
if n.is_one() {
Ok(forms[0].clone())
} else {
Ok(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_str_parts(&value.abs()),
};
let forms = self
.currency_forms(currency)
.unwrap_or(&self.currency_forms[0].1);
let mut result = format!(
"{} {}",
int_to_word(&left),
plural_form(&forms.unit, &left)?
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(&plural_form(&forms.subunit, &right)?);
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cur(arg: &str, code: &str) -> String {
let is_int = !arg.contains('.') && !arg.to_lowercase().contains('e');
let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
LangRw::new()
.to_currency(&v, code, true, None, false)
.unwrap()
}
fn cheque(arg: &str, code: &str) -> Result<String> {
LangRw::new().to_cheque(&BigDecimal::from_str(arg).unwrap(), code)
}
use bigdecimal::BigDecimal;
use std::str::FromStr;
#[test]
fn corpus_currency_rows() {
assert_eq!(cur("0", "EUR"), "zeru iyero");
assert_eq!(cur("1", "EUR"), "rimwe iyero");
assert_eq!(cur("2", "EUR"), "kabiri iyero");
assert_eq!(cur("100", "EUR"), "ijana iyero");
assert_eq!(
cur("12.34", "EUR"),
"icumi na kabiri iyero mirongo itatu na kane sentime"
);
assert_eq!(cur("0.01", "EUR"), "zeru iyero rimwe sentime");
assert_eq!(cur("1.0", "EUR"), "rimwe iyero");
assert_eq!(
cur("99.99", "EUR"),
"mirongo cyenda na icyenda iyero mirongo cyenda na icyenda sentime"
);
assert_eq!(
cur("1234.56", "EUR"),
"igihumbi na kabiri ijana na mirongo itatu na kane iyero \
mirongo itanu na gatandatu sentime"
);
assert_eq!(
cur("-12.34", "EUR"),
"munsi ya icumi na kabiri iyero mirongo itatu na kane sentime"
);
assert_eq!(cur("1000000", "EUR"), "miliyoni iyero");
assert_eq!(cur("0.5", "EUR"), "zeru iyero mirongo itanu sentime");
assert_eq!(cur("0", "USD"), "zeru idolari");
assert_eq!(cur("1", "USD"), "rimwe idolari");
assert_eq!(
cur("12.34", "USD"),
"icumi na kabiri idolari mirongo itatu na kane sentime"
);
assert_eq!(cur("0.01", "USD"), "zeru idolari rimwe sentime");
assert_eq!(cur("1.0", "USD"), "rimwe idolari");
assert_eq!(cur("1000000", "USD"), "miliyoni idolari");
}
#[test]
fn corpus_unknown_codes_fall_back_to_rwf() {
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
assert_eq!(cur("0", code), "zeru amafaranga");
assert_eq!(cur("1", code), "rimwe amafaranga");
assert_eq!(cur("2", code), "kabiri amafaranga");
assert_eq!(cur("100", code), "ijana amafaranga");
assert_eq!(
cur("12.34", code),
"icumi na kabiri amafaranga mirongo itatu na kane sentime"
);
assert_eq!(cur("0.01", code), "zeru amafaranga rimwe sentime");
assert_eq!(cur("1.0", code), "rimwe amafaranga");
assert_eq!(
cur("99.99", code),
"mirongo cyenda na icyenda amafaranga mirongo cyenda na icyenda sentime"
);
assert_eq!(
cur("1234.56", code),
"igihumbi na kabiri ijana na mirongo itatu na kane amafaranga \
mirongo itanu na gatandatu sentime"
);
assert_eq!(
cur("-12.34", code),
"munsi ya icumi na kabiri amafaranga mirongo itatu na kane sentime"
);
assert_eq!(cur("1000000", code), "miliyoni amafaranga");
assert_eq!(cur("0.5", code), "zeru amafaranga mirongo itanu sentime");
}
}
#[test]
fn corpus_cheque_rows() {
assert_eq!(
cheque("1234.56", "EUR").unwrap(),
"IGIHUMBI NA KABIRI IJANA NA MIRONGO ITATU NA KANE AND 56/100 IYERO"
);
assert_eq!(
cheque("1234.56", "USD").unwrap(),
"IGIHUMBI NA KABIRI IJANA NA MIRONGO ITATU NA KANE AND 56/100 IDOLARI"
);
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_RW\"",
code
)
),
other => panic!("{code}: expected NotImplemented, got {other:?}"),
}
}
}
#[test]
fn python_quirks() {
assert_eq!(
cur("12.345", "USD"),
"icumi na kabiri idolari mirongo itatu na kane sentime"
);
assert_eq!(cur("1.005", "USD"), "rimwe idolari");
assert_eq!(cur("12.3", "USD"), "icumi na kabiri idolari mirongo itatu sentime");
assert_eq!(
cur("2.999", "USD"),
"kabiri idolari mirongo cyenda na icyenda sentime"
);
assert_eq!(cur("-0.0", "USD"), "zeru idolari");
assert_eq!(cur("-1", "RWF"), "munsi ya rimwe amafaranga");
assert_eq!(cur("-1.0", "RWF"), "munsi ya rimwe amafaranga");
let v = CurrencyValue::parse("10000000000000000", false, false, false).unwrap();
assert_eq!(
LangRw::new()
.to_currency(&v, "USD", true, None, false)
.unwrap(),
"10000000000000000 idolari"
);
}
#[test]
fn untested_knobs_match_python() {
let l = LangRw::new();
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
assert_eq!(
l.to_currency(&v, "USD", false, None, false).unwrap(),
"icumi na kabiri idolari"
);
assert_eq!(
l.to_currency(&v, "USD", true, Some(" na"), false).unwrap(),
"icumi na kabiri idolari namirongo itatu na kane sentime"
);
assert_eq!(
l.to_currency(&v, "USD", true, None, true).unwrap(),
"icumi na kabiri idolari mirongo itatu na kane sentime"
);
}
#[test]
fn pluralize_is_last_not_second() {
let l = LangRw::new();
let forms: Vec<String> = ["a", "b", "c"].iter().map(|s| s.to_string()).collect();
assert_eq!(l.pluralize(&BigInt::from(1), &forms).unwrap(), "a");
assert_eq!(l.pluralize(&BigInt::from(2), &forms).unwrap(), "c");
assert_eq!(l.pluralize(&BigInt::from(2), &[]).unwrap(), "");
}
fn cf(value: f64, precision: u32) -> String {
LangRw::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn cd(decimal_str: &str, precision: u32) -> String {
LangRw::new()
.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(decimal_str).unwrap(),
precision,
},
None,
)
.unwrap()
}
#[test]
fn corpus_cardinal_float_rows() {
assert_eq!(cf(0.0, 1), "zeru akadomo zeru");
assert_eq!(cf(0.5, 1), "zeru akadomo gatanu");
assert_eq!(cf(1.0, 1), "rimwe akadomo zeru");
assert_eq!(cf(1.5, 1), "rimwe akadomo gatanu");
assert_eq!(cf(2.25, 2), "kabiri akadomo kabiri gatanu");
assert_eq!(cf(3.14, 2), "gatatu akadomo rimwe kane");
assert_eq!(cf(0.01, 2), "zeru akadomo zeru rimwe");
assert_eq!(cf(0.1, 1), "zeru akadomo rimwe");
assert_eq!(cf(0.99, 2), "zeru akadomo icyenda icyenda");
assert_eq!(cf(1.01, 2), "rimwe akadomo zeru rimwe");
assert_eq!(cf(12.34, 2), "icumi na kabiri akadomo gatatu kane");
assert_eq!(
cf(99.99, 2),
"mirongo cyenda na icyenda akadomo icyenda icyenda"
);
assert_eq!(cf(100.5, 1), "ijana akadomo gatanu");
assert_eq!(
cf(1234.56, 2),
"igihumbi na kabiri ijana na mirongo itatu na kane akadomo gatanu gatandatu"
);
assert_eq!(cf(-0.5, 1), "munsi ya zeru akadomo gatanu");
assert_eq!(cf(-1.5, 1), "munsi ya rimwe akadomo gatanu");
assert_eq!(
cf(-12.34, 2),
"munsi ya icumi na kabiri akadomo gatatu kane"
);
assert_eq!(cf(1.005, 3), "rimwe akadomo zeru zeru gatanu");
assert_eq!(cf(2.675, 3), "kabiri akadomo gatandatu karindwi gatanu");
}
#[test]
fn corpus_cardinal_dec_rows() {
assert_eq!(cd("0.01", 2), "zeru akadomo zeru rimwe");
assert_eq!(cd("1.10", 2), "rimwe akadomo rimwe zeru");
assert_eq!(
cd("12.345", 3),
"icumi na kabiri akadomo gatatu kane gatanu"
);
assert_eq!(
cd("98746251323029.99", 2),
"98746251323029 akadomo icyenda icyenda"
);
assert_eq!(cd("0.001", 3), "zeru akadomo zeru zeru rimwe");
}
#[test]
fn cardinal_float_quirks() {
assert_eq!(cf(-0.0, 1), "munsi ya zeru akadomo zeru");
assert_eq!(cf(5.0, 1), "gatanu akadomo zeru");
assert_eq!(cd("12.340", 3), "icumi na kabiri akadomo gatatu kane zeru");
assert_eq!(
cf(123456789.5, 1),
"ijana na makumyabiri na gatatu miliyoni na kane ijana na mirongo \
itanu na gatandatu igihumbi na karindwi ijana na mirongo inani na \
icyenda akadomo gatanu"
);
assert_eq!(
LangRw::new()
.to_cardinal_float(
&FloatValue::Float { value: 2.675, precision: 3 },
Some(1),
)
.unwrap(),
"kabiri akadomo gatandatu karindwi gatanu"
);
assert_eq!(cd("5", 0), "gatanu");
}
fn fv_f(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn fv_d(s: &str, precision: u32) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
}
}
#[test]
fn corpus_float_entry_rows() {
let l = LangRw::new();
assert_eq!(
l.cardinal_float_entry(&fv_f(5.0, 1), None).unwrap(),
"gatanu akadomo zeru"
);
assert_eq!(
l.cardinal_float_entry(&fv_f(-0.0, 1), None).unwrap(),
"munsi ya zeru akadomo zeru"
);
assert_eq!(
l.cardinal_float_entry(&fv_f(1234.0, 1), None).unwrap(),
"igihumbi na kabiri ijana na mirongo itatu na kane akadomo zeru"
);
assert_eq!(
l.cardinal_float_entry(&fv_f(1e9, 1), None).unwrap(),
"1000000000 akadomo zeru"
);
assert_eq!(l.cardinal_float_entry(&fv_d("5", 0), None).unwrap(), "gatanu");
assert_eq!(
l.cardinal_float_entry(&fv_d("5.00", 2), None).unwrap(),
"gatanu akadomo zeru zeru"
);
assert_eq!(
l.cardinal_float_entry(&fv_d("12345.000", 3), None).unwrap(),
"icumi na kabiri igihumbi na gatatu ijana na mirongo ine na gatanu \
akadomo zeru zeru zeru"
);
for v in [
l.cardinal_float_entry(&fv_f(1e16, 16), None),
l.cardinal_float_entry(&fv_f(1e20, 20), None),
l.cardinal_float_entry(&fv_d("1E+2", 0), None),
l.cardinal_float_entry(&fv_d("1E+20", 0), None),
] {
assert!(matches!(v, Err(N2WError::Value(_))), "{v:?}");
}
}
#[test]
fn corpus_ordinal_entry_rows() {
let l = LangRw::new();
assert_eq!(l.ordinal_float_entry(&fv_f(1.0, 1)).unwrap(), "mbere");
assert_eq!(l.ordinal_float_entry(&fv_d("1.00", 2)).unwrap(), "mbere");
assert_eq!(
l.ordinal_float_entry(&fv_f(2.0, 1)).unwrap(),
"wa kabiri akadomo zeru"
);
assert_eq!(
l.ordinal_float_entry(&fv_f(-0.0, 1)).unwrap(),
"wa munsi ya zeru akadomo zeru"
);
assert_eq!(l.ordinal_float_entry(&fv_d("0", 0)).unwrap(), "wa zeru");
assert_eq!(l.ordinal_float_entry(&fv_d("5", 0)).unwrap(), "wa gatanu");
assert_eq!(
l.ordinal_float_entry(&fv_f(0.5, 1)).unwrap(),
"wa zeru akadomo gatanu"
);
assert!(matches!(
l.ordinal_float_entry(&fv_f(1e16, 16)),
Err(N2WError::Value(_))
));
assert_eq!(
l.ordinal_num_float_entry(&fv_f(1e16, 16), "1e+16").unwrap(),
"wa 1e+16"
);
assert_eq!(
l.ordinal_num_float_entry(&fv_d("5.00", 2), "5.00").unwrap(),
"wa 5.00"
);
assert_eq!(
l.year_float_entry(&fv_f(5.0, 1)).unwrap(),
"gatanu akadomo zeru"
);
assert!(matches!(
l.year_float_entry(&fv_d("1E+2", 0)),
Err(N2WError::Value(_))
));
}
#[test]
fn corpus_string_rows() {
let l = LangRw::new();
let parsed = l.str_to_number("1e3").unwrap();
match parsed {
ParsedNumber::Dec(d) => {
let fv = FloatValue::Decimal { value: d, precision: 0 };
assert!(matches!(
l.cardinal_float_entry(&fv, None),
Err(N2WError::Value(_))
));
}
other => panic!("expected Dec, got {other:?}"),
}
assert!(matches!(
l.str_to_number("Infinity"),
Err(N2WError::Value(_))
));
assert!(matches!(
l.str_to_number("-Infinity"),
Err(N2WError::Value(_))
));
assert!(matches!(l.str_to_number("NaN"), Ok(ParsedNumber::NaN)));
}
#[test]
fn float_repr_matches_cpython() {
for (v, want) in [
(0.0, "0.0"),
(-0.0, "-0.0"),
(1.0, "1.0"),
(0.5, "0.5"),
(1234.0, "1234.0"),
(1000000000000000.0, "1000000000000000.0"),
(1e16, "1e+16"),
(1e20, "1e+20"),
(2.675, "2.675"),
(1.005, "1.005"),
(123456789.5, "123456789.5"),
] {
assert_eq!(python_float_repr(v), want);
}
}
#[test]
fn with_scale_zero_truncates() {
for (s, want) in [("0.9", 0), ("1.5", 1), ("2.5", 2), ("34.99", 34)] {
assert_eq!(
BigDecimal::from_str(s)
.unwrap()
.with_scale(0)
.as_bigint_and_exponent()
.0,
BigInt::from(want),
"with_scale(0) on {s}"
);
}
}
}