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 ONES: [&str; 10] = [
"", "satu", "dua", "tiga", "empat", "lima", "enam", "tujuh", "lapan", "sembilan",
];
const TENS: [&str; 10] = [
"",
"sepuluh",
"dua puluh",
"tiga puluh",
"empat puluh",
"lima puluh",
"enam puluh",
"tujuh puluh",
"lapan puluh",
"sembilan puluh",
];
const TEENS: [&str; 9] = [
"sebelas",
"dua belas",
"tiga belas",
"empat belas",
"lima belas",
"enam belas",
"tujuh belas",
"lapan belas",
"sembilan belas",
];
const ORDINALS: [&str; 11] = [
"",
"pertama",
"kedua",
"ketiga",
"keempat",
"kelima",
"keenam",
"ketujuh",
"kelapan",
"kesembilan",
"kesepuluh",
];
const ZERO_WORD: &str = "kosong";
const FRACTIONAL_CENTS_WORD: &str = ZERO_WORD;
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const DOLAR: [&str; 2] = ["dolar", "dolar"];
const SEN: [&str; 2] = ["sen", "sen"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("MYR", CurrencyForms::new(&["ringgit", "ringgit"], &SEN));
m.insert("SGD", CurrencyForms::new(&DOLAR, &SEN));
m.insert("USD", CurrencyForms::new(&DOLAR, &SEN));
m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &SEN));
m.insert(
"GBP",
CurrencyForms::new(&["paun", "paun"], &["peni", "peni"]),
);
m.insert("IDR", CurrencyForms::new(&["rupiah", "rupiah"], &SEN));
m.insert("BND", CurrencyForms::new(&DOLAR, &SEN));
m
}
fn index_error(i: &BigInt) -> N2WError {
N2WError::Index(format!("list index out of range (ordinals[{}])", i))
}
fn ordinal_at(n: &BigInt) -> Result<&'static str> {
let len = BigInt::from(ORDINALS.len());
let resolved = if n.is_negative() { &len + n } else { n.clone() };
if resolved.is_negative() || resolved >= len {
return Err(index_error(n));
}
let i = resolved.to_usize().ok_or_else(|| index_error(n))?;
ORDINALS.get(i).copied().ok_or_else(|| index_error(n))
}
fn int_to_word(n: &BigInt) -> String {
if n.is_zero() {
return ZERO_WORD.to_string();
}
let trillion = BigInt::from(1_000_000_000_000u64);
let billion = BigInt::from(1_000_000_000u64);
let million = BigInt::from(1_000_000u64);
let thousand = BigInt::from(1_000u64);
let hundred = BigInt::from(100u64);
let mut n = n.clone();
let mut parts: Vec<String> = Vec::new();
for (scale, one_form, plural_suffix) in [
(&trillion, "satu trilion", " trilion"),
(&billion, "satu bilion", " bilion"),
(&million, "satu juta", " juta"),
(&thousand, "seribu", " ribu"),
] {
if n >= *scale {
let (div, rem) = n.div_rem(scale);
if div.is_one() {
parts.push(one_form.to_string());
} else {
parts.push(format!("{}{}", int_to_word(&div), plural_suffix));
}
n = rem;
}
}
if n >= hundred {
let (div, rem) = n.div_rem(&hundred);
if div.is_one() {
parts.push("seratus".to_string());
} else {
parts.push(format!("{} ratus", ONES[div.to_usize().unwrap_or(0)]));
}
n = rem;
}
let small = n.to_u32().unwrap_or(0);
if small > 10 && small < 20 {
parts.push(TEENS[(small - 11) as usize].to_string());
} else {
let mut small = small;
if small >= 10 {
if small == 10 {
parts.push("sepuluh".to_string());
} else {
parts.push(TENS[(small / 10) as usize].to_string());
}
small %= 10;
}
if small > 0 {
parts.push(ONES[small as usize].to_string());
}
}
parts.join(" ")
}
fn int_to_cardinal(n: &BigInt) -> String {
if n.is_zero() {
return ZERO_WORD.to_string();
}
if n.is_negative() {
return format!("{}{}", "negatif ", int_to_word(&(-n)));
}
int_to_word(n)
}
fn trunc_toward_zero(value: &FloatValue) -> Result<BigInt> {
match value {
FloatValue::Float { value, .. } => BigInt::from_f64(value.trunc()).ok_or_else(|| {
N2WError::Value(format!("cannot convert non-finite float {} to int", value))
}),
FloatValue::Decimal { value, .. } => Ok(value.with_scale(0).as_bigint_and_exponent().0),
}
}
fn list_index_type_error() -> N2WError {
N2WError::Type("list indices must be integers or slices, not float".to_string())
}
fn ms_word_numeric(whole: &BigInt, frac: bool) -> Result<String> {
let trillion = BigInt::from(1_000_000_000_000u64);
let billion = BigInt::from(1_000_000_000u64);
let million = BigInt::from(1_000_000u64);
let thousand = BigInt::from(1_000u64);
let hundred = BigInt::from(100u64);
let mut w = whole.clone();
let mut parts: Vec<String> = Vec::new();
for (scale, one_form, plural_suffix) in [
(&trillion, "satu trilion", " trilion"),
(&billion, "satu bilion", " bilion"),
(&million, "satu juta", " juta"),
(&thousand, "seribu", " ribu"),
] {
if &w >= scale {
let (div, rem) = w.div_rem(scale);
if div.is_one() {
parts.push(one_form.to_string());
} else {
parts.push(format!(
"{}{}",
ms_word_numeric(&div, false)?,
plural_suffix
));
}
w = rem;
}
}
if w >= hundred {
let (div, rem) = w.div_rem(&hundred);
if div.is_one() {
parts.push("seratus".to_string());
} else {
return Err(list_index_type_error());
}
w = rem;
}
let small = w.to_u32().unwrap_or(0);
if (11..=19).contains(&small) || (small == 10 && frac) {
if frac {
return Err(N2WError::Key(format!("{}.…", small)));
}
parts.push(TEENS[(small - 11) as usize].to_string());
} else {
let mut rest = small;
if small >= 10 {
if small == 10 && !frac {
parts.push("sepuluh".to_string());
} else {
return Err(list_index_type_error());
}
rest %= 10;
}
if rest > 0 || frac {
return Err(list_index_type_error());
}
}
Ok(parts.join(" "))
}
fn ms_cardinal_numeric(value: &FloatValue) -> Result<String> {
let (neg, frac) = match value {
FloatValue::Float { value, .. } => (*value < 0.0, value.fract() != 0.0),
FloatValue::Decimal { value, .. } => (value.is_negative(), !value.is_integer()),
};
let whole = trunc_toward_zero(value)?.abs();
if whole.is_zero() && !frac {
return Ok(ZERO_WORD.to_string());
}
if neg {
return Ok(format!("{}{}", "negatif ", ms_word_numeric(&whole, frac)?));
}
ms_word_numeric(&whole, frac)
}
fn int_to_ordinal(n: &BigInt) -> Result<String> {
if n.is_zero() {
return Ok(ZERO_WORD.to_string());
}
if *n <= BigInt::from(10) {
return ordinal_at(n).map(|s| s.to_string());
}
Ok(format!("ke-{}", int_to_cardinal(n)))
}
pub struct LangMs {
currency_forms: HashMap<&'static str, CurrencyForms>,
decimal_ctx_limit: BigInt,
decimal_ctx_scaled_limit: BigDecimal,
}
impl LangMs {
pub fn new() -> Self {
let limit = BigInt::from(10u8).pow(26);
LangMs {
currency_forms: build_currency_forms(),
decimal_ctx_scaled_limit: BigDecimal::from(&limit * 100),
decimal_ctx_limit: limit,
}
}
}
impl Default for LangMs {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangMs {
fn default_currency(&self) -> &str {
"MYR"
}
fn negword(&self) -> &str {
"negatif "
}
fn pointword(&self) -> &str {
"titik"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
Ok(int_to_cardinal(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
int_to_ordinal(value)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("ke-{}", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(int_to_cardinal(value))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
int_to_ordinal(&trunc_toward_zero(value)?)
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("ke-{}", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
ms_cardinal_numeric(value)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let int_part = match value {
FloatValue::Float { value, .. } => {
BigInt::from_f64(value.trunc()).ok_or_else(|| {
N2WError::Value(format!("cannot convert non-finite float {} to int", value))
})?
}
FloatValue::Decimal { value, .. } => value.with_scale(0).as_bigint_and_exponent().0,
};
Ok(int_to_cardinal(&int_part))
}
fn lang_name(&self) -> &str {
"Num2Word_MS"
}
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> {
if let CurrencyValue::Int(v) = val {
if v.abs() >= self.decimal_ctx_limit {
return Ok(format!("{} {}", v, currency));
}
}
let has_fractional_cents = match val {
CurrencyValue::Int(_) => false,
CurrencyValue::Decimal { value, .. } => {
let scaled = value * BigDecimal::from(100);
if scaled.abs() >= self.decimal_ctx_scaled_limit {
return Err(N2WError::NotImplemented(format!(
"Num2Word_MS.to_currency falls back to str(value) for \
|value| >= 10**26, which this core cannot reconstruct \
(currency {:?})",
currency
)));
}
&scaled - scaled.with_scale(0) != BigDecimal::zero()
}
};
let (left, right, is_negative) = parse_currency_parts(val, true, false, 100);
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let mut result: Vec<String> = Vec::new();
if is_negative {
result.push(self.negword().trim().to_string());
}
result.push(int_to_cardinal(&left));
result.push(forms.unit[0].clone());
let right = right.as_bigint_and_exponent().0;
if right.is_positive() {
result.push(if has_fractional_cents {
FRACTIONAL_CENTS_WORD.to_string()
} else {
int_to_cardinal(&right)
});
result.push(forms.subunit[0].clone());
}
Ok(result.join(" "))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn n(s: &str) -> BigInt {
s.parse().unwrap()
}
fn cur(arg: &str, currency: &str) -> Result<String> {
let is_int = !arg.contains('.') && !arg.to_lowercase().contains('e');
let val = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
LangMs::new().to_currency(&val, currency, true, None, false)
}
fn cheque(arg: &str, currency: &str) -> Result<String> {
LangMs::new().to_cheque(&BigDecimal::from_str(arg).unwrap(), currency)
}
fn card(s: &str) -> String {
LangMs::new().to_cardinal(&n(s)).unwrap()
}
fn ord(s: &str) -> Result<String> {
LangMs::new().to_ordinal(&n(s))
}
fn year(s: &str) -> String {
LangMs::new().to_year(&n(s)).unwrap()
}
fn ord_num(s: &str) -> String {
LangMs::new().to_ordinal_num(&n(s)).unwrap()
}
#[test]
fn cardinals_from_corpus() {
assert_eq!(card("0"), "kosong");
assert_eq!(card("1"), "satu");
assert_eq!(card("9"), "sembilan");
assert_eq!(card("10"), "sepuluh");
assert_eq!(card("11"), "sebelas");
assert_eq!(card("12"), "dua belas");
assert_eq!(card("19"), "sembilan belas");
assert_eq!(card("20"), "dua puluh");
assert_eq!(card("21"), "dua puluh satu");
assert_eq!(card("80"), "lapan puluh");
assert_eq!(card("99"), "sembilan puluh sembilan");
assert_eq!(card("100"), "seratus");
assert_eq!(card("101"), "seratus satu");
assert_eq!(card("111"), "seratus sebelas");
assert_eq!(card("200"), "dua ratus");
assert_eq!(card("999"), "sembilan ratus sembilan puluh sembilan");
assert_eq!(card("1000"), "seribu");
assert_eq!(card("1234"), "seribu dua ratus tiga puluh empat");
assert_eq!(card("2000"), "dua ribu");
assert_eq!(card("10000"), "sepuluh ribu");
assert_eq!(card("12345"), "dua belas ribu tiga ratus empat puluh lima");
assert_eq!(card("100000"), "seratus ribu");
assert_eq!(
card("123456"),
"seratus dua puluh tiga ribu empat ratus lima puluh enam"
);
assert_eq!(card("1000000"), "satu juta");
assert_eq!(card("1000001"), "satu juta satu");
assert_eq!(
card("1234567"),
"satu juta dua ratus tiga puluh empat ribu lima ratus enam puluh tujuh"
);
assert_eq!(card("1000000000"), "satu bilion");
assert_eq!(
card("123456789"),
"seratus dua puluh tiga juta empat ratus lima puluh enam ribu tujuh ratus lapan puluh sembilan"
);
assert_eq!(card("1000000000000"), "satu trilion");
}
#[test]
fn unbounded_trillions_stacking() {
assert_eq!(card("1000000000000000"), "seribu trilion");
assert_eq!(card("1000000000000000000"), "satu juta trilion");
assert_eq!(card("1000000000000000000000"), "satu bilion trilion");
}
#[test]
fn negative_cardinals() {
assert_eq!(card("-1"), "negatif satu");
assert_eq!(card("-7"), "negatif tujuh");
assert_eq!(card("-21"), "negatif dua puluh satu");
assert_eq!(card("-100"), "negatif seratus");
assert_eq!(card("-1000"), "negatif seribu");
assert_eq!(card("-1000000"), "negatif satu juta");
}
#[test]
fn ordinals_from_corpus() {
assert_eq!(ord("0").unwrap(), "kosong");
assert_eq!(ord("1").unwrap(), "pertama");
assert_eq!(ord("10").unwrap(), "kesepuluh");
assert_eq!(ord("11").unwrap(), "ke-sebelas");
assert_eq!(ord("21").unwrap(), "ke-dua puluh satu");
assert_eq!(ord("100").unwrap(), "ke-seratus");
assert_eq!(ord("1000000").unwrap(), "ke-satu juta");
}
#[test]
fn negative_ordinals_wrap() {
assert_eq!(ord("-1").unwrap(), "kesepuluh");
assert_eq!(ord("-7").unwrap(), "keempat");
assert_eq!(ord("-11").unwrap(), "");
}
#[test]
fn negative_ordinals_index_error() {
for v in ["-12", "-21", "-42", "-100", "-999", "-1000", "-1000000"] {
assert!(matches!(ord(v), Err(N2WError::Index(_))), "{v}");
}
}
#[test]
fn ordinal_num_is_raw_concat() {
assert_eq!(ord_num("0"), "ke-0");
assert_eq!(ord_num("1"), "ke-1");
assert_eq!(ord_num("1234567890"), "ke-1234567890");
assert_eq!(ord_num("-1"), "ke--1");
assert_eq!(ord_num("-1000000"), "ke--1000000");
}
#[test]
fn years_are_plain_cardinals() {
assert_eq!(year("1"), "satu");
assert_eq!(year("999"), "sembilan ratus sembilan puluh sembilan");
assert_eq!(year("1000"), "seribu");
assert_eq!(year("1492"), "seribu empat ratus sembilan puluh dua");
assert_eq!(
year("1999"),
"seribu sembilan ratus sembilan puluh sembilan"
);
assert_eq!(year("2024"), "dua ribu dua puluh empat");
assert_eq!(year("2100"), "dua ribu seratus");
assert_eq!(year("-44"), "negatif empat puluh empat");
assert_eq!(year("-500"), "negatif lima ratus");
}
#[test]
fn corpus_currency() {
for (arg, want) in [
("0", "kosong euro"),
("1", "kosong euro satu sen"),
("2", "kosong euro dua sen"),
("100", "satu euro"),
("12.34", "dua belas euro tiga puluh empat sen"),
("0.01", "kosong euro satu sen"),
("1.0", "satu euro"),
(
"99.99",
"sembilan puluh sembilan euro sembilan puluh sembilan sen",
),
(
"1234.56",
"seribu dua ratus tiga puluh empat euro lima puluh enam sen",
),
("-12.34", "negatif dua belas euro tiga puluh empat sen"),
("1000000", "sepuluh ribu euro"),
("0.5", "kosong euro lima puluh sen"),
] {
assert_eq!(cur(arg, "EUR").unwrap(), want, "EUR {}", arg);
}
for (arg, want) in [
("0", "kosong dolar"),
("1", "kosong dolar satu sen"),
("2", "kosong dolar dua sen"),
("100", "satu dolar"),
("12.34", "dua belas dolar tiga puluh empat sen"),
("0.01", "kosong dolar satu sen"),
("1.0", "satu dolar"),
(
"99.99",
"sembilan puluh sembilan dolar sembilan puluh sembilan sen",
),
(
"1234.56",
"seribu dua ratus tiga puluh empat dolar lima puluh enam sen",
),
("-12.34", "negatif dua belas dolar tiga puluh empat sen"),
("1000000", "sepuluh ribu dolar"),
("0.5", "kosong dolar lima puluh sen"),
] {
assert_eq!(cur(arg, "USD").unwrap(), want, "USD {}", arg);
}
for (arg, want) in [
("0", "kosong paun"),
("1", "kosong paun satu peni"),
("2", "kosong paun dua peni"),
("100", "satu paun"),
("12.34", "dua belas paun tiga puluh empat peni"),
("0.01", "kosong paun satu peni"),
("1.0", "satu paun"),
(
"99.99",
"sembilan puluh sembilan paun sembilan puluh sembilan peni",
),
(
"1234.56",
"seribu dua ratus tiga puluh empat paun lima puluh enam peni",
),
("-12.34", "negatif dua belas paun tiga puluh empat peni"),
("1000000", "sepuluh ribu paun"),
("0.5", "kosong paun lima puluh peni"),
] {
assert_eq!(cur(arg, "GBP").unwrap(), want, "GBP {}", arg);
}
}
#[test]
fn currency_untested_codes() {
assert_eq!(
cur("12.34", "MYR").unwrap(),
"dua belas ringgit tiga puluh empat sen"
);
assert_eq!(
cur("12.34", "SGD").unwrap(),
"dua belas dolar tiga puluh empat sen"
);
assert_eq!(
cur("12.34", "BND").unwrap(),
"dua belas dolar tiga puluh empat sen"
);
assert_eq!(
cur("12.34", "IDR").unwrap(),
"dua belas rupiah tiga puluh empat sen"
);
}
#[test]
fn corpus_currency_not_implemented() {
for code in ["JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
match cur("12.34", code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!(
"Currency code \"{}\" not implemented for \"Num2Word_MS\"",
code
)
),
other => panic!("{}: expected NotImplemented, got {:?}", code, other),
}
assert!(matches!(cur("100", code), Err(N2WError::NotImplemented(_))));
}
}
#[test]
fn currency_negative_ints() {
assert_eq!(cur("-1", "MYR").unwrap(), "negatif kosong ringgit satu sen");
assert_eq!(cur("-100", "MYR").unwrap(), "negatif satu ringgit");
assert_eq!(cur("-101", "MYR").unwrap(), "negatif satu ringgit satu sen");
assert_eq!(
cur("-1000000", "MYR").unwrap(),
"negatif sepuluh ribu ringgit"
);
}
#[test]
fn currency_fractional_cents_are_always_kosong() {
assert_eq!(cur("1.011", "MYR").unwrap(), "satu ringgit kosong sen");
assert_eq!(cur("1.005", "MYR").unwrap(), "satu ringgit kosong sen");
assert_eq!(cur("1.234", "MYR").unwrap(), "satu ringgit kosong sen");
assert_eq!(cur("2.675", "MYR").unwrap(), "dua ringgit kosong sen");
assert_eq!(cur("0.009", "MYR").unwrap(), "kosong ringgit kosong sen");
assert_eq!(
cur("-1.011", "MYR").unwrap(),
"negatif satu ringgit kosong sen"
);
assert_eq!(cur("0.001", "MYR").unwrap(), "kosong ringgit");
assert_eq!(cur("1.999", "MYR").unwrap(), "dua ringgit");
assert_eq!(cur("0.999", "MYR").unwrap(), "satu ringgit");
}
#[test]
fn currency_no_has_decimal_guard() {
for arg in ["5", "5.00", "5.0"] {
let val = CurrencyValue::parse(arg, false, true, true).unwrap();
assert_eq!(
LangMs::new()
.to_currency(&val, "MYR", true, None, false)
.unwrap(),
"lima ringgit",
"Decimal({})",
arg
);
}
assert_eq!(cur("0.0", "MYR").unwrap(), "kosong ringgit");
assert_eq!(cur("-0.0", "MYR").unwrap(), "kosong ringgit");
}
#[test]
fn currency_decimal_context_limit_on_ints() {
assert!(cur("99999999999999999999999999", "MYR")
.unwrap()
.starts_with("sembilan ratus"));
assert_eq!(
cur("100000000000000000000000000", "MYR").unwrap(),
"100000000000000000000000000 MYR"
);
assert_eq!(
cur("-100000000000000000000000000", "MYR").unwrap(),
"-100000000000000000000000000 MYR"
);
assert_eq!(
cur("1000000000000000000000000000000", "JPY").unwrap(),
"1000000000000000000000000000000 JPY"
);
assert!(matches!(
cur("10000000000000000000000000", "JPY"),
Err(N2WError::NotImplemented(_))
));
}
#[test]
fn currency_decimal_context_limit_defers_on_floats() {
for arg in ["100000000000000000000000000", "1e26", "1e30", "1e300"] {
let val = CurrencyValue::parse(arg, false, true, true).unwrap();
assert!(
matches!(
LangMs::new().to_currency(&val, "MYR", true, None, false),
Err(N2WError::NotImplemented(_))
),
"float {} should defer to Python, not answer",
arg
);
}
let val = CurrencyValue::parse("99999999999999999999999999.99", false, true, true).unwrap();
assert!(LangMs::new()
.to_currency(&val, "MYR", true, None, false)
.unwrap()
.starts_with("sembilan puluh sembilan trilion"));
}
#[test]
fn currency_ignores_base_kwargs() {
let val = CurrencyValue::parse("12.34", false, true, true).unwrap();
let want = "dua belas ringgit tiga puluh empat sen";
let ms = LangMs::new();
for cents in [true, false] {
for adjective in [true, false] {
for separator in [None, Some(" dan"), Some(",")] {
assert_eq!(
ms.to_currency(&val, "MYR", cents, separator, adjective)
.unwrap(),
want
);
}
}
}
}
#[test]
fn corpus_cheque() {
for (code, want) in [
("EUR", "SERIBU DUA RATUS TIGA PULUH EMPAT AND 56/100 EURO"),
("USD", "SERIBU DUA RATUS TIGA PULUH EMPAT AND 56/100 DOLAR"),
("GBP", "SERIBU DUA RATUS TIGA PULUH EMPAT AND 56/100 PAUN"),
] {
assert_eq!(cheque("1234.56", code).unwrap(), want, "cheque {}", code);
}
for code in ["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_MS\"",
code
)
),
other => panic!("{}: expected NotImplemented, got {:?}", code, other),
}
}
}
#[test]
fn cheque_extras() {
assert_eq!(cheque("1.05", "MYR").unwrap(), "SATU AND 05/100 RINGGIT");
assert_eq!(
cheque("-1234.56", "MYR").unwrap(),
"MINUS SERIBU DUA RATUS TIGA PULUH EMPAT AND 56/100 RINGGIT"
);
assert_eq!(cheque("0", "MYR").unwrap(), "KOSONG AND 00/100 RINGGIT");
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use std::str::FromStr;
fn f(value: f64, precision: u32) -> String {
LangMs::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(s: &str, precision: u32) -> String {
LangMs::new()
.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
},
None,
)
.unwrap()
}
#[test]
fn corpus_cardinal_float() {
let rows: &[(f64, u32, &str)] = &[
(0.0, 1, "kosong"),
(0.5, 1, "kosong"),
(1.0, 1, "satu"),
(1.5, 1, "satu"),
(2.25, 2, "dua"),
(3.14, 2, "tiga"),
(0.01, 2, "kosong"),
(0.1, 1, "kosong"),
(0.99, 2, "kosong"),
(1.01, 2, "satu"),
(12.34, 2, "dua belas"),
(99.99, 2, "sembilan puluh sembilan"),
(100.5, 1, "seratus"),
(1234.56, 2, "seribu dua ratus tiga puluh empat"),
(-0.5, 1, "kosong"),
(-1.5, 1, "negatif satu"),
(-12.34, 2, "negatif dua belas"),
(1.005, 3, "satu"),
(2.675, 3, "dua"),
];
for (v, p, want) in rows {
assert_eq!(f(*v, *p), *want, "float {}", v);
}
}
#[test]
fn corpus_cardinal_dec() {
assert_eq!(d("0.01", 2), "kosong");
assert_eq!(d("1.10", 2), "satu");
assert_eq!(d("12.345", 3), "dua belas");
assert_eq!(
d("98746251323029.99", 2),
"sembilan puluh lapan trilion tujuh ratus empat puluh enam bilion \
dua ratus lima puluh satu juta tiga ratus dua puluh tiga ribu \
dua puluh sembilan"
);
assert_eq!(d("0.001", 3), "kosong");
}
#[test]
fn extra_float_and_decimal() {
assert_eq!(f(-0.0, 1), "kosong");
assert_eq!(d("-0.5", 1), "kosong");
assert_eq!(d("-12.34", 2), "negatif dua belas");
assert_eq!(d("5.00", 2), "lima");
assert_eq!(d("1000.00", 2), "seribu");
assert_eq!(
d("-98746251323029.99", 2).split(' ').next().unwrap(),
"negatif"
);
assert_eq!(d("12.00", 2), "dua belas");
assert_eq!(d("19.99", 2), "sembilan belas");
}
#[test]
fn precision_override_is_ignored() {
let ms = LangMs::new();
for p in [None, Some(0), Some(1), Some(5)] {
assert_eq!(
ms.to_cardinal_float(
&FloatValue::Float {
value: 12.34,
precision: 2
},
p
)
.unwrap(),
"dua belas"
);
assert_eq!(
ms.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str("12.345").unwrap(),
precision: 3
},
p
)
.unwrap(),
"dua belas"
);
}
}
}