use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const ONES: [&str; 10] = [
"", "siki", "dua", "telu", "papat", "lima", "nem", "pitu", "kutus", "sia",
];
const TENS: [&str; 10] = [
"",
"dasa",
"kalih dasa",
"tigang dasa",
"petang dasa",
"seket",
"nem dasa",
"pitung dasa",
"kutus dasa",
"siangang dasa",
];
const HUNDRED: &str = "satus";
const THOUSAND: &str = "siu";
const MILLION: &str = "yuta";
const NEGWORD: &str = "minus ";
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return "nol".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[number.to_usize().expect("0..=9 fits usize")].to_string();
}
if number < &hundred {
let (t, o) = number.div_mod_floor(&ten);
let mut s = TENS[t.to_usize().expect("1..=9 fits usize")].to_string();
if !o.is_zero() {
s.push(' ');
s.push_str(ONES[o.to_usize().expect("1..=9 fits usize")]);
}
return s;
}
if number < &thousand {
let (h, r) = number.div_mod_floor(&hundred);
let mut s = String::new();
if h > BigInt::one() {
s.push_str(ONES[h.to_usize().expect("2..=9 fits usize")]);
s.push(' ');
}
s.push_str(HUNDRED);
if !r.is_zero() {
s.push(' ');
s.push_str(&int_to_word(&r));
}
return s;
}
if number < &million {
let (t, r) = number.div_mod_floor(&thousand);
let mut s = int_to_word(&t);
s.push(' ');
s.push_str(THOUSAND);
if !r.is_zero() {
s.push(' ');
s.push_str(&int_to_word(&r));
}
return s;
}
if number < &billion {
let (m, r) = number.div_mod_floor(&million);
let mut s = int_to_word(&m);
s.push(' ');
s.push_str(MILLION);
if !r.is_zero() {
s.push(' ');
s.push_str(&int_to_word(&r));
}
return s;
}
number.to_string()
}
const FALLBACK_CURRENCY: &str = "IDR";
pub struct LangBan {
forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangBan {
fn default() -> Self {
Self::new()
}
}
impl LangBan {
pub fn new() -> Self {
let mut forms = HashMap::new();
forms.insert(
"IDR",
CurrencyForms::new(&["rupiah", "rupiah"], &["sen", "sen"]),
);
forms.insert(
"USD",
CurrencyForms::new(&["dolar", "dolar"], &["sen", "sen"]),
);
forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euro"], &["sen", "sen"]),
);
LangBan { forms }
}
}
fn split_currency_parts(value: &bigdecimal::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::from_str(&padded[..split]).expect("digits only");
let mut two: String = padded[split..].chars().take(2).collect();
while two.len() < 2 {
two.push('0');
}
let right = BigInt::from_str(&two).expect("digits only");
Ok((left, right))
}
fn parse_int(token: &str) -> Result<BigInt> {
let stripped = token.trim();
let (negative, digits) = match stripped.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, stripped.strip_prefix('+').unwrap_or(stripped)),
};
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
token
)));
}
let value = BigInt::from_str(digits).expect("ascii digits only");
Ok(if negative { -value } else { value })
}
fn python_float_repr(value: f64) -> String {
if value.is_nan() {
return "nan".to_string();
}
if value.is_infinite() {
return if value.is_sign_negative() {
"-inf"
} else {
"inf"
}
.to_string();
}
let sign = if value.is_sign_negative() { "-" } else { "" };
let formatted = format!("{:e}", value.abs());
let (mantissa, exponent) = formatted
.split_once('e')
.expect("f64 LowerExp always emits an 'e'");
let exponent: i32 = exponent.parse().expect("LowerExp emits a decimal exponent");
let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
let decpt = exponent + 1;
if decpt <= -4 || decpt > 16 {
let mut out = String::from(sign);
out.push_str(&digits[..1]);
if digits.len() > 1 {
out.push('.');
out.push_str(&digits[1..]);
}
out.push('e');
let exp10 = decpt - 1;
out.push(if exp10 < 0 { '-' } else { '+' });
out.push_str(&format!("{:02}", (exp10 as i64).abs()));
out
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), digits)
} else if decpt as usize >= digits.len() {
format!(
"{}{}{}.0",
sign,
digits,
"0".repeat(decpt as usize - digits.len())
)
} else {
format!(
"{}{}.{}",
sign,
&digits[..decpt as usize],
&digits[decpt as usize..]
)
}
}
fn python_decimal_str(value: &BigDecimal) -> String {
let (coefficient, scale) = value.as_bigint_and_exponent();
let sign = if coefficient.is_negative() { "-" } else { "" };
let int_digits = coefficient.abs().to_string();
let exp = -scale;
let leftdigits = exp + int_digits.len() as i64;
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), int_digits),
)
} else if dotplace >= int_digits.len() as i64 {
(
format!(
"{}{}",
int_digits,
"0".repeat((dotplace - int_digits.len() as i64) as usize)
),
String::new(),
)
} else {
(
int_digits[..dotplace as usize].to_string(),
format!(".{}", &int_digits[dotplace as usize..]),
)
};
let exponent = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
format!("{}{}{}{}", sign, intpart, fracpart, exponent)
}
impl LangBan {
fn cardinal_from_str(&self, number: &str) -> Result<String> {
let n = number.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = self.cardinal_from_str(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
if let Some((left, right)) = n.split_once('.') {
let mut ret = format!("{} {}", int_to_word(&parse_int(left)?), self.pointword());
for digit in right.chars() {
let index = parse_int(&digit.to_string())?
.to_usize()
.expect("a single ASCII digit is 0..=9");
ret.push(' ');
let word = ONES[index];
ret.push_str(if word.is_empty() { "nol" } else { word });
}
return Ok(ret.trim().to_string());
}
Ok(int_to_word(&parse_int(n)?))
}
}
impl Lang for LangBan {
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> {
Ok(format!("ka-{}", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("ka-{}", 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(),
)),
other => Ok(other),
}
}
fn negword(&self) -> &str {
"minus "
}
fn pointword(&self) -> &str {
"koma"
}
fn default_currency(&self) -> &str {
"IDR"
}
fn default_separator(&self) -> &str {
" "
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = int_to_word(&value.abs());
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("ka-{}", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("ka-{}", 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 n = match value {
FloatValue::Float { value, .. } => python_float_repr(*value),
FloatValue::Decimal { value, .. } => python_decimal_str(value),
};
self.cardinal_from_str(&n)
}
fn lang_name(&self) -> &str {
"Num2Word_BAN"
}
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 separator = if separator == "," { " " } else { separator };
let is_negative = val.is_negative();
let (left, right) = match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => split_currency_parts(d)?,
};
let forms = self
.forms
.get(currency)
.or_else(|| self.forms.get(FALLBACK_CURRENCY))
.expect("IDR is always present");
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let unit = if left.is_one() { &cr1[0] } else { &cr1[1] };
let mut result = format!("{} {}", int_to_word(&left), unit);
if cents && !right.is_zero() {
let sub = if right.is_one() { &cr2[0] } else { &cr2[1] };
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(sub);
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn card(n: i64) -> String {
LangBan::new().to_cardinal(&BigInt::from(n)).unwrap()
}
fn ord_(n: i64) -> String {
LangBan::new().to_ordinal(&BigInt::from(n)).unwrap()
}
fn ord_num(n: i64) -> String {
LangBan::new().to_ordinal_num(&BigInt::from(n)).unwrap()
}
fn year(n: i64) -> String {
LangBan::new().to_year(&BigInt::from(n)).unwrap()
}
#[test]
fn corpus_cardinal() {
assert_eq!(card(0), "nol");
assert_eq!(card(1), "siki");
assert_eq!(card(9), "sia");
assert_eq!(card(10), "dasa");
assert_eq!(card(11), "dasa siki");
assert_eq!(card(20), "kalih dasa");
assert_eq!(card(50), "seket");
assert_eq!(card(99), "siangang dasa sia");
assert_eq!(card(100), "satus");
assert_eq!(card(101), "satus siki");
assert_eq!(card(110), "satus dasa");
assert_eq!(card(200), "dua satus");
assert_eq!(card(999), "sia satus siangang dasa sia");
assert_eq!(card(1000), "siki siu");
assert_eq!(card(1010), "siki siu dasa");
assert_eq!(card(12345), "dasa dua siu telu satus petang dasa lima");
assert_eq!(card(100000), "satus siu");
assert_eq!(card(1000000), "siki yuta");
assert_eq!(
card(1234567),
"siki yuta dua satus tigang dasa papat siu lima satus nem dasa pitu"
);
assert_eq!(
card(123456789),
"satus kalih dasa telu yuta papat satus seket nem siu pitu satus kutus dasa sia"
);
}
#[test]
fn corpus_cardinal_above_billion_is_bare_digits() {
assert_eq!(card(1_000_000_000), "1000000000");
assert_eq!(card(1_234_567_890), "1234567890");
let big: BigInt = BigInt::from(10).pow(21u32);
assert_eq!(
LangBan::new().to_cardinal(&big).unwrap(),
"1000000000000000000000"
);
}
#[test]
fn corpus_negative() {
assert_eq!(card(-1), "minus siki");
assert_eq!(card(-21), "minus kalih dasa siki");
assert_eq!(card(-100), "minus satus");
assert_eq!(card(-1000), "minus siki siu");
assert_eq!(card(-1000000), "minus siki yuta");
}
#[test]
fn corpus_ordinal() {
assert_eq!(ord_(0), "ka-nol");
assert_eq!(ord_(1), "ka-siki");
assert_eq!(ord_(11), "ka-dasa siki");
assert_eq!(ord_(1000000), "ka-siki yuta");
assert_eq!(ord_(-1), "ka-minus siki");
assert_eq!(ord_(1_000_000_000), "ka-1000000000");
}
#[test]
fn corpus_ordinal_num() {
assert_eq!(ord_num(0), "ka-0");
assert_eq!(ord_num(9), "ka-9");
assert_eq!(ord_num(1000000), "ka-1000000");
assert_eq!(ord_num(-1), "ka--1");
assert_eq!(ord_num(-1000), "ka--1000");
}
use bigdecimal::BigDecimal;
fn cur(arg: &str, is_int: bool, code: &str) -> String {
let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
LangBan::new()
.to_currency(&v, code, true, None, false)
.unwrap()
}
fn cheque(arg: &str, code: &str) -> Result<String> {
LangBan::new().to_cheque(&BigDecimal::from_str(arg).unwrap(), code)
}
#[test]
fn corpus_currency_known_codes() {
assert_eq!(cur("0", true, "EUR"), "nol euro");
assert_eq!(cur("1", true, "EUR"), "siki euro");
assert_eq!(cur("2", true, "EUR"), "dua euro");
assert_eq!(cur("100", true, "EUR"), "satus euro");
assert_eq!(cur("1000000", true, "EUR"), "siki yuta euro");
assert_eq!(
cur("12.34", false, "EUR"),
"dasa dua euro tigang dasa papat sen"
);
assert_eq!(cur("0.01", false, "EUR"), "nol euro siki sen");
assert_eq!(cur("0.5", false, "EUR"), "nol euro seket sen");
assert_eq!(
cur("99.99", false, "EUR"),
"siangang dasa sia euro siangang dasa sia sen"
);
assert_eq!(
cur("1234.56", false, "EUR"),
"siki siu dua satus tigang dasa papat euro seket nem sen"
);
assert_eq!(
cur("-12.34", false, "EUR"),
"minus dasa dua euro tigang dasa papat sen"
);
assert_eq!(
cur("12.34", false, "USD"),
"dasa dua dolar tigang dasa papat sen"
);
assert_eq!(cur("1", true, "USD"), "siki dolar");
}
#[test]
fn corpus_currency_float_with_zero_cents() {
assert_eq!(cur("1.0", false, "EUR"), "siki euro");
assert_eq!(cur("1.0", false, "USD"), "siki dolar");
assert_eq!(cur("1.0", false, "GBP"), "siki rupiah");
assert_eq!(cur("1", true, "EUR"), "siki euro");
}
#[test]
fn corpus_currency_unknown_code_falls_back_to_idr() {
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
assert_eq!(cur("0", true, code), "nol rupiah");
assert_eq!(cur("1", true, code), "siki rupiah");
assert_eq!(cur("100", true, code), "satus rupiah");
assert_eq!(
cur("12.34", false, code),
"dasa dua rupiah tigang dasa papat sen"
);
assert_eq!(cur("0.01", false, code), "nol rupiah siki sen");
assert_eq!(cur("0.5", false, code), "nol rupiah seket sen");
assert_eq!(cur("1000000", true, code), "siki yuta rupiah");
assert_eq!(
cur("1234.56", false, code),
"siki siu dua satus tigang dasa papat rupiah seket nem sen"
);
assert_eq!(
cur("-12.34", false, code),
"minus dasa dua rupiah tigang dasa papat sen"
);
}
}
#[test]
fn currency_quirks_verified_against_python() {
let one = LangBan::new();
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
assert_eq!(
one.to_currency(&v, "EUR", false, None, false).unwrap(),
"dasa dua euro"
);
assert_eq!(
one.to_currency(&v, "EUR", true, None, true).unwrap(),
"dasa dua euro tigang dasa papat sen"
);
assert_eq!(
one.to_currency(&v, "EUR", true, Some(" ne "), false)
.unwrap(),
"dasa dua euro ne tigang dasa papat sen"
);
let v = CurrencyValue::parse("12.345", false, true, true).unwrap();
assert_eq!(
one.to_currency(&v, "EUR", true, None, false).unwrap(),
"dasa dua euro tigang dasa papat sen"
);
}
#[test]
fn currency_exponential_repr_is_valueerror() {
for arg in ["1e+16", "1E+2"] {
let v = CurrencyValue::parse(arg, false, true, true).unwrap();
match LangBan::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"), "nol euro");
}
#[test]
fn corpus_cheque() {
assert_eq!(
cheque("1234.56", "EUR").unwrap(),
"SIKI SIU DUA SATUS TIGANG DASA PAPAT AND 56/100 EURO"
);
assert_eq!(
cheque("1234.56", "USD").unwrap(),
"SIKI SIU DUA SATUS TIGANG DASA PAPAT AND 56/100 DOLAR"
);
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_BAN\"",
code
)
),
other => panic!("{} expected NotImplementedError, got {:?}", code, other),
}
}
}
fn flt(value: f64) -> Result<String> {
let precision = match format!("{}", value).split_once('.') {
Some((_, frac)) => frac.len() as u32,
None => 0,
};
LangBan::new().to_cardinal_float(&FloatValue::Float { value, precision }, None)
}
fn fltok(value: f64) -> String {
flt(value).unwrap()
}
fn dec(s: &str) -> Result<String> {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.unsigned_abs() as u32;
LangBan::new().to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
}
fn decok(s: &str) -> String {
dec(s).unwrap()
}
#[test]
fn corpus_cardinal_float() {
assert_eq!(fltok(0.0), "nol koma nol");
assert_eq!(fltok(0.5), "nol koma lima");
assert_eq!(fltok(1.0), "siki koma nol");
assert_eq!(fltok(1.5), "siki koma lima");
assert_eq!(fltok(2.25), "dua koma dua lima");
assert_eq!(fltok(3.14), "telu koma siki papat");
assert_eq!(fltok(0.01), "nol koma nol siki");
assert_eq!(fltok(0.1), "nol koma siki");
assert_eq!(fltok(0.99), "nol koma sia sia");
assert_eq!(fltok(1.01), "siki koma nol siki");
assert_eq!(fltok(12.34), "dasa dua koma telu papat");
assert_eq!(fltok(99.99), "siangang dasa sia koma sia sia");
assert_eq!(fltok(100.5), "satus koma lima");
assert_eq!(
fltok(1234.56),
"siki siu dua satus tigang dasa papat koma lima nem"
);
assert_eq!(fltok(-0.5), "minus nol koma lima");
assert_eq!(fltok(-1.5), "minus siki koma lima");
assert_eq!(fltok(-12.34), "minus dasa dua koma telu papat");
assert_eq!(fltok(1.005), "siki koma nol nol lima");
assert_eq!(fltok(2.675), "dua koma nem pitu lima");
}
#[test]
fn corpus_cardinal_decimal() {
assert_eq!(decok("0.01"), "nol koma nol siki");
assert_eq!(decok("1.10"), "siki koma siki nol");
assert_eq!(decok("12.345"), "dasa dua koma telu papat lima");
assert_eq!(decok("98746251323029.99"), "98746251323029 koma sia sia");
assert_eq!(decok("0.001"), "nol koma nol nol siki");
}
#[test]
fn float_artefacts_never_arise() {
assert_eq!(fltok(2.675), "dua koma nem pitu lima");
assert_eq!(fltok(1.005), "siki koma nol nol lima");
assert_eq!(
fltok(0.1 + 0.2),
"nol koma telu nol nol nol nol nol nol nol nol nol nol nol nol nol nol nol papat"
);
}
#[test]
fn precision_override_is_ignored() {
let value = FloatValue::Float {
value: 2.675,
precision: 3,
};
for override_ in [None, Some(1), Some(5)] {
assert_eq!(
LangBan::new().to_cardinal_float(&value, override_).unwrap(),
"dua koma nem pitu lima"
);
}
}
#[test]
fn negative_zero_keeps_its_sign() {
assert_eq!(fltok(-0.0), "minus nol koma nol");
assert_eq!(fltok(0.0), "nol koma nol");
}
#[test]
fn negative_zero_decimal_loses_its_sign() {
assert_eq!(
BigDecimal::from_str("-0.0").unwrap().sign(),
num_bigint::Sign::NoSign
);
assert_eq!(decok("-0.0"), "nol koma nol");
assert_eq!(decok("-0"), "nol");
assert_eq!(decok("-0.5"), "minus nol koma lima");
}
#[test]
fn float_above_billion_keeps_bare_integer_part() {
assert_eq!(
fltok(123456789.5),
"satus kalih dasa telu yuta papat satus seket nem siu pitu satus \
kutus dasa sia koma lima"
);
assert_eq!(fltok(1234567890.5), "1234567890 koma lima");
assert_eq!(fltok(1e15), "1000000000000000 koma nol");
}
#[test]
fn exponential_float_repr_is_valueerror() {
for (value, token) in [
(1e16, "1e+16"),
(1e21, "1e+21"),
(1e100, "1e+100"),
(1e-5, "1e-05"),
(1e-323, "1e-323"),
] {
match flt(value) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", token)
),
other => panic!("{} expected ValueError, got {:?}", value, other),
}
}
for value in [1.5e16, 1.5e-5, 2e16 + 8.0] {
match flt(value) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: 'e'")
}
other => panic!("{} expected ValueError, got {:?}", value, other),
}
}
assert_eq!(fltok(1e-4), "nol koma nol nol nol siki");
}
#[test]
fn non_finite_floats_are_valueerror() {
for (value, token) in [
(f64::INFINITY, "inf"),
(f64::NEG_INFINITY, "inf"),
(f64::NAN, "nan"),
] {
match flt(value) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", token)
),
other => panic!("{} expected ValueError, got {:?}", value, other),
}
}
}
#[test]
fn exponential_decimal_str_is_valueerror() {
for (arg, token) in [
("1E+2", "1E+2"),
("1E-7", "1E-7"),
("0.0000001", "1E-7"),
] {
match dec(arg) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", token)
),
other => panic!("{} expected ValueError, got {:?}", arg, other),
}
}
match dec("1.10E+3") {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: 'E'")
}
other => panic!("1.10E+3 expected ValueError, got {:?}", other),
}
assert_eq!(decok("100"), "satus");
assert_eq!(decok("0.000001"), "nol koma nol nol nol nol nol siki");
}
#[test]
fn decimal_scale_is_observable() {
assert_eq!(decok("5"), "lima");
assert_eq!(decok("5.00"), "lima koma nol nol");
assert_eq!(decok("0.000"), "nol koma nol nol nol");
assert_eq!(decok("-0.5"), "minus nol koma lima");
}
#[test]
fn str_reimplementations_match_python() {
for (value, expected) in [
(0.0, "0.0"),
(-0.0, "-0.0"),
(0.5, "0.5"),
(1.0, "1.0"),
(2.675, "2.675"),
(1.005, "1.005"),
(0.1, "0.1"),
(1e-4, "0.0001"),
(1e-5, "1e-05"),
(1.5e-5, "1.5e-05"),
(1e15, "1000000000000000.0"),
(1e16, "1e+16"),
(1.5e16, "1.5e+16"),
(2e16 + 8.0, "2.000000000000001e+16"),
(1e21, "1e+21"),
(1e100, "1e+100"),
(1e-323, "1e-323"),
(98746251323029.99, "98746251323029.98"),
(123456789.5, "123456789.5"),
(100.0, "100.0"),
(-12.34, "-12.34"),
] {
assert_eq!(python_float_repr(value), expected, "repr({})", value);
}
for (arg, expected) in [
("0.01", "0.01"),
("1.10", "1.10"),
("5", "5"),
("5.00", "5.00"),
("0.000", "0.000"),
("98746251323029.99", "98746251323029.99"),
("1E+2", "1E+2"),
("1E-7", "1E-7"),
("1.10E+3", "1.10E+3"),
("0.0000001", "1E-7"),
("-0.5", "-0.5"),
] {
assert_eq!(
python_decimal_str(&BigDecimal::from_str(arg).unwrap()),
expected,
"str(Decimal({:?}))",
arg
);
}
}
#[test]
fn corpus_year() {
assert_eq!(year(1), "siki");
assert_eq!(year(100), "satus");
assert_eq!(year(1492), "siki siu papat satus siangang dasa dua");
assert_eq!(year(1776), "siki siu pitu satus pitung dasa nem");
assert_eq!(year(2024), "dua siu kalih dasa papat");
assert_eq!(year(9999), "sia siu sia satus siangang dasa sia");
assert_eq!(year(-500), "minus lima satus");
assert_eq!(year(-44), "minus petang dasa papat");
}
}