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::{Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const NEGWORD: &str = "ঋণাত্মক ";
const POINTWORD: &str = "দশমিক";
const ZERO_WORD: &str = "শূন্য";
const ONES: [&str; 10] = ["", "এক", "দুই", "তিনি", "চাৰি", "পাঁচ", "ছয়", "সাত", "আঠ", "নয়"];
const TENS: [&str; 10] = [
"",
"",
"বিশ",
"ত্ৰিশ",
"চল্লিশ",
"পঞ্চাশ",
"ষাঠি",
"সত্তৰ",
"আশী",
"নব্বই",
];
const TEENS: [&str; 10] = [
"দহ",
"এঘাৰ",
"বাৰ",
"তেৰ",
"চৈধ্য",
"পোন্ধৰ",
"ষোল্ল",
"সোতৰ",
"ওঠৰ",
"উনিশ",
];
const HUNDRED: &str = " শ";
const THOUSAND: &str = "হাজাৰ";
const LAKH: &str = "লাখ";
const CRORE: &str = "কোটি";
const ORDINAL_SUFFIX: &str = " -তম";
const ORDINAL_NUM_SUFFIX: &str = "-তম";
const YEAR_PREFIX: &str = "চন ";
const SEPARATOR_DEFAULT: &str = " আৰু";
const SEPARATOR_UNSET: &str = ",";
fn split_currency(n: &BigDecimal) -> Result<(BigInt, BigInt)> {
let (digits, scale) = n.as_bigint_and_exponent();
if scale < 0 {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
n
)));
}
if scale == 0 {
return Ok((digits, BigInt::zero()));
}
let s = digits.abs().to_string();
let scale = scale as usize;
let (int_part, frac_part) = if s.len() > scale {
let (a, b) = s.split_at(s.len() - scale);
(a.to_string(), b.to_string())
} else {
("0".to_string(), format!("{:0>width$}", s, width = scale))
};
let left = int_part
.parse::<BigInt>()
.unwrap_or_else(|_| BigInt::zero());
let head: String = frac_part.chars().take(2).collect();
let right = format!("{:0<2}", head)
.parse::<BigInt>()
.unwrap_or_else(|_| BigInt::zero());
Ok((left, right))
}
fn shortest_digits(a: f64) -> (String, i32) {
let sci = format!("{:e}", a);
let (mant, exp) = sci
.split_once('e')
.expect("{:e} on a finite f64 always emits an exponent");
let mut digits: Vec<u8> = mant.bytes().filter(|c| *c != b'.').collect();
let mut decpt: i32 = exp.parse::<i32>().expect("{:e} exponent is an integer") + 1;
let bits = a.to_bits();
let biased = ((bits >> 52) & 0x7ff) as i32;
let frac = bits & ((1u64 << 52) - 1);
let (mut m, mut e) = if biased == 0 {
(frac, -1074i32) } else {
(frac | (1u64 << 52), biased - 1075)
};
if m == 0 {
return (String::from_utf8(digits).expect("ASCII digits"), decpt);
}
let z = m.trailing_zeros() as i32;
m >>= z;
e += z;
let q = digits.len() as i32 - decpt;
let mut tie = e + q + 1 == 0;
if tie && q < 0 {
let r = -q as u32;
tie = r <= 22 && m % 5u64.pow(r) == 0;
}
if !tie {
return (String::from_utf8(digits).expect("ASCII digits"), decpt);
}
let last = digits[digits.len() - 1] - b'0';
if last % 2 == 1 {
if m % 4 == 1 {
*digits.last_mut().expect("non-empty") -= 1;
} else {
let mut i = digits.len();
loop {
if i == 0 {
digits.insert(0, b'1');
decpt += 1;
break;
}
i -= 1;
if digits[i] == b'9' {
digits[i] = b'0';
} else {
digits[i] += 1;
break;
}
}
}
while digits.len() > 1 && *digits.last().expect("non-empty") == b'0' {
digits.pop();
}
}
(String::from_utf8(digits).expect("ASCII digits"), decpt)
}
fn py_float_repr(value: f64) -> String {
if value.is_nan() {
return "nan".to_string();
}
if value.is_infinite() {
return if value > 0.0 { "inf" } else { "-inf" }.to_string();
}
let sign = if value.is_sign_negative() { "-" } else { "" };
let (digits, decpt) = shortest_digits(value.abs());
let ndigits = digits.len() as i32;
if decpt <= -4 || decpt > 16 {
let exp = decpt - 1;
let mut mant = String::from(&digits[..1]);
if digits.len() > 1 {
mant.push('.');
mant.push_str(&digits[1..]);
}
format!(
"{}{}e{}{:02}",
sign,
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
)
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat(-decpt as usize), digits)
} else if decpt >= ndigits {
format!(
"{}{}{}.0",
sign,
digits,
"0".repeat((decpt - ndigits) as usize)
)
} else {
let d = decpt as usize;
format!("{}{}.{}", sign, &digits[..d], &digits[d..])
}
}
fn py_decimal_str(value: &BigDecimal) -> String {
let (int_val, scale) = value.as_bigint_and_exponent();
let exp = -(scale as i128);
let sign = if int_val.is_negative() { "-" } else { "" };
let int_digits = int_val.abs().to_string(); let len = int_digits.len() as i128;
let leftdigits = exp + len;
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 >= len {
(
format!("{}{}", int_digits, "0".repeat((dotplace - len) as usize)),
String::new(),
)
} else {
let d = dotplace as usize;
(
int_digits[..d].to_string(),
format!(".{}", &int_digits[d..]),
)
};
let expstr = if leftdigits == dotplace {
String::new()
} else {
let d = leftdigits - dotplace;
format!("E{}{}", if d < 0 { '-' } else { '+' }, d.abs())
};
format!("{}{}{}{}", sign, intpart, fracpart, expstr)
}
fn py_int(s: &str) -> Result<BigInt> {
let err = || N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s));
let t = s.trim();
let (negative, body) = match t.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, t.strip_prefix('+').unwrap_or(t)),
};
if body.is_empty()
|| body.starts_with('_')
|| body.ends_with('_')
|| body.contains("__")
|| !body.chars().all(|c| c.is_ascii_digit() || c == '_')
{
return Err(err());
}
let digits: String = body.chars().filter(|c| *c != '_').collect();
let n: BigInt = digits.parse().map_err(|_| err())?;
Ok(if negative { -n } else { n })
}
fn cardinal_from_str(number: &str) -> Result<String> {
let n = number.trim();
let (n, mut ret) = match n.strip_prefix('-') {
Some(rest) => (rest, NEGWORD.to_string()),
None => (n, String::new()),
};
let Some(dot) = n.find('.') else {
ret.push_str(&int_to_word(&py_int(n)?));
return Ok(ret);
};
let (left, right) = (&n[..dot], &n[dot + 1..]);
ret.push_str(&int_to_word(&py_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
let mut first = true;
for d in right.chars() {
if !first {
ret.push(' ');
}
first = false;
let mut buf = [0u8; 4];
ret.push_str(&int_to_word(&py_int(d.encode_utf8(&mut buf))?));
}
Ok(ret)
}
fn small(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 < BigInt::from(10) {
return ONES[small(number)].to_string();
}
if *number < BigInt::from(20) {
return TEENS[small(number) - 10].to_string();
}
if *number < BigInt::from(100) {
let n = small(number);
let mut out = TENS[n / 10].to_string();
if n % 10 != 0 {
out.push(' ');
out.push_str(ONES[n % 10]);
}
return out;
}
if *number < BigInt::from(1000) {
let n = small(number);
let mut out = ONES[n / 100].to_string();
out.push_str(HUNDRED);
if n % 100 != 0 {
out.push_str(&int_to_word(&BigInt::from(n % 100)));
}
return out;
}
let (divisor, word) = if *number < BigInt::from(100_000) {
(BigInt::from(1_000), THOUSAND)
} else if *number < BigInt::from(10_000_000) {
(BigInt::from(100_000), LAKH)
} else {
(BigInt::from(10_000_000), CRORE)
};
let (quotient, remainder) = number.div_mod_floor(&divisor);
let mut out = int_to_word("ient);
out.push(' ');
out.push_str(word);
if !remainder.is_zero() {
out.push(' ');
out.push_str(&int_to_word(&remainder));
}
out
}
pub struct LangAs {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangAs {
fn default() -> Self {
Self::new()
}
}
impl LangAs {
pub fn new() -> Self {
let currency_forms: HashMap<&'static str, CurrencyForms> = [
("INR", CurrencyForms::new(&["ৰুপী", "ৰুপী"], &["পইচা", "পইচা"])),
(
"USD",
CurrencyForms::new(&["ডলাৰ", "ডলাৰ"], &["চেণ্ট", "চেণ্ট"]),
),
(
"EUR",
CurrencyForms::new(&["ইউৰো", "ইউৰো"], &["চেণ্ট", "চেণ্ট"]),
),
]
.into_iter()
.collect();
LangAs { currency_forms }
}
}
impl Lang for LangAs {
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!(
"{}{}",
self.to_cardinal_float(value, None)?,
ORDINAL_SUFFIX
))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}{}", repr_str, ORDINAL_NUM_SUFFIX))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!(
"{}{}",
YEAR_PREFIX,
self.to_cardinal_float(value, None)?
))
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
let parsed = python_decimal_parse(s)?;
Ok(parsed)
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
match to {
"ordinal_num" => {
let token = if negative { "-Infinity" } else { "Infinity" };
Ok(format!("{}{}", token, ORDINAL_NUM_SUFFIX))
}
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".to_string(),
)),
}
}
fn nan_result(&self, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok(format!("NaN{}", ORDINAL_NUM_SUFFIX)),
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".to_string(),
)),
}
}
fn default_currency(&self) -> &str {
"INR"
}
fn default_separator(&self) -> &str {
" আৰু"
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"দশমিক"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (prefix, magnitude) = if value.is_negative() {
(NEGWORD, -value)
} else {
("", value.clone())
};
Ok(format!("{}{}", prefix, int_to_word(&magnitude)))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
let _ = precision_override;
let n = match value {
FloatValue::Float { value, .. } => py_float_repr(*value),
FloatValue::Decimal { value, .. } => py_decimal_str(value),
};
cardinal_from_str(&n)
}
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_NUM_SUFFIX))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", YEAR_PREFIX, self.to_cardinal(value)?))
}
fn lang_name(&self) -> &str {
"Num2Word_AS"
}
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 == SEPARATOR_UNSET {
SEPARATOR_DEFAULT
} else {
separator
};
let is_negative = val.is_negative();
let (left, right) = match val {
CurrencyValue::Int(v) => (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => split_currency(&d.abs())?,
};
let forms = match self.currency_forms.get(currency) {
Some(f) => f,
None => self
.currency_forms
.get("INR")
.expect("CURRENCY_FORMS[\"INR\"] is populated by LangAs::new"),
};
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let one = BigInt::from(1);
let left_str = int_to_word(&left);
let cents_str = if cents && !right.is_zero() {
int_to_word(&right)
} else {
String::new()
};
let mut result = format!(
"{} {}",
left_str,
if left != one { &cr1[1] } else { &cr1[0] }
);
if !cents_str.is_empty() {
result.push_str(separator);
result.push_str(¢s_str);
result.push(' ');
result.push_str(if right != one { &cr2[1] } else { &cr2[0] });
}
Ok(if is_negative {
format!("{}{}", NEGWORD, result)
} else {
result
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn flt(v: f64) -> String {
let precision = py_float_repr(v)
.split_once('.')
.map(|(_, f)| f.len() as u32)
.unwrap_or(0);
LangAs::new()
.to_cardinal_float(
&FloatValue::Float {
value: v,
precision,
},
None,
)
.unwrap()
}
fn dec(s: &str) -> String {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.unsigned_abs() as u32;
LangAs::new()
.to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
.unwrap()
}
#[test]
fn corpus_float_rows() {
assert_eq!(flt(0.0), "শূন্য দশমিক শূন্য");
assert_eq!(flt(0.5), "শূন্য দশমিক পাঁচ");
assert_eq!(flt(1.0), "এক দশমিক শূন্য");
assert_eq!(flt(1.5), "এক দশমিক পাঁচ");
assert_eq!(flt(2.25), "দুই দশমিক দুই পাঁচ");
assert_eq!(flt(3.14), "তিনি দশমিক এক চাৰি");
assert_eq!(flt(0.01), "শূন্য দশমিক শূন্য এক");
assert_eq!(flt(0.1), "শূন্য দশমিক এক");
assert_eq!(flt(0.99), "শূন্য দশমিক নয় নয়");
assert_eq!(flt(1.01), "এক দশমিক শূন্য এক");
assert_eq!(flt(12.34), "বাৰ দশমিক তিনি চাৰি");
assert_eq!(flt(99.99), "নব্বই নয় দশমিক নয় নয়");
assert_eq!(flt(100.5), "এক শ দশমিক পাঁচ");
assert_eq!(flt(1234.56), "এক হাজাৰ দুই শত্ৰিশ চাৰি দশমিক পাঁচ ছয়");
assert_eq!(flt(-0.5), "ঋণাত্মক শূন্য দশমিক পাঁচ");
assert_eq!(flt(-1.5), "ঋণাত্মক এক দশমিক পাঁচ");
assert_eq!(flt(-12.34), "ঋণাত্মক বাৰ দশমিক তিনি চাৰি");
assert_eq!(flt(1.005), "এক দশমিক শূন্য শূন্য পাঁচ");
assert_eq!(flt(2.675), "দুই দশমিক ছয় সাত পাঁচ");
}
#[test]
fn corpus_decimal_rows() {
assert_eq!(dec("0.01"), "শূন্য দশমিক শূন্য এক");
assert_eq!(dec("1.10"), "এক দশমিক এক শূন্য");
assert_eq!(dec("12.345"), "বাৰ দশমিক তিনি চাৰি পাঁচ");
assert_eq!(
dec("98746251323029.99"),
"নব্বই আঠ লাখ সত্তৰ চাৰি হাজাৰ ছয় শবিশ পাঁচ কোটি তেৰ লাখ \
বিশ তিনি হাজাৰ বিশ নয় দশমিক নয় নয়"
);
assert_eq!(dec("0.001"), "শূন্য দশমিক শূন্য শূন্য এক");
}
#[test]
fn negative_zero_keeps_negword() {
assert_eq!(flt(-0.0), "ঋণাত্মক শূন্য দশমিক শূন্য");
assert_eq!(flt(0.0), "শূন্য দশমিক শূন্য");
}
#[test]
fn precision_override_is_ignored() {
let l = LangAs::new();
let v = FloatValue::Float {
value: 1.23456,
precision: 5,
};
let want = "এক দশমিক দুই তিনি চাৰি পাঁচ ছয়";
assert_eq!(l.to_cardinal_float(&v, None).unwrap(), want);
for p in [0u32, 1, 2, 5, 9] {
assert_eq!(l.to_cardinal_float(&v, Some(p)).unwrap(), want);
}
}
#[test]
fn exponent_notation_raises_value_error() {
let l = LangAs::new();
let f = |v: f64| {
l.to_cardinal_float(
&FloatValue::Float {
value: v,
precision: 0,
},
None,
)
.unwrap_err()
};
assert!(matches!(f(1e16), N2WError::Value(m)
if m == "invalid literal for int() with base 10: '1e+16'"));
assert!(matches!(f(1e21), N2WError::Value(m)
if m == "invalid literal for int() with base 10: '1e+21'"));
assert!(matches!(f(1e-5), N2WError::Value(m)
if m == "invalid literal for int() with base 10: '1e-05'"));
assert!(matches!(f(1.5e16), N2WError::Value(m)
if m == "invalid literal for int() with base 10: 'e'"));
assert!(matches!(f(f64::INFINITY), N2WError::Value(m)
if m == "invalid literal for int() with base 10: 'inf'"));
assert!(matches!(f(f64::NAN), N2WError::Value(m)
if m == "invalid literal for int() with base 10: 'nan'"));
let d = |s: &str| {
l.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
},
None,
)
.unwrap_err()
};
assert!(matches!(d("1E+16"), N2WError::Value(m)
if m == "invalid literal for int() with base 10: '1E+16'"));
assert!(matches!(d("1E-7"), N2WError::Value(m)
if m == "invalid literal for int() with base 10: '1E-7'"));
}
#[test]
fn large_and_small_boundaries() {
assert_eq!(flt(1e15), "দহ কোটি কোটি দশমিক শূন্য");
assert_eq!(flt(0.0001), "শূন্য দশমিক শূন্য শূন্য শূন্য এক");
assert_eq!(dec("0.000001"), "শূন্য দশমিক শূন্য শূন্য শূন্য শূন্য শূন্য এক");
assert_eq!(dec("1.00E+2"), "এক শ");
assert_eq!(dec("0.00"), "শূন্য দশমিক শূন্য শূন্য");
assert_eq!(dec("-0.5"), "ঋণাত্মক শূন্য দশমিক পাঁচ");
assert_eq!(dec("-12.34"), "ঋণাত্মক বাৰ দশমিক তিনি চাৰি");
}
#[test]
fn py_float_repr_matches_cpython() {
for (v, want) in [
(0.0, "0.0"),
(-0.0, "-0.0"),
(0.5, "0.5"),
(1.0, "1.0"),
(0.01, "0.01"),
(1.005, "1.005"),
(2.675, "2.675"),
(100.5, "100.5"),
(1e15, "1000000000000000.0"),
(1e16, "1e+16"),
(1.5e16, "1.5e+16"),
(1e21, "1e+21"),
(1e100, "1e+100"),
(0.0001, "0.0001"),
(1e-5, "1e-05"),
(5e-324, "5e-324"),
(1.7976931348623157e308, "1.7976931348623157e+308"),
(98746251323029.99, "98746251323029.98"),
(-78198386800398.125, "-78198386800398.12"),
(-1267860061485775.25, "-1267860061485775.2"),
] {
assert_eq!(py_float_repr(v), want, "repr({:?})", v);
}
}
#[test]
fn py_decimal_str_matches_cpython() {
for (s, want) in [
("1.10", "1.10"),
("0.00", "0.00"),
("-0.5", "-0.5"),
("1E+16", "1E+16"),
("1E-7", "1E-7"),
("0.0000001", "1E-7"),
("0.000001", "0.000001"),
("1.00E+2", "100"),
("12.345", "12.345"),
("98746251323029.99", "98746251323029.99"),
("0.01", "0.01"),
] {
assert_eq!(
py_decimal_str(&BigDecimal::from_str(s).unwrap()),
want,
"{}",
s
);
}
}
}