use crate::base::{set_low_numwords, set_mid_numwords, Cards, Lang, N2WError, Result};
use crate::currency::{default_to_currency, CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use num_bigint::BigInt;
use num_traits::{One, Signed, Zero};
use std::collections::HashMap;
const REGULAR_ORDINAL_SUFFIX: &str = "वाँ";
const IRREGULAR_ORDINALS: [(u8, &str); 6] = [
(0, "शून्य"),
(1, "पहला"),
(2, "दूसरा"),
(3, "तीसरा"),
(4, "चौथा"),
(6, "छठा"),
];
const IRREGULAR_ORDINALS_NUMS: [(u8, &str); 6] = [
(0, "०"),
(1, "१ला"),
(2, "२रा"),
(3, "३रा"),
(4, "४था"),
(6, "६ठा"),
];
const HINDI_DIGITS: [char; 10] = ['०', '१', '२', '३', '४', '५', '६', '७', '८', '९'];
const HIGH_NUMWORDS: [(u32, &str); 19] = [
(41, "महागण"),
(37, "गण"),
(35, "महाओम"),
(33, "ओम"),
(31, "महा अशोहिणी"),
(29, "अशोहिणी"),
(27, "महामध्या"),
(25, "मध्या"),
(23, "महाअंत्या"),
(21, "अंत्या"),
(19, "महाशंख"),
(17, "शंख"),
(15, "पद्म"),
(13, "नील"),
(11, "खरब"),
(9, "अरब"),
(7, "करोड़"),
(5, "लाख"),
(3, "हज़ार"),
];
const LOW_NUMWORDS: [&str; 100] = [
"निन्यानवे",
"अट्ठानवे",
"सत्तानवे",
"छियानवे",
"पचानवे",
"चौरानवे",
"तिरानवे",
"बानवे",
"इक्यानवे",
"नब्बे",
"नवासी",
"अट्ठासी",
"सतासी",
"छियासी",
"पचासी",
"चौरासी",
"तिरासी",
"बयासी",
"इक्यासी",
"अस्सी",
"उनासी",
"अठहत्तर",
"सतहत्तर",
"छिहत्तर",
"पचहत्तर",
"चौहत्तर",
"तिहत्तर",
"बहत्तर",
"इकहत्तर",
"सत्तर",
"उनहत्तर",
"अड़सठ",
"सड़सठ",
"छियासठ",
"पैंसठ",
"चौंसठ",
"तिरसठ",
"बासठ",
"इकसठ",
"साठ",
"उनसठ",
"अट्ठावन",
"सत्तावन",
"छप्पन",
"पचपन",
"चौवन",
"तिरेपन",
"बावन",
"इक्यावन",
"पचास",
"उनचास",
"अड़तालीस",
"सैंतालीस",
"छियालीस",
"पैंतालीस",
"चौवालीस",
"तैंतालीस",
"बयालीस",
"इकतालीस",
"चालीस",
"उनतालीस",
"अड़तीस",
"सैंतीस",
"छत्तीस",
"पैंतीस",
"चौंतीस",
"तैंतीस",
"बत्तीस",
"इकत्तीस",
"तीस",
"उनतीस",
"अट्ठाईस",
"सत्ताईस",
"छब्बीस",
"पच्चीस",
"चौबीस",
"तेईस",
"बाईस",
"इक्कीस",
"बीस",
"उन्नीस",
"अट्ठारह",
"सत्रह",
"सोलह",
"पंद्रह",
"चौदह",
"तेरह",
"बारह",
"ग्यारह",
"दस",
"नौ",
"आठ",
"सात",
"छः",
"पाँच",
"चार",
"तीन",
"दो",
"एक",
"शून्य",
];
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("INR", CurrencyForms::new(&["रुपया", "रुपये"], &["पैसा", "पैसे"]));
m.insert("USD", CurrencyForms::new(&["डॉलर", "डॉलर"], &["सेंट", "सेंट"]));
m.insert("EUR", CurrencyForms::new(&["यूरो", "यूरो"], &["सेंट", "सेंट"]));
m
}
fn convert_to_hindi_numerals(value: &BigInt) -> Result<String> {
let mut out = String::new();
for ch in value.to_string().chars() {
match ch.to_digit(10) {
Some(d) => out.push(HINDI_DIGITS[d as usize]),
None => return Err(N2WError::Key(format!("'{}'", ch))),
}
}
Ok(out)
}
fn irregular_lookup(table: &[(u8, &'static str)], value: &BigInt) -> Option<&'static str> {
table
.iter()
.find(|(k, _)| value == &BigInt::from(*k))
.map(|(_, w)| *w)
}
pub struct LangHi {
cards: Cards,
maxval: BigInt,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangHi {
fn default() -> Self {
Self::new()
}
}
impl LangHi {
pub fn new() -> Self {
let mut cards = Cards::new();
for (n, word) in HIGH_NUMWORDS.iter() {
cards.insert(BigInt::from(10u8).pow(*n), *word);
}
set_mid_numwords(&mut cards, &[(100, "सौ")]);
set_low_numwords(&mut cards, &LOW_NUMWORDS);
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
LangHi {
cards,
maxval,
currency_forms: build_currency_forms(),
}
}
}
impl Lang for LangHi {
fn default_currency(&self) -> &str {
"INR"
}
fn default_separator(&self) -> &str {
","
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"माइनस "
}
fn pointword(&self) -> &str {
"दशमलव"
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
let (ltext, lnum) = l;
let (rtext, rnum) = r;
let hundred = BigInt::from(100);
let drops_ek = rnum == &hundred
|| rnum == &BigInt::from(100_000u32)
|| rnum == &BigInt::from(10_000_000u32);
if lnum.is_one() && drops_ek {
(rtext.to_string(), rnum.clone())
} else if lnum.is_one() && rnum < &hundred {
(rtext.to_string(), rnum.clone())
} else if lnum >= &hundred && &hundred > rnum {
(format!("{} {}", ltext, rtext), lnum + rnum)
} else if rnum > lnum {
(format!("{} {}", ltext, rtext), lnum * rnum)
} else {
(format!("{} {}", ltext, rtext), lnum + rnum)
}
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if let Some(word) = irregular_lookup(&IRREGULAR_ORDINALS, value) {
return Ok(word.to_string());
}
let cardinal = self.to_cardinal(value)?;
Ok(format!("{}{}", cardinal, REGULAR_ORDINAL_SUFFIX))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
if let Some(word) = irregular_lookup(&IRREGULAR_ORDINALS_NUMS, value) {
return Ok(word.to_string());
}
Ok(format!(
"{}{}",
convert_to_hindi_numerals(value)?,
REGULAR_ORDINAL_SUFFIX
))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if let Some(i) = value.as_whole_int() {
return self.to_ordinal(&i);
}
Ok(format!(
"{}{}",
self.cardinal_float_entry(value, None)?,
REGULAR_ORDINAL_SUFFIX
))
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
if let Some(i) = value.as_whole_int() {
if let Some(word) = irregular_lookup(&IRREGULAR_ORDINALS_NUMS, &i) {
return Ok(word.to_string());
}
}
let mut out = String::new();
for ch in repr_str.chars() {
match ch.to_digit(10) {
Some(d) => out.push(HINDI_DIGITS[d as usize]),
None => return Err(N2WError::Key(format!("'{}'", ch))),
}
}
Ok(format!("{}{}", out, REGULAR_ORDINAL_SUFFIX))
}
fn lang_name(&self) -> &str {
"Num2Word_HI"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
if n.is_one() {
return forms
.first()
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()));
}
forms
.get(1)
.or_else(|| forms.first())
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
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());
if let CurrencyValue::Int(v) = val {
let forms = match self.currency_forms.get(currency) {
Some(f) => f,
None => {
return default_to_currency(self, val, currency, cents, separator, adjective)
}
};
let minus_str = if v.is_negative() { self.negword() } else { "" };
let abs_val = v.abs();
let money_str = self.to_cardinal(&abs_val)?;
let unit = &forms.unit;
let currency_str = if abs_val.is_one() {
unit.first()
} else {
unit.get(1).or_else(|| unit.first())
}
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
return Ok(format!("{} {} {}", minus_str, money_str, currency_str)
.trim()
.to_string());
}
default_to_currency(self, val, currency, cents, separator, adjective)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn cur(arg: &str, code: &str) -> Result<String> {
let is_int = !arg.contains('.');
let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
LangHi::new().to_currency(&v, code, true, None, false)
}
fn cheque(arg: &str, code: &str) -> Result<String> {
LangHi::new().to_cheque(&BigDecimal::from_str(arg).unwrap(), code)
}
#[test]
fn corpus_currency_implemented() {
for (code, unit1, unit2, sub1, sub2) in [
("INR", "रुपया", "रुपये", "पैसा", "पैसे"),
("USD", "डॉलर", "डॉलर", "सेंट", "सेंट"),
("EUR", "यूरो", "यूरो", "सेंट", "सेंट"),
] {
assert_eq!(cur("0", code).unwrap(), format!("शून्य {unit2}"));
assert_eq!(cur("1", code).unwrap(), format!("एक {unit1}"));
assert_eq!(cur("2", code).unwrap(), format!("दो {unit2}"));
assert_eq!(cur("100", code).unwrap(), format!("सौ {unit2}"));
assert_eq!(cur("1000000", code).unwrap(), format!("दस लाख {unit2}"));
assert_eq!(
cur("12.34", code).unwrap(),
format!("बारह {unit2}, चौंतीस {sub2}")
);
assert_eq!(
cur("0.01", code).unwrap(),
format!("शून्य {unit2}, एक {sub1}")
);
assert_eq!(cur("1.0", code).unwrap(), format!("एक {unit1}, शून्य {sub2}"));
assert_eq!(
cur("99.99", code).unwrap(),
format!("निन्यानवे {unit2}, निन्यानवे {sub2}")
);
assert_eq!(
cur("1234.56", code).unwrap(),
format!("एक हज़ार दो सौ चौंतीस {unit2}, छप्पन {sub2}")
);
assert_eq!(
cur("-12.34", code).unwrap(),
format!("माइनस बारह {unit2}, चौंतीस {sub2}")
);
assert_eq!(
cur("0.5", code).unwrap(),
format!("शून्य {unit2}, पचास {sub2}")
);
}
}
#[test]
fn corpus_currency_not_implemented() {
for code in ["GBP", "JPY", "KWD", "BHD", "CNY", "CHF"] {
for arg in [
"0", "1", "2", "100", "12.34", "0.01", "1.0", "99.99", "1234.56", "-12.34",
"1000000", "0.5",
] {
match cur(arg, code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{code}\" not implemented for \"Num2Word_HI\"")
),
other => panic!("{code} {arg}: expected NotImplementedError, got {other:?}"),
}
}
}
}
#[test]
fn corpus_cheque() {
assert_eq!(
cheque("1234.56", "INR").unwrap(),
"एक हज़ार दो सौ चौंतीस AND 56/100 रुपये"
);
assert_eq!(
cheque("1234.56", "USD").unwrap(),
"एक हज़ार दो सौ चौंतीस AND 56/100 डॉलर"
);
assert_eq!(
cheque("1234.56", "EUR").unwrap(),
"एक हज़ार दो सौ चौंतीस AND 56/100 यूरो"
);
for code in ["GBP", "JPY", "KWD", "BHD", "CNY", "CHF"] {
match cheque("1234.56", code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{code}\" not implemented for \"Num2Word_HI\"")
),
other => panic!("{code}: expected NotImplementedError, got {other:?}"),
}
}
}
#[test]
fn quirks_match_python() {
assert_eq!(cur("-1", "INR").unwrap(), "माइनस एक रुपया");
assert_eq!(cur("-2", "INR").unwrap(), "माइनस दो रुपये");
assert_eq!(cur("-100", "INR").unwrap(), "माइनस सौ रुपये");
assert_eq!(cur("-12", "INR").unwrap(), "माइनस बारह रुपये");
let plain = CurrencyValue::Decimal {
value: BigDecimal::from_str("5").unwrap(),
has_decimal: false,
is_float: false,
};
assert_eq!(
LangHi::new()
.to_currency(&plain, "INR", true, None, false)
.unwrap(),
"पाँच रुपये"
);
let scaled = CurrencyValue::Decimal {
value: BigDecimal::from_str("5.00").unwrap(),
has_decimal: true,
is_float: false,
};
assert_eq!(
LangHi::new()
.to_currency(&scaled, "INR", true, None, false)
.unwrap(),
"पाँच रुपये, शून्य पैसे"
);
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
assert_eq!(
LangHi::new()
.to_currency(&v, "INR", false, None, false)
.unwrap(),
"बारह रुपये, 34 पैसे"
);
let v = CurrencyValue::parse("12.04", false, true, true).unwrap();
assert_eq!(
LangHi::new()
.to_currency(&v, "INR", false, None, false)
.unwrap(),
"बारह रुपये, 04 पैसे"
);
let v = CurrencyValue::parse("2", true, false, false).unwrap();
assert_eq!(
LangHi::new()
.to_currency(&v, "INR", false, None, false)
.unwrap(),
"दो रुपये"
);
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
assert_eq!(
LangHi::new()
.to_currency(&v, "INR", true, Some(" और"), false)
.unwrap(),
"बारह रुपये और चौंतीस पैसे"
);
let v = CurrencyValue::parse("2", true, false, false).unwrap();
assert_eq!(
LangHi::new()
.to_currency(&v, "INR", true, Some(" और"), false)
.unwrap(),
"दो रुपये"
);
let v = CurrencyValue::parse("12.34", false, true, true).unwrap();
assert_eq!(
LangHi::new()
.to_currency(&v, "INR", true, None, true)
.unwrap(),
"बारह रुपये, चौंतीस पैसे"
);
assert_eq!(cur("10000000000000", "INR").unwrap(), "एक नील रुपये");
assert!(matches!(
cur("100000000000000000000000000000000000000000000", "INR"),
Err(N2WError::Overflow(_))
));
}
#[test]
fn fractional_cents_match_python() {
assert_eq!(cur("2.675", "INR").unwrap(), "दो रुपये, सड़सठ दशमलव पाँच पैसे");
assert_eq!(cur("1.005", "INR").unwrap(), "एक रुपया, शून्य दशमलव पाँच पैसे");
assert_eq!(
cur("12.999", "INR").unwrap(),
"बारह रुपये, निन्यानवे दशमलव नौ पैसे"
);
assert_eq!(cur("1.011", "INR").unwrap(), "एक रुपया, एक दशमलव एक पैसे");
assert_eq!(cur("0.001", "INR").unwrap(), "शून्य रुपये, शून्य दशमलव एक पैसे");
assert_eq!(
cur("-0.001", "INR").unwrap(),
"माइनस शून्य रुपये, शून्य दशमलव एक पैसे"
);
}
}