use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "point";
const ZERO: &str = "zero";
const ONES: [&str; 10] = [
"", "hiji", "dua", "tilu", "opat", "lima", "genep", "tujuh", "dalapan", "salapan",
];
const TENS: [&str; 10] = [
"",
"sapuluh",
"dua puluh",
"tilu puluh",
"opat puluh",
"lima puluh",
"genep puluh",
"tujuh puluh",
"dalapan puluh",
"salapan puluh",
];
const HUNDRED: &str = "ratus";
const THOUSAND: &str = "rebu";
const MILLION: &str = "juta";
const CURRENCY_ORDER: [&str; 3] = ["IDR", "USD", "EUR"];
fn python_str(d: &BigDecimal) -> String {
let (int_val, scale) = d.as_bigint_and_exponent();
let digits = int_val.abs().to_string();
if scale < 0 {
let exp = digits.len() as i64 - 1 - scale;
let mut mantissa = String::from(&digits[..1]);
if digits.len() > 1 {
mantissa.push('.');
mantissa.push_str(&digits[1..]);
}
return format!("{}e+{:02}", mantissa, exp);
}
let scale = scale as usize;
if scale == 0 {
return digits;
}
if digits.len() > scale {
let point = digits.len() - scale;
format!("{}.{}", &digits[..point], &digits[point..])
} else {
format!("0.{}{}", "0".repeat(scale - digits.len()), digits)
}
}
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 float_no_point_repr(v: f64) -> String {
if v.is_nan() {
return "nan".to_string();
}
if v.is_infinite() {
return "inf".to_string();
}
let digits = BigInt::from_f64(v.abs())
.expect("finite whole f64 converts exactly")
.to_string();
let exp = digits.len() - 1; let mant = digits.trim_end_matches('0');
let mant = if mant.len() <= 1 {
digits[..1].to_string()
} else {
format!("{}.{}", &mant[..1], &mant[1..])
};
format!("{}e+{}", mant, exp)
}
pub struct LangSu {
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_fallback: CurrencyForms,
}
impl Default for LangSu {
fn default() -> Self {
Self::new()
}
}
impl LangSu {
pub fn new() -> Self {
let mut currency_forms = HashMap::with_capacity(CURRENCY_ORDER.len());
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 currency_fallback = currency_forms[CURRENCY_ORDER[0]].clone();
LangSu {
currency_forms,
currency_fallback,
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO.to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&number.abs()));
}
if *number < BigInt::from(10) {
return ONES[number.to_usize().expect("0 < number < 10")].to_string();
}
if *number < BigInt::from(100) {
let (tens_val, ones_val) = number.div_rem(&BigInt::from(10));
let tens_val = tens_val.to_usize().expect("10 <= number < 100");
let ones_val = ones_val.to_usize().expect("remainder mod 10");
if ones_val == 0 {
return TENS[tens_val].to_string();
}
return format!("{} {}", TENS[tens_val], ONES[ones_val]);
}
if *number < BigInt::from(1000) {
let (hundreds_val, remainder) = number.div_rem(&BigInt::from(100));
let hundreds_val = hundreds_val.to_usize().expect("100 <= number < 1000");
let mut result = format!("{} {}", ONES[hundreds_val], HUNDRED);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
return result;
}
if *number < BigInt::from(1_000_000) {
let (thousands_val, remainder) = number.div_rem(&BigInt::from(1000));
let mut result = format!("{} {}", self.int_to_word(&thousands_val), THOUSAND);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
return result;
}
if *number < BigInt::from(1_000_000_000) {
let (millions_val, remainder) = number.div_rem(&BigInt::from(1_000_000));
let mut result = format!("{} {}", self.int_to_word(&millions_val), MILLION);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
return result;
}
number.to_string()
}
}
impl Lang for LangSu {
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, self.int_to_word(&n)).trim().to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-na", 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 cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
if value.has_visible_point() {
return self.to_cardinal_float(value, precision_override);
}
let is_negative = value.is_negative();
let text = match value {
FloatValue::Float { value, .. } => float_no_point_repr(*value),
FloatValue::Decimal { value, .. } => python_decimal_str(&value.abs()),
};
let body = self.int_to_word(&py_int(&text)?);
let out = if is_negative {
format!("{}{}", NEGWORD, body)
} else {
body
};
Ok(out.trim().to_string())
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!("{}-na", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", 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 to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let s = match value {
FloatValue::Float { value: f, precision } => {
format!("{:.*}", *precision as usize, f)
}
FloatValue::Decimal { value: d, .. } => {
let body = python_str(d);
if d.is_negative() {
format!("-{}", body)
} else {
body
}
}
};
let n = s.trim();
let (is_neg, n) = match n.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, n),
};
let mut ret = if is_neg {
NEGWORD.to_string()
} else {
String::new()
};
match n.split_once('.') {
Some((left, right)) => {
ret.push_str(&self.int_to_word(&py_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
for ch in right.chars() {
ret.push_str(&self.int_to_word(&py_int(&ch.to_string())?));
ret.push(' ');
}
Ok(ret.trim().to_string())
}
None => {
Ok(format!("{}{}", ret, self.int_to_word(&py_int(n)?))
.trim()
.to_string())
}
}
}
fn lang_name(&self) -> &str {
"Num2Word_SU"
}
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 is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(v) => v.abs().to_string(),
CurrencyValue::Decimal { value: d, .. } => python_str(&d.abs()),
};
let parts: Vec<&str> = s.split('.').collect();
let left = if parts[0].is_empty() {
BigInt::zero()
} else {
py_int(parts[0])?
};
let right = if parts.len() > 1 && !parts[1].is_empty() {
let mut frac: String = parts[1].chars().take(2).collect();
while frac.chars().count() < 2 {
frac.push('0');
}
py_int(&frac)?
} else {
BigInt::zero()
};
let cr = self
.currency_forms
.get(currency)
.unwrap_or(&self.currency_fallback);
let mut result = format!(
"{} {}",
self.int_to_word(&left),
if left.is_one() { &cr.unit[0] } else { &cr.unit[1] }
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(if right.is_one() {
&cr.subunit[0]
} else {
&cr.subunit[1]
});
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}