use crate::base::{Lang, N2WError, Result};
use crate::currency::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, Zero};
use std::str::FromStr;
use std::sync::OnceLock;
const RANKING: &[&str] = &[
"", "প্রথম", "দ্বিতীয়", "তৃতীয়", "চতুর্থ", "পঞ্চম", "ষষ্ঠ", "সপ্তম", "অষ্টম", "নবম", "দশম", ];
const AKOK: &[&str] = &[
"", "এক", "দুই", "তিন", "চার", "পাঁচ", "ছয়", "সাত", "আট", "নয়", ];
const DOSOK: &[&str] = &[
"দশ", "এগারো", "বারো", "তেরো", "চৌদ্দ", "পনের", "ষোল", "সতের", "আঠারো", "উনিশ", "বিশ", "একুশ", "বাইশ", "তেইশ", "চব্বিশ", "পঁচিশ", "ছাব্বিশ", "সাতাশ", "আটাশ", "উনত্রিশ", "ত্রিশ", "একত্রিশ", "বত্রিশ", "তেত্রিশ", "চৌত্রিশ", "পঁইত্রিশ", "ছত্রিশ", "সাতত্রিশ", "আটত্রিশ", "উনচল্লিশ", "চল্লিশ", "একচল্লিশ", "বিয়াল্লিশ", "তেতাল্লিশ", "চৌচল্লিশ", "পঁয়তাল্লিশ", "ছেচল্লিশ", "সাতচল্লিশ", "আটচল্লিশ", "উনপঞ্চাশ", "পঞ্চাশ", "একান্ন", "বাহান্ন", "তিপ্পান্ন", "চুয়ান্ন", "পঞ্চান্ন", "ছাপ্পান্ন", "সাতান্ন", "আটান্ন", "উনষাট", "ষাট", "একষট্টি", "বাষট্টি", "তেষট্টি", "চৌষট্টি", "পঁয়ষট্টি", "ছিষট্টি", "সাতষট্টি", "আটষট্টি", "উনসত্তর", "সত্তর", "একাত্তর ", "বাহাত্তর", "তিয়াত্তর", "চুয়াত্তর", "পঁচাত্তর", "ছিয়াত্তর", "সাতাত্তর", "আটাত্তর", "উনআশি", "আশি", "একাশি", "বিরাশি", "তিরাশি", "চুরাশি", "পঁচাশি", "ছিয়াশি", "সাতাশি", "আটাশি", "উননব্বই", "নব্বই", "একানব্বই", "বিরানব্বই", "তিরানব্বই", "চুরানব্বই", "পঁচানব্বই", "ছিয়ানব্বই", "সাতানব্বই", "আটানব্বই", "নিরানব্বই", ];
const HAZAR: &str = " হাজার ";
const LAKH: &str = " লাখ ";
const KOTI: &str = " কোটি ";
const SHATA: &str = "শত ";
const ZERO_WORD: &str = "শূন্য";
const NEGWORD: &str = "ঋণাত্মক";
const SHAL: &str = " সাল";
const DOSHOMIK: &str = " দশমিক";
const TAKA: &str = "টাকা";
const PAISA: &str = "পয়সা";
fn max_number() -> BigInt {
BigInt::from(10u8).pow(307) - BigInt::one()
}
const DECIMAL_PREC: u64 = 28;
fn number_too_large() -> N2WError {
N2WError::Custom {
module: "num2words2.lang_BN",
class: "NumberTooLargeError",
msg: format!("Too Large number maximum value={}", max_number()),
}
}
pub fn round_to_28_significant(n: &BigInt) -> BigInt {
if n.is_zero() {
return BigInt::zero();
}
let digits = n.to_string().len() as u64;
if digits <= DECIMAL_PREC {
return n.clone();
}
let drop = digits - DECIMAL_PREC;
let divisor = BigInt::from(10u8).pow(drop as u32);
let (q, r) = n.div_rem(&divisor);
let half = &divisor / 2u8;
let rounded = match r.cmp(&half) {
std::cmp::Ordering::Greater => q + BigInt::one(),
std::cmp::Ordering::Less => q,
std::cmp::Ordering::Equal => {
if q.is_odd() {
q + BigInt::one()
} else {
q
}
}
};
rounded * divisor
}
fn max_number_ref() -> &'static BigInt {
static MAX: OnceLock<BigInt> = OnceLock::new();
MAX.get_or_init(max_number)
}
fn max_number_decimal() -> &'static BigDecimal {
static MAX: OnceLock<BigDecimal> = OnceLock::new();
MAX.get_or_init(|| BigDecimal::from(max_number_ref().clone()))
}
fn decimal_prec_limit() -> &'static BigDecimal {
static LIMIT: OnceLock<BigDecimal> = OnceLock::new();
LIMIT.get_or_init(|| BigDecimal::from(BigInt::from(10u8).pow(DECIMAL_PREC as u32)))
}
fn number_too_large_currency() -> N2WError {
N2WError::Custom {
module: "num2words2.lang_BN",
class: "NumberTooLargeError",
msg: format!("Number is too large. Max: {}", max_number_ref()),
}
}
fn invalid_operation() -> N2WError {
N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.DivisionImpossible'>]".to_string(),
}
}
fn fix_prec28_decimal(v: &BigDecimal) -> BigDecimal {
let (coeff, scale) = v.as_bigint_and_exponent();
if coeff.is_zero() {
return v.clone();
}
let digits = coeff.magnitude().to_string().len() as u64;
if digits <= DECIMAL_PREC {
return v.clone();
}
let drop = digits - DECIMAL_PREC;
let divisor = BigInt::from(10u8).pow(drop as u32);
let (q, r) = coeff.div_rem(&divisor);
let half = &divisor / 2u8;
let mut rounded = match r.cmp(&half) {
std::cmp::Ordering::Greater => q + BigInt::one(),
std::cmp::Ordering::Less => q,
std::cmp::Ordering::Equal => {
if q.is_odd() {
q + BigInt::one()
} else {
q
}
}
};
let mut new_scale = scale - drop as i64;
if rounded.magnitude().to_string().len() as u64 > DECIMAL_PREC {
rounded /= BigInt::from(10u8);
new_scale -= 1;
}
BigDecimal::new(rounded, new_scale)
}
fn frac_after_dot(number: &BigDecimal) -> Option<String> {
let (coeff, scale) = number.as_bigint_and_exponent();
if scale <= 0 {
return None;
}
let modulus = BigInt::from(10u8).pow(scale as u32);
let f = coeff.abs().mod_floor(&modulus);
let digits = f.to_string(); let exp = -scale;
let adjusted = exp + digits.chars().count() as i64 - 1;
if adjusted >= -6 {
Some(format!("{:0>width$}", digits, width = scale as usize))
} else if digits.chars().count() == 1 {
None
} else {
let rest: String = digits.chars().skip(1).collect();
Some(format!("{}E{}", rest, adjusted))
}
}
fn parse_paisa(number: &BigDecimal) -> Result<(BigInt, u32)> {
let int_part = number.with_scale(0).as_bigint_and_exponent().0;
let paisa = match frac_after_dot(number) {
None => 0u32,
Some(s) => {
let f = BigInt::from_str(&s).map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
})?;
let scaled = f * BigInt::from(100u8);
let two: String = scaled.to_string().chars().take(2).collect();
two.parse::<u32>()
.expect("digits of a non-negative BigInt, at most 2 of them")
}
};
Ok((int_part, paisa))
}
fn parse_number(number: &BigDecimal) -> Result<(BigInt, BigInt)> {
let int_part = number.with_scale(0).as_bigint_and_exponent().0;
let decimal_part = match frac_after_dot(number) {
None => BigInt::zero(),
Some(s) => BigInt::from_str(&s).map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
})?,
};
Ok((int_part, decimal_part))
}
fn float_str_to_number(value: f64) -> Result<BigDecimal> {
if value.is_nan() {
return Err(N2WError::Value("cannot convert NaN to integer".to_string()));
}
if value.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert Infinity to integer".to_string(),
));
}
BigDecimal::from_str(&format!("{}", value)).map_err(|e| N2WError::Value(e.to_string()))
}
fn fv_trunc_abs(value: &FloatValue) -> Result<BigInt> {
let d = match value {
FloatValue::Float { value, .. } => float_str_to_number(*value)?,
FloatValue::Decimal { value, .. } => value.clone(),
};
Ok(d.abs().with_scale(0).as_bigint_and_exponent().0)
}
pub struct LangBn;
impl LangBn {
fn check_max_float(&self, value: &FloatValue) -> Result<()> {
let raw = match value {
FloatValue::Float { value, .. } => float_str_to_number(*value)?,
FloatValue::Decimal { value, .. } => value.clone(),
};
if raw > *max_number_decimal() {
return Err(number_too_large());
}
Ok(())
}
pub fn new() -> Self {
LangBn
}
fn check_max(&self, number: &BigInt) -> Result<()> {
if max_number() >= *number {
Ok(())
} else {
Err(number_too_large())
}
}
fn number_to_bengali_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
let mut number = number.clone();
let mut words = String::new();
let koti = BigInt::from(10u8).pow(7);
if number >= koti {
words.push_str(&self.number_to_bengali_word(&(&number / &koti)));
words.push_str(KOTI);
number %= &koti;
}
let lakh = BigInt::from(10u8).pow(5);
if number >= lakh {
words.push_str(&self.number_to_bengali_word(&(&number / &lakh)));
words.push_str(LAKH);
number %= &lakh;
}
let thousand = BigInt::from(1000u16);
if number >= thousand {
words.push_str(&self.number_to_bengali_word(&(&number / &thousand)));
words.push_str(HAZAR);
number %= &thousand;
}
let hundred = BigInt::from(100u8);
if number >= hundred {
let idx = usize::try_from(&number / &hundred).expect("number/100 is 1..=9 here");
words.push_str(AKOK[idx]);
words.push_str(SHATA);
number %= &hundred;
}
if number >= BigInt::from(10u8) && number <= BigInt::from(99u8) {
let idx = usize::try_from(&number - BigInt::from(10u8)).expect("0..=89 here");
words.push_str(DOSOK[idx]);
words.push(' ');
number = BigInt::zero();
}
if number > BigInt::zero() && number < BigInt::from(10u8) {
let idx = usize::try_from(&number).expect("1..=9 here");
words.push_str(AKOK[idx]);
words.push(' ');
}
words.trim().to_string()
}
fn dosomik_to_bengali_word(&self, number: &BigInt) -> String {
let mut word = String::new();
for ch in number.to_string().chars() {
let d = ch
.to_digit(10)
.expect("decimal_part is a non-negative BigInt, so all digits");
word.push(' ');
word.push_str(AKOK[d as usize]);
}
word
}
fn cardinal_inner(&self, value: &BigInt, is_negative: bool) -> Result<String> {
let number = round_to_28_significant(&value.abs());
self.check_max(&number)?;
let words = self.number_to_bengali_word(&number);
if is_negative {
Ok(format!("{} {}", NEGWORD, words))
} else {
Ok(words)
}
}
}
impl Default for LangBn {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangBn {
fn default_currency(&self) -> &str {
"BDT"
}
fn default_separator(&self) -> &str {
","
}
fn negword(&self) -> &str {
NEGWORD
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
self.cardinal_inner(value, value.is_negative())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.check_max(value)?;
if *value >= BigInt::one() && *value <= BigInt::from(10u8) {
let idx = usize::try_from(value).expect("1..=10 here");
return Ok(RANKING[idx].to_string());
}
let rank = self.cardinal_inner(&value.abs(), false)?;
if rank.ends_with('ত') {
Ok(format!("{}ম", rank))
} else {
Ok(format!("{}তম", rank))
}
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.check_max(value)?;
let words = self.cardinal_inner(&value.abs(), false)?;
Ok(format!("{}{}", words, SHAL))
}
fn ordinal_num_float_entry(&self, value: &FloatValue, _repr_str: &str) -> Result<String> {
self.check_max_float(value)?;
if let Some(i) = value.as_whole_int() {
if i >= BigInt::one() && i <= BigInt::from(10u8) {
return Err(N2WError::Type(match value {
FloatValue::Float { .. } => {
"list indices must be integers or slices, not float".to_string()
}
FloatValue::Decimal { .. } => {
"list indices must be integers or slices, not decimal.Decimal"
.to_string()
}
}));
}
}
let rank = self.cardinal_inner(&fv_trunc_abs(value)?, false)?;
if rank.ends_with('ত') {
Ok(format!("{}ম", rank))
} else {
Ok(format!("{}তম", rank))
}
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.check_max_float(value)?;
let words = self.cardinal_inner(&fv_trunc_abs(value)?, false)?;
Ok(format!("{}{}", words, SHAL))
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Dec(d) => Ok(ParsedNumber::Dec(d.abs())),
ParsedNumber::Inf { .. } => Ok(ParsedNumber::Inf { negative: false }),
ParsedNumber::NaN => Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.InvalidOperation'>]".to_string(),
}),
other => Ok(other),
}
}
fn to_fraction(&self, _numerator: &BigInt, _denominator: &BigInt) -> Result<String> {
Err(N2WError::Attribute(
"'Num2Word_BN' object has no attribute 'to_fraction'".to_string(),
))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let is_negative = match value {
FloatValue::Float { value, .. } => *value < 0.0,
FloatValue::Decimal { value, .. } => *value < BigDecimal::zero(),
};
let number = match value {
FloatValue::Float { value, .. } => float_str_to_number(*value)?,
FloatValue::Decimal { value, .. } => value.clone(),
};
let number = fix_prec28_decimal(&number.abs());
let (number, decimal_part) = parse_number(&number)?;
self.check_max(&number)?;
let dosomik_word = if decimal_part > BigInt::zero() {
Some(format!(
"{}{}",
DOSHOMIK,
self.dosomik_to_bengali_word(&decimal_part)
))
} else {
None
};
let words = self.number_to_bengali_word(&number);
let result = match dosomik_word {
Some(d) => format!("{}{}", words, d).trim().to_string(),
None => words.trim().to_string(),
};
if is_negative {
Ok(format!("{} {}", NEGWORD, result))
} else {
Ok(result)
}
}
fn lang_name(&self) -> &str {
"Num2Word_BN"
}
fn to_currency(
&self,
val: &CurrencyValue,
_currency: &str,
_cents: bool,
_separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let too_large = match val {
CurrencyValue::Int(i) => &i.abs() >= max_number_ref(),
CurrencyValue::Decimal { value, .. } => &value.abs() >= max_number_decimal(),
};
if too_large {
return Err(number_too_large_currency());
}
let decimal_val: BigDecimal = match val {
CurrencyValue::Int(i) => BigDecimal::from(i.clone()),
CurrencyValue::Decimal { value, .. } => value.clone(),
};
let scaled = decimal_val.abs() * BigDecimal::from(100);
if scaled >= *decimal_prec_limit() {
return Err(invalid_operation());
}
if let CurrencyValue::Int(i) = val {
let number = round_to_28_significant(&i.abs());
self.check_max(&number)?;
return Ok(format!("{} {}", self.number_to_bengali_word(&number), TAKA));
}
let value = match val {
CurrencyValue::Decimal { value, .. } => value,
CurrencyValue::Int(_) => unreachable!("the int arm returned above"),
};
let number = fix_prec28_decimal(&value.abs());
let (number, decimal_part) = parse_paisa(&number)?;
self.check_max(&number)?;
let dosomik_word = if decimal_part == 0 {
format!(" {} {}", ZERO_WORD, PAISA)
} else {
format!(
" {} {}",
self.number_to_bengali_word(&BigInt::from(decimal_part)),
PAISA
)
};
let words = format!("{} {}", self.number_to_bengali_word(&number), TAKA);
Ok(format!("{}{}", words, dosomik_word))
}
fn to_cheque(&self, _val: &BigDecimal, _currency: &str) -> Result<String> {
Err(N2WError::Attribute(
"'Num2Word_BN' object has no attribute 'to_cheque'".to_string(),
))
}
}
#[cfg(test)]
mod float_tests {
use super::*;
fn f(v: f64) -> String {
let s = format!("{}", v);
let precision = match s.split_once('.') {
Some((_, frac)) => frac.len() as u32,
None => 0,
};
LangBn::new()
.to_cardinal_float(&FloatValue::Float { value: v, precision }, None)
.unwrap()
}
fn f_err(v: f64) -> N2WError {
LangBn::new()
.to_cardinal_float(
&FloatValue::Float {
value: v,
precision: 0,
},
None,
)
.unwrap_err()
}
fn d(s: &str) -> String {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.max(0) as u32;
LangBn::new()
.to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
.unwrap()
}
#[test]
fn corpus_cardinal_float() {
assert_eq!(f(0.0), "শূন্য");
assert_eq!(f(0.5), "শূন্য দশমিক পাঁচ");
assert_eq!(f(1.0), "এক");
assert_eq!(f(1.5), "এক দশমিক পাঁচ");
assert_eq!(f(2.25), "দুই দশমিক দুই পাঁচ");
assert_eq!(f(3.14), "তিন দশমিক এক চার");
assert_eq!(f(0.01), "শূন্য দশমিক এক");
assert_eq!(f(0.1), "শূন্য দশমিক এক");
assert_eq!(f(0.99), "শূন্য দশমিক নয় নয়");
assert_eq!(f(1.01), "এক দশমিক এক");
assert_eq!(f(12.34), "বারো দশমিক তিন চার");
assert_eq!(f(99.99), "নিরানব্বই দশমিক নয় নয়");
assert_eq!(f(100.5), "একশত দশমিক পাঁচ");
assert_eq!(f(1234.56), "এক হাজার দুইশত চৌত্রিশ দশমিক পাঁচ ছয়");
assert_eq!(f(-0.5), "ঋণাত্মক শূন্য দশমিক পাঁচ");
assert_eq!(f(-1.5), "ঋণাত্মক এক দশমিক পাঁচ");
assert_eq!(f(-12.34), "ঋণাত্মক বারো দশমিক তিন চার");
assert_eq!(f(1.005), "এক দশমিক পাঁচ");
assert_eq!(f(2.675), "দুই দশমিক ছয় সাত পাঁচ");
}
#[test]
fn corpus_cardinal_dec() {
assert_eq!(d("0.01"), "শূন্য দশমিক এক");
assert_eq!(d("1.10"), "এক দশমিক এক");
assert_eq!(d("12.345"), "বারো দশমিক তিন চার পাঁচ");
assert_eq!(
d("98746251323029.99"),
"আটানব্বই লাখ চুয়াত্তর হাজার ছয়শত পঁচিশ কোটি তেরো লাখ তেইশ হাজার উনত্রিশ দশমিক নয় নয়"
);
assert_eq!(d("0.001"), "শূন্য দশমিক এক");
}
#[test]
fn leading_zeros_are_eaten() {
assert_eq!(f(0.01), f(0.1));
assert_eq!(d("0.001"), d("0.1"));
assert_eq!(f(1.005), "এক দশমিক পাঁচ");
assert_eq!(f(3.001), "তিন দশমিক এক");
}
#[test]
fn interior_zero_double_space() {
assert_eq!(f(1.102), "এক দশমিক এক দুই");
assert_eq!(f(1.507), "এক দশমিক পাঁচ সাত");
assert_eq!(d("1.10"), "এক দশমিক এক");
assert_eq!(d("0.10"), "শূন্য দশমিক এক");
assert_eq!(f(0.1 + 0.2), "শূন্য দশমিক তিন চার");
}
#[test]
fn no_float2tuple_artefacts() {
assert_eq!(f(2.675), "দুই দশমিক ছয় সাত পাঁচ");
assert_eq!(f(12.34), "বারো দশমিক তিন চার");
}
#[test]
fn sci_notation_cliff() {
assert_eq!(f(1e-5), "শূন্য দশমিক এক");
assert_eq!(f(1e-7), "শূন্য");
assert_eq!(f(5e-324), "শূন্য");
match f_err(1.5e-7) {
N2WError::Value(m) => {
assert_eq!(m, "invalid literal for int() with base 10: '5E-7'")
}
e => panic!("expected ValueError, got {:?}", e),
}
}
#[test]
fn zero_fraction_prints_no_point() {
assert_eq!(f(1.0), "এক");
assert_eq!(f(0.0), "শূন্য");
assert_eq!(f(-0.0), "শূন্য");
assert_eq!(d("1.00"), "এক");
assert_eq!(d("1E+2"), "একশত");
}
#[test]
fn prec28_rounding_is_live() {
assert_eq!(d(&format!("0.{}", "9".repeat(30))), "এক");
assert_eq!(d(&format!("0.{}", "9".repeat(2))), "শূন্য দশমিক নয় নয়");
}
#[test]
fn large_values() {
assert_eq!(f(1e16), "একশত কোটি কোটি");
assert_eq!(f(1e300), LangBn::new().to_cardinal(&(BigInt::from(10u8).pow(300))).unwrap());
match f_err(1e307) {
N2WError::Custom { module, class, .. } => {
assert_eq!(module, "num2words2.lang_BN");
assert_eq!(class, "NumberTooLargeError");
}
e => panic!("expected NumberTooLargeError, got {:?}", e),
}
}
#[test]
fn non_finite() {
match f_err(f64::INFINITY) {
N2WError::Overflow(m) => assert_eq!(m, "cannot convert Infinity to integer"),
e => panic!("expected OverflowError, got {:?}", e),
}
match f_err(f64::NEG_INFINITY) {
N2WError::Overflow(m) => assert_eq!(m, "cannot convert Infinity to integer"),
e => panic!("expected OverflowError, got {:?}", e),
}
match f_err(f64::NAN) {
N2WError::Value(m) => assert_eq!(m, "cannot convert NaN to integer"),
e => panic!("expected ValueError, got {:?}", e),
}
}
#[test]
fn float_cardinal_matches_word_for_every_paisa() {
let l = LangBn::new();
for dp in 1..=99u32 {
assert_eq!(
f(dp as f64),
l.number_to_bengali_word(&BigInt::from(dp)),
"paisa {} diverges",
dp
);
}
}
#[test]
fn precision_override_is_ignored() {
let l = LangBn::new();
let v = FloatValue::Float {
value: 3.14159,
precision: 5,
};
let expected = "তিন দশমিক এক চার এক পাঁচ নয়";
assert_eq!(l.to_cardinal_float(&v, None).unwrap(), expected);
assert_eq!(l.to_cardinal_float(&v, Some(1)).unwrap(), expected);
assert_eq!(l.to_cardinal_float(&v, Some(8)).unwrap(), expected);
}
}