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;
use std::sync::OnceLock;
const ONES: [&str; 10] = [
"", "pakhat", "pahnih", "pathum", "pali", "panga", "paruk", "parih", "pariat", "pakua",
];
const TENS: [&str; 10] = [
"",
"pahra",
"pahnih kip",
"pathum kip",
"pali kip",
"panga kip",
"paruk kip",
"parih kip",
"pariat kip",
"pakua kip",
];
const HUNDRED: &str = "phazar";
const THOUSAND: &str = "thawngkhat";
const MILLION: &str = "milin";
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "decimal";
const ZERO_WORD: &str = "zero";
const ORDINAL_SUFFIX: &str = "-nak";
const LANG_NAME: &str = "Num2Word_CNH";
const CURRENCY_FALLBACK_KEY: &str = "MMK";
fn idx(n: &BigInt) -> usize {
n.to_usize()
.expect("guarded by _int_to_word's range checks: always 0..=9")
}
fn decimal_to_plain_string(value: &BigDecimal, precision: u32) -> String {
if precision == 0 {
return value.with_scale(0).as_bigint_and_exponent().0.to_string();
}
let scaled = value.with_scale(precision as i64);
let digits = scaled.as_bigint_and_exponent().0.to_string();
let precision = precision as usize;
let width = digits.len().max(precision + 1);
let padded = format!("{}{}", "0".repeat(width - digits.len()), digits);
let (int_part, frac_part) = padded.split_at(padded.len() - precision);
format!("{}.{}", int_part, frac_part)
}
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)
}
fn pick_form(n: &BigInt, forms: &[String]) -> Result<String> {
let i = if n.is_one() { 0 } else { 1 };
forms
.get(i)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
pub struct LangCnh {
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_fallback: CurrencyForms,
}
impl Default for LangCnh {
fn default() -> Self {
Self::new()
}
}
impl LangCnh {
pub fn new() -> Self {
let currency_forms: HashMap<&'static str, CurrencyForms> = [
(
"MMK",
CurrencyForms::new(&["kyat", "kyat"], &["pya", "pya"]),
),
(
"USD",
CurrencyForms::new(&["dollar", "dollar"], &["cent", "cent"]),
),
(
"EUR",
CurrencyForms::new(&["euro", "euro"], &["cent", "cent"]),
),
]
.into_iter()
.collect();
let currency_fallback = currency_forms
.get(CURRENCY_FALLBACK_KEY)
.expect("CURRENCY_FALLBACK_KEY is inserted directly above")
.clone();
LangCnh {
currency_forms,
currency_fallback,
}
}
fn split_currency(&self, val: &BigDecimal) -> Result<(BigInt, BigInt)> {
let (int_val, scale) = val.as_bigint_and_exponent();
if scale < 0 {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
val
)));
}
if scale == 0 {
return Ok((int_val, BigInt::zero()));
}
let scale = scale as usize;
let digits = int_val.to_string();
let padded = if digits.len() <= scale {
format!("{}{}", "0".repeat(scale + 1 - digits.len()), digits)
} else {
digits
};
let (int_part, frac_part) = padded.split_at(padded.len() - scale);
let left = BigInt::from_str(int_part).expect("int_part is ASCII digits");
let mut frac: String = frac_part.chars().take(2).collect();
while frac.len() < 2 {
frac.push('0');
}
let right = BigInt::from_str(&frac).expect("frac is two ASCII digits");
Ok((left, right))
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if *number < BigInt::from(10) {
return ONES[idx(number)].to_string();
}
if *number < BigInt::from(100) {
let (t, o) = number.div_mod_floor(&BigInt::from(10));
let mut out = TENS[idx(&t)].to_string();
if !o.is_zero() {
out.push_str(" le ");
out.push_str(ONES[idx(&o)]);
}
return out;
}
if *number < BigInt::from(1000) {
let (h, r) = number.div_mod_floor(&BigInt::from(100));
let mut out = format!("{} {}", ONES[idx(&h)], HUNDRED);
if !r.is_zero() {
out.push_str(" le ");
out.push_str(&self.int_to_word(&r));
}
return out;
}
if *number < BigInt::from(1_000_000) {
let (t, r) = number.div_mod_floor(&BigInt::from(1000));
let mut out = format!("{} {}", self.int_to_word(&t), THOUSAND);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
if *number < BigInt::from(1_000_000_000) {
let (m, r) = number.div_mod_floor(&BigInt::from(1_000_000));
let mut out = format!("{} {}", self.int_to_word(&m), MILLION);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
number.to_string()
}
fn render_float_text(&self, is_negative: bool, s: &str) -> Result<String> {
let body = match s.split_once('.') {
Some((left, right)) => {
let left_int = BigInt::from_str(left).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
left
))
})?;
let mut ret = format!("{} {}", self.int_to_word(&left_int), POINTWORD);
for ch in right.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})?;
ret.push(' ');
ret.push_str(if d == 0 { ZERO_WORD } else { ONES[d as usize] });
}
ret
}
None => {
let n = BigInt::from_str(s).map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
})?;
self.int_to_word(&n)
}
};
let out = if is_negative {
format!("{}{}", NEGWORD, body)
} else {
body
};
Ok(out.trim().to_string())
}
}
impl Lang for LangCnh {
fn default_currency(&self) -> &str {
"MMK"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"decimal"
}
fn exclude_title(&self) -> &[String] {
static EXCL: OnceLock<Vec<String>> = OnceLock::new();
EXCL.get_or_init(|| vec!["minus".to_string(), "decimal".to_string(), "le".to_string()])
.as_slice()
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = self.int_to_word(&value.abs());
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(self.int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", self.to_cardinal(value)?, ORDINAL_SUFFIX))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", value, ORDINAL_SUFFIX))
}
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> {
match value {
FloatValue::Float { value, precision } => {
let is_negative = value.is_sign_negative();
let text = format!("{:.prec$}", value.abs(), prec = *precision as usize);
self.render_float_text(is_negative, &text)
}
FloatValue::Decimal { value, precision } => {
let is_negative = value.is_negative();
let text = decimal_to_plain_string(&value.abs(), *precision);
self.render_float_text(is_negative, &text)
}
}
}
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()),
};
self.render_float_text(is_negative, &text)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!(
"{}{}",
self.cardinal_float_entry(value, None)?,
ORDINAL_SUFFIX
))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}{}", repr_str, ORDINAL_SUFFIX))
}
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 lang_name(&self) -> &str {
LANG_NAME
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
match forms.last() {
None => Ok(String::new()),
Some(last) => Ok(if n.is_one() {
forms[0].clone()
} else {
last.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 is_negative = val.is_negative();
let (left, right) = match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value, .. } => self.split_currency(&value.abs())?,
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.currency_fallback);
let mut result = format!(
"{} {}",
self.int_to_word(&left),
pick_form(&left, &forms.unit)?
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(&pick_form(&right, &forms.subunit)?);
}
if is_negative {
result.insert_str(0, NEGWORD);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use crate::floatpath::FloatValue;
fn f(value: f64, precision: u32) -> String {
LangCnh::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(s: &str, precision: u32) -> String {
LangCnh::new()
.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
},
None,
)
.unwrap()
}
#[test]
fn corpus_floats() {
assert_eq!(f(0.0, 1), "zero decimal zero");
assert_eq!(f(0.5, 1), "zero decimal panga");
assert_eq!(f(1.0, 1), "pakhat decimal zero");
assert_eq!(f(1.5, 1), "pakhat decimal panga");
assert_eq!(f(2.25, 2), "pahnih decimal pahnih panga");
assert_eq!(f(3.14, 2), "pathum decimal pakhat pali");
assert_eq!(f(0.01, 2), "zero decimal zero pakhat");
assert_eq!(f(0.1, 1), "zero decimal pakhat");
assert_eq!(f(0.99, 2), "zero decimal pakua pakua");
assert_eq!(f(1.01, 2), "pakhat decimal zero pakhat");
assert_eq!(f(12.34, 2), "pahra le pahnih decimal pathum pali");
assert_eq!(f(99.99, 2), "pakua kip le pakua decimal pakua pakua");
assert_eq!(f(100.5, 1), "pakhat phazar decimal panga");
assert_eq!(
f(1234.56, 2),
"pakhat thawngkhat pahnih phazar le pathum kip le pali decimal panga paruk"
);
assert_eq!(f(-0.5, 1), "minus zero decimal panga");
assert_eq!(f(-1.5, 1), "minus pakhat decimal panga");
assert_eq!(f(-12.34, 2), "minus pahra le pahnih decimal pathum pali");
assert_eq!(f(1.005, 3), "pakhat decimal zero zero panga");
assert_eq!(f(2.675, 3), "pahnih decimal paruk parih panga");
}
#[test]
fn corpus_decimals() {
assert_eq!(d("0.01", 2), "zero decimal zero pakhat");
assert_eq!(d("1.10", 2), "pakhat decimal pakhat zero");
assert_eq!(d("12.345", 3), "pahra le pahnih decimal pathum pali panga");
assert_eq!(
d("98746251323029.99", 2),
"98746251323029 decimal pakua pakua"
);
assert_eq!(d("0.001", 3), "zero decimal zero zero pakhat");
}
#[test]
fn entry_routing() {
let l = LangCnh::new();
let f5 = FloatValue::Float {
value: 5.0,
precision: 1,
};
assert_eq!(
l.cardinal_float_entry(&f5, None).unwrap(),
"panga decimal zero"
);
assert_eq!(
l.ordinal_float_entry(&f5).unwrap(),
"panga decimal zero-nak"
);
assert_eq!(l.year_float_entry(&f5).unwrap(), "panga decimal zero");
assert_eq!(l.ordinal_num_float_entry(&f5, "5.0").unwrap(), "5.0-nak");
assert_eq!(
l.ordinal_float_entry(&FloatValue::Float {
value: -0.0,
precision: 1
})
.unwrap(),
"minus zero decimal zero-nak"
);
let d5 = FloatValue::Decimal {
value: BigDecimal::from_str("5").unwrap(),
precision: 0,
};
assert_eq!(l.cardinal_float_entry(&d5, None).unwrap(), "panga");
assert_eq!(l.ordinal_float_entry(&d5).unwrap(), "panga-nak");
let e16 = FloatValue::Float {
value: 1e16,
precision: 16,
};
match l.cardinal_float_entry(&e16, None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1e+16'")
}
other => panic!("expected ValueError, got {:?}", other),
}
assert!(matches!(
l.year_float_entry(&FloatValue::Float {
value: 1e20,
precision: 20
}),
Err(N2WError::Value(_))
));
let e2 = FloatValue::Decimal {
value: BigDecimal::from_str("1E+2").unwrap(),
precision: 2,
};
match l.cardinal_float_entry(&e2, None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1E+2'")
}
other => panic!("expected ValueError, got {:?}", other),
}
assert_eq!(
l.ordinal_num_float_entry(&e16, "1e+16").unwrap(),
"1e+16-nak"
);
assert!(matches!(
l.str_to_number("Infinity"),
Err(N2WError::Value(_))
));
assert!(matches!(
l.str_to_number("-Infinity"),
Err(N2WError::Value(_))
));
assert!(matches!(l.str_to_number("1.5"), Ok(ParsedNumber::Dec(_))));
}
#[test]
fn live_edges() {
let l = LangCnh::new();
for p in [Some(1u32), Some(5), None] {
assert_eq!(
l.to_cardinal_float(
&FloatValue::Float {
value: 2.675,
precision: 3
},
p
)
.unwrap(),
"pahnih decimal paruk parih panga"
);
}
assert_eq!(f(-0.0, 1), "minus zero decimal zero");
assert_eq!(f(1_000_000.25, 2), "pakhat milin decimal pahnih panga");
assert_eq!(d("1.100", 3), "pakhat decimal pakhat zero zero");
assert_eq!(d("5.00", 2), "panga decimal zero zero");
assert_eq!(d("10.0", 1), "pahra decimal zero");
assert_eq!(d("-0.5", 1), "minus zero decimal panga");
assert_eq!(d("-12.34", 2), "minus pahra le pahnih decimal pathum pali");
}
}