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 verify_ordinal(value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
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> {
verify_ordinal(value)?;
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> {
let whole = match value.as_whole_int() {
Some(i) => i,
None => {
return Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
repr_str
)))
}
};
verify_ordinal(&whole)?;
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(),
"माइनस शून्य रुपये, शून्य दशमलव एक पैसे"
);
}
}