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 num_bigint::BigInt;
use num_traits::{Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "ដក ";
const ONES: [&str; 10] = [
"សូន្យ", "មួយ", "ពីរ", "បី", "បួន", "ប្រាំ", "ប្រាំមួយ", "ប្រាំពីរ", "ប្រាំបី", "ប្រាំបួន", ];
const TENS: [&str; 10] = [
"", "ដប់", "ម្ភៃ", "សាមសិប", "សែសិប", "ហាសិប", "ហុកសិប", "ចិតសិប", "ប៉ែតសិប", "កៅសិប", ];
const HUNDRED: &str = "រយ";
const THOUSAND: &str = "ពាន់";
const TEN_THOUSAND: &str = "ម៉ឺន";
const HUNDRED_THOUSAND: &str = "សែន";
const MILLION: &str = " លាន";
const ORDINAL_PREFIX: &str = "ទី";
const YEAR_PREFIX: &str = "ឆ្នាំ ";
const KHR: &str = "KHR";
pub struct LangKm {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl LangKm {
pub fn new() -> Self {
let mut currency_forms = HashMap::with_capacity(3);
currency_forms.insert(KHR, CurrencyForms::new(&["រៀល", "រៀល"], &["សេន", "សេន"]));
currency_forms.insert(
"USD",
CurrencyForms::new(&["ដុល្លារ", "ដុល្លារ"], &["សេន", "សេន"]),
);
currency_forms.insert("EUR", CurrencyForms::new(&["អឺរ៉ូ", "អឺរ៉ូ"], &["សេន", "សេន"]));
LangKm { currency_forms }
}
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 int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ONES[0].to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&number.abs()));
}
macro_rules! small {
($v:expr) => {
$v.to_usize().expect("bounded by the enclosing guard")
};
}
let ten = BigInt::from(10);
let twenty = BigInt::from(20);
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let ten_thousand = BigInt::from(10_000);
let hundred_thousand = BigInt::from(100_000);
let million = BigInt::from(1_000_000);
let billion = BigInt::from(1_000_000_000);
if *number < ten {
return ONES[small!(number)].to_string();
}
if *number == ten {
return TENS[1].to_string();
}
if *number < twenty {
return format!("{}{}", TENS[1], ONES[small!(number - &ten)]);
}
if *number < hundred {
let tens_val = number / &ten;
let ones_val = number % &ten;
if ones_val.is_zero() {
return TENS[small!(&tens_val)].to_string();
}
return format!("{}{}", TENS[small!(&tens_val)], ONES[small!(&ones_val)]);
}
if *number < thousand {
return self.glued(number, &hundred, HUNDRED);
}
if *number < ten_thousand {
return self.glued(number, &thousand, THOUSAND);
}
if *number < hundred_thousand {
return self.glued(number, &ten_thousand, TEN_THOUSAND);
}
if *number < million {
return self.glued(number, &hundred_thousand, HUNDRED_THOUSAND);
}
if *number < billion {
let millions_val = number / &million;
let remainder = number % &million;
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()
}
fn glued(&self, number: &BigInt, unit: &BigInt, word: &str) -> String {
let digit = number / unit;
let remainder = number % unit;
let mut result = format!(
"{}{}",
ONES[digit.to_usize().expect("bounded by the enclosing guard")],
word
);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
result
}
fn cardinal_from_pystr(&self, s: &str) -> Result<String> {
let n = s.trim();
let (ret, n) = match n.strip_prefix('-') {
Some(rest) => (NEGWORD, rest),
None => ("", n),
};
if let Some((left, right)) = n.split_once('.') {
let mut out = String::from(ret);
out.push_str(&self.int_to_word(&Self::py_int(left)?));
out.push(' ');
out.push_str(self.pointword());
out.push(' ');
for ch in right.chars() {
let d = Self::py_int(&ch.to_string())?;
out.push_str(&self.int_to_word(&d));
out.push(' ');
}
Ok(out.trim().to_string())
} else {
Ok(format!("{}{}", ret, self.int_to_word(&Self::py_int(n)?))
.trim()
.to_string())
}
}
}
fn py_float_str(value: f64, precision: u32) -> String {
if value.is_nan() {
return "nan".to_string();
}
let neg = value.is_sign_negative();
let mag = value.abs();
let body = if mag.is_infinite() {
"inf".to_string()
} else if mag != 0.0 && !(1e-4..1e16).contains(&mag) {
let s = format!("{:e}", mag);
let (mant, exp) = s.split_once('e').expect("{:e} always contains 'e'");
let e: i32 = exp.parse().expect("{:e} exponent is a valid i32");
format!("{}e{}{:02}", mant, if e < 0 { '-' } else { '+' }, e.abs())
} else {
format!("{:.*}", precision as usize, mag)
};
if neg {
format!("-{}", body)
} else {
body
}
}
fn py_str(value: &FloatValue) -> String {
match value {
FloatValue::Float { value, precision } => py_float_str(*value, *precision),
FloatValue::Decimal { value, .. } => python_decimal_str(value),
}
}
impl Default for LangKm {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangKm {
fn default_currency(&self) -> &str {
"KHR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"ចំណុច"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let ret = if value.is_negative() { NEGWORD } else { "" };
let out = format!("{}{}", ret, self.int_to_word(&value.abs()));
Ok(out.trim().to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", ORDINAL_PREFIX, self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", ORDINAL_PREFIX, value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", YEAR_PREFIX, self.to_cardinal(value)?))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
self.cardinal_from_pystr(&py_str(value))
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!(
"{}{}",
ORDINAL_PREFIX,
self.cardinal_float_entry(value, None)?
))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}{}", ORDINAL_PREFIX, repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!(
"{}{}",
YEAR_PREFIX,
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(),
)),
ParsedNumber::NaN => Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".into(),
)),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_KM"
}
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 text = match val {
CurrencyValue::Int(v) => v.abs().to_string(),
CurrencyValue::Decimal { value: d, .. } => d.abs().to_string(),
};
let mut parts = text.splitn(2, '.');
let whole_part = parts.next().unwrap_or("");
let frac_part = parts.next();
let left = if whole_part.is_empty() {
BigInt::zero()
} else {
Self::py_int(whole_part)?
};
let right = match frac_part {
Some(frac) if !frac.is_empty() => {
let mut digits: String = frac.chars().take(2).collect();
while digits.chars().count() < 2 {
digits.push('0');
}
Self::py_int(&digits)?
}
_ => BigInt::zero(),
};
let forms = self
.currency_forms(currency)
.or_else(|| self.currency_forms(KHR))
.ok_or_else(|| N2WError::Key(format!("'{}'", KHR)))?;
let mut result = format!("{} {}", self.int_to_word(&left), forms.unit[0]);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(&forms.subunit[0]);
}
Ok(if is_negative {
format!("{}{}", NEGWORD, result)
} else {
result
})
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use bigdecimal::BigDecimal;
fn dec(s: &str) -> FloatValue {
let bd = BigDecimal::from_str(s).unwrap();
let precision = bd.as_bigint_and_exponent().1.unsigned_abs() as u32;
FloatValue::Decimal { value: bd, precision }
}
#[test]
fn whole_floats_keep_their_point_digits() {
let km = LangKm::new();
assert_eq!(
km.cardinal_float_entry(&FloatValue::Float { value: 5.0, precision: 1 }, None)
.unwrap(),
"ប្រាំ ចំណុច សូន្យ"
);
assert_eq!(
km.cardinal_float_entry(&FloatValue::Float { value: -0.0, precision: 1 }, None)
.unwrap(),
"ដក សូន្យ ចំណុច សូន្យ"
);
assert_eq!(
km.cardinal_float_entry(&dec("5.00"), None).unwrap(),
"ប្រាំ ចំណុច សូន្យ សូន្យ"
);
assert_eq!(km.cardinal_float_entry(&dec("5"), None).unwrap(), "ប្រាំ");
assert_eq!(
km.cardinal_float_entry(
&FloatValue::Float { value: 1_000_000_000.0, precision: 1 },
None
)
.unwrap(),
"1000000000 ចំណុច សូន្យ"
);
}
#[test]
fn float_entries_prefix_like_python() {
let km = LangKm::new();
let v = FloatValue::Float { value: -3.0, precision: 1 };
assert_eq!(
km.ordinal_float_entry(&v).unwrap(),
"ទីដក បី ចំណុច សូន្យ"
);
assert_eq!(km.year_float_entry(&v).unwrap(), "ឆ្នាំ ដក បី ចំណុច សូន្យ");
assert_eq!(km.ordinal_num_float_entry(&v, "-3.0").unwrap(), "ទី-3.0");
}
#[test]
fn exponential_reprs_raise_value_error() {
let km = LangKm::new();
let e16 = FloatValue::Float { value: 1e16, precision: 16 };
assert!(matches!(
km.cardinal_float_entry(&e16, None),
Err(N2WError::Value(_))
));
assert!(matches!(
km.year_float_entry(&dec("1E+2")),
Err(N2WError::Value(_))
));
assert_eq!(km.ordinal_num_float_entry(&e16, "1e+16").unwrap(), "ទី1e+16");
}
#[test]
fn python_float_str_matches_cpython() {
assert_eq!(py_float_str(5.0, 1), "5.0");
assert_eq!(py_float_str(-0.0, 1), "-0.0");
assert_eq!(py_float_str(1e15, 1), "1000000000000000.0");
assert_eq!(py_float_str(1e16, 16), "1e+16");
assert_eq!(py_float_str(1e20, 20), "1e+20");
assert_eq!(py_float_str(0.0001, 4), "0.0001");
assert_eq!(py_float_str(1e-5, 5), "1e-05");
assert_eq!(py_float_str(f64::INFINITY, 0), "inf");
assert_eq!(py_float_str(f64::NAN, 0), "nan");
}
#[test]
fn infinity_string_raises_value_error() {
let km = LangKm::new();
assert!(matches!(km.str_to_number("Infinity"), Err(N2WError::Value(_))));
assert!(matches!(km.str_to_number("-Infinity"), Err(N2WError::Value(_))));
assert!(matches!(km.str_to_number("NaN"), Err(N2WError::Value(_))));
assert!(matches!(km.str_to_number("1.5"), Ok(ParsedNumber::Dec(_))));
}
}