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_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const ZERO_WORD: &str = "zero";
const ONES: [&str; 10] = [
"", "siji", "loro", "telu", "papat", "lima", "enem", "pitu", "wolu", "sanga",
];
const TENS: [&str; 10] = [
"",
"sepuluh",
"rong puluh",
"telung puluh",
"patang puluh",
"seket",
"sewidak",
"pitung puluh",
"wolung puluh",
"sanga puluh",
];
const HUNDRED: &str = "atus";
const THOUSAND: &str = "ewu";
const MILLION: &str = "yuta";
const POINTWORD: &str = "point";
const LANG_NAME: &str = "Num2Word_JW";
const DEFAULT_SEPARATOR: &str = " ";
const BASE_DEFAULT_SEPARATOR: &str = ",";
const FALLBACK_CURRENCY: &str = "IDR";
pub struct LangJv {
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl LangJv {
pub fn new() -> Self {
let mut currency_forms: HashMap<&'static str, CurrencyForms> = HashMap::new();
currency_forms.insert(
"IDR",
CurrencyForms::new(&["rupiah", "rupiah"], &["sen", "sen"]),
);
currency_forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
currency_forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["cent", "cents"]),
);
let fallback_forms = currency_forms
.get(FALLBACK_CURRENCY)
.expect("FALLBACK_CURRENCY is inserted above")
.clone();
LangJv {
currency_forms,
fallback_forms,
}
}
}
impl Default for LangJv {
fn default() -> Self {
Self::new()
}
}
fn idx(n: &BigInt) -> usize {
n.to_usize().unwrap_or(0)
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, int_to_word(&number.abs()));
}
let ten = BigInt::from(10u32);
let hundred = BigInt::from(100u32);
let thousand = BigInt::from(1_000u32);
let million = BigInt::from(1_000_000u32);
let billion = BigInt::from(1_000_000_000u32);
if number < &ten {
return ONES[idx(number)].to_string();
}
if number < &hundred {
let (tens_val, ones_val) = number.div_rem(&ten);
if ones_val.is_zero() {
return TENS[idx(&tens_val)].to_string();
}
return format!("{} {}", TENS[idx(&tens_val)], ONES[idx(&ones_val)]);
}
if number < &thousand {
let (hundreds_val, remainder) = number.div_rem(&hundred);
let mut result = format!("{} {}", ONES[idx(&hundreds_val)], HUNDRED);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
if number < &million {
let (thousands_val, remainder) = number.div_rem(&thousand);
let mut result = format!("{} {}", int_to_word(&thousands_val), THOUSAND);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
if number < &billion {
let (millions_val, remainder) = number.div_rem(&million);
let mut result = format!("{} {}", int_to_word(&millions_val), MILLION);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
number.to_string()
}
fn py_int(s: &str) -> Result<BigInt> {
BigInt::from_str(s).map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
})
}
fn py_int_digit(ch: char) -> Result<usize> {
ch.to_digit(10).map(|d| d as usize).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})
}
fn shortest_repr_digits(a: f64) -> (String, i32) {
let split = |s: &str| -> (String, i32) {
let (mant, exp) = s.split_once('e').expect("{:e} always emits an 'e'");
(
mant.chars().filter(|c| *c != '.').collect(),
exp.parse::<i32>().expect("{:e} exponent is an integer") + 1,
)
};
let shortest = format!("{:e}", a);
let (digits, decpt) = split(&shortest);
let ties_even = format!("{:.*e}", digits.len() - 1, a);
if ties_even.parse::<f64>() == Ok(a) {
return split(&ties_even);
}
(digits, decpt)
}
fn py_str_f64(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 (digits, decpt) = shortest_repr_digits(v.abs());
let ndig = digits.len() as i32;
if decpt <= -4 || decpt > 16 {
let mantissa = if ndig > 1 {
format!("{}.{}", &digits[..1], &digits[1..])
} else {
digits.clone()
};
let exp = decpt - 1;
let (esign, eabs) = if exp < 0 {
("-", -(exp as i64))
} else {
("+", exp as i64)
};
return format!("{}{}e{}{:0>2}", sign, mantissa, esign, eabs);
}
if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), digits)
} else if decpt >= ndig {
format!("{}{}{}.0", sign, digits, "0".repeat((decpt - ndig) as usize))
} else {
format!("{}{}.{}", sign, &digits[..decpt as usize], &digits[decpt as usize..])
}
}
fn py_str_decimal(value: &BigDecimal) -> String {
let (coefficient, scale) = value.as_bigint_and_exponent();
let exp = -scale;
let sign = if coefficient.is_negative() { "-" } else { "" };
let int_digits = coefficient.abs().to_string();
let ndig = int_digits.len() as i64;
let leftdigits = exp + ndig;
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 >= ndig {
(
format!("{}{}", int_digits, "0".repeat((dotplace - ndig) 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)
}
fn cardinal_from_repr(n: &str) -> Result<String> {
let n = n.trim();
let (ret, n) = match n.strip_prefix('-') {
Some(rest) => (NEGWORD.to_string(), rest),
None => (String::new(), n),
};
match n.split_once('.') {
Some((left, right)) => {
let mut ret = ret;
ret.push_str(&int_to_word(&py_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
for digit in right.chars() {
ret.push_str(&int_to_word(&BigInt::from(py_int_digit(digit)?)));
ret.push(' ');
}
Ok(ret.trim().to_string())
}
None => Ok(format!("{}{}", ret, int_to_word(&py_int(n)?)).trim().to_string()),
}
}
impl Lang for LangJv {
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!("{}-e", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", repr_str))
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
python_decimal_parse(s)
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
if to == "ordinal_num" {
return Ok(format!("{}Infinity.", if negative { "-" } else { "" }));
}
Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
))
}
fn nan_result(&self, to: &str) -> Result<String> {
if to == "ordinal_num" {
return Ok("NaN.".into());
}
Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".into(),
))
}
fn default_currency(&self) -> &str {
"IDR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"point"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (ret, n) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, int_to_word(&n)).trim().to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-e", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", 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 _ = precision_override;
let n = match value {
FloatValue::Float { value, .. } => py_str_f64(*value),
FloatValue::Decimal { value, .. } => py_str_decimal(value),
};
cardinal_from_repr(&n)
}
fn lang_name(&self) -> &str {
LANG_NAME
}
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> {
let separator = separator.unwrap_or(self.default_separator());
let _ = adjective;
let separator = if separator == BASE_DEFAULT_SEPARATOR {
DEFAULT_SEPARATOR
} else {
separator
};
let is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(v) => v.abs().to_string(),
CurrencyValue::Decimal { value: d, .. } => d.abs().to_string(),
};
let mut parts = s.split('.');
let part0 = parts.next().unwrap_or("");
let part1 = parts.next();
let left = if part0.is_empty() {
BigInt::zero()
} else {
BigInt::from_str(part0).map_err(|e| N2WError::Value(e.to_string()))?
};
let right = match part1 {
Some(f) if !f.is_empty() => {
let mut two: String = f.chars().take(2).collect();
while two.chars().count() < 2 {
two.push('0');
}
BigInt::from_str(&two).map_err(|e| N2WError::Value(e.to_string()))?
}
_ => BigInt::zero(),
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.fallback_forms);
let cr1 = &forms.unit;
let cr2 = &forms.subunit;
let mut result = format!(
"{} {}",
int_to_word(&left),
if left.is_one() { &cr1[0] } else { &cr1[1] }
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(if right.is_one() { &cr2[0] } else { &cr2[1] });
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod float_tests {
use super::*;
fn prec(arg: &str) -> u32 {
match arg.split_once('.') {
Some((_, frac)) => frac.len() as u32,
None => 0,
}
}
fn card_float(arg: &str) -> String {
let v = FloatValue::Float {
value: f64::from_str(arg).unwrap(),
precision: prec(arg),
};
LangJv::new().to_cardinal_float(&v, None).unwrap()
}
fn card_dec(arg: &str) -> String {
let v = FloatValue::Decimal {
value: BigDecimal::from_str(arg).unwrap(),
precision: prec(arg),
};
LangJv::new().to_cardinal_float(&v, None).unwrap()
}
#[test]
fn corpus_cardinal_float() {
let cases = [
("0.0", "zero point zero"),
("0.5", "zero point lima"),
("1.0", "siji point zero"),
("1.5", "siji point lima"),
("2.25", "loro point loro lima"),
("3.14", "telu point siji papat"),
("0.01", "zero point zero siji"),
("0.1", "zero point siji"),
("0.99", "zero point sanga sanga"),
("1.01", "siji point zero siji"),
("12.34", "sepuluh loro point telu papat"),
("99.99", "sanga puluh sanga point sanga sanga"),
("100.5", "siji atus point lima"),
("1234.56", "siji ewu loro atus telung puluh papat point lima enem"),
("-0.5", "minus zero point lima"),
("-1.5", "minus siji point lima"),
("-12.34", "minus sepuluh loro point telu papat"),
("1.005", "siji point zero zero lima"),
("2.675", "loro point enem pitu lima"),
];
for (arg, want) in cases {
assert_eq!(card_float(arg), want, "float {}", arg);
}
}
#[test]
fn corpus_cardinal_dec() {
let cases = [
("0.01", "zero point zero siji"),
("1.10", "siji point siji zero"),
("12.345", "sepuluh loro point telu papat lima"),
("98746251323029.99", "98746251323029 point sanga sanga"),
("0.001", "zero point zero zero siji"),
];
for (arg, want) in cases {
assert_eq!(card_dec(arg), want, "dec {}", arg);
}
}
#[test]
fn negative_zero_float_carries_sign() {
assert_eq!(card_float("-0.0"), "minus zero point zero");
}
#[test]
fn extra_live_interpreter_rows() {
assert_eq!(card_float("1000000000.5"), "1000000000 point lima");
assert_eq!(
card_float("12345.678"),
"sepuluh loro ewu telu atus patang puluh lima point enem pitu wolu"
);
assert_eq!(card_dec("100"), "siji atus");
}
#[test]
fn exponent_notation_raises_valueerror_like_python() {
let l = LangJv::new();
for (f, tok) in [(1e16, "1e+16"), (1e-5, "1e-05"), (1e21, "1e+21")] {
let v = FloatValue::Float { value: f, precision: 0 };
match l.to_cardinal_float(&v, None) {
Err(N2WError::Value(m)) => assert_eq!(
m,
format!("invalid literal for int() with base 10: '{}'", tok)
),
other => panic!("{}: expected ValueError, got {:?}", tok, other.map(|_| ())),
}
}
let v = FloatValue::Decimal {
value: BigDecimal::from_str("1E+2").unwrap(),
precision: 0,
};
match l.to_cardinal_float(&v, None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1E+2'")
}
other => panic!("expected ValueError, got {:?}", other.map(|_| ())),
}
}
#[test]
fn precision_override_is_inert() {
let v = FloatValue::Float { value: 2.675, precision: 3 };
let base = LangJv::new().to_cardinal_float(&v, None).unwrap();
for p in [0u32, 1, 2, 5, 10] {
assert_eq!(
LangJv::new().to_cardinal_float(&v, Some(p)).unwrap(),
base,
"precision override {}",
p
);
}
}
}