use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const ZERO: &str = "zero";
const NEGWORD: &str = "minus ";
const ONES: [&str; 10] = [
"", "\u{0647}\u{06AA}", "\u{067B}\u{0647}", "\u{067D}\u{064A}", "\u{0686}\u{0627}\u{0631}", "\u{067E}\u{0646}\u{062C}", "\u{0687}\u{0647}\u{0647}", "\u{0633}\u{062A}", "\u{0627}\u{067A}", "\u{0646}\u{0648}", ];
const TENS: [&str; 10] = [
"", "\u{068F}\u{0647}\u{0647}", "\u{0648}\u{064A}\u{0647}\u{0647}", "\u{067D}\u{064A}\u{0647}\u{0647}", "\u{0686}\u{0627}\u{0644}\u{064A}\u{0647}\u{0647}", "\u{067E}\u{0646}\u{062C}\u{0627}\u{0647}\u{0647}", "\u{0633}\u{067A}", "\u{0633}\u{062A}\u{0631}", "\u{0627}\u{0633}\u{064A}", "\u{0646}\u{0648}\u{064A}", ];
const HUNDRED: &str = "\u{0633}\u{0648}";
const THOUSAND: &str = "\u{0647}\u{0632}\u{0627}\u{0631}";
const MILLION: &str = "\u{0644}\u{06A9}";
const ORDINAL_SUFFIX: &str = "-\u{0648}";
const POINTWORD: &str = "point";
const FALLBACK_THRESHOLD: u64 = 1_000_000_000;
const RUPEE_SG: &str = "\u{0631}\u{0648}\u{067E}\u{064A}";
const RUPEE_PL: &str = "\u{0631}\u{0648}\u{067E}\u{064A}\u{0627}";
const PAISA_SG: &str = "\u{067E}\u{0626}\u{0633}\u{0648}";
const PAISA_PL: &str = "\u{067E}\u{0626}\u{0633}\u{0627}";
const SD_SEPARATOR: &str = " ";
const SEPARATOR_UNSET: &str = ",";
const CENTS_DIVISOR: i64 = 100;
pub struct LangSd {
forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl LangSd {
pub fn new() -> Self {
let pkr = CurrencyForms::new(&[RUPEE_SG, RUPEE_PL], &[PAISA_SG, PAISA_PL]);
let mut forms = HashMap::new();
forms.insert("PKR", pkr.clone());
forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["cent", "cents"]),
);
LangSd {
forms,
fallback_forms: pkr,
}
}
}
impl Default for LangSd {
fn default() -> Self {
Self::new()
}
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return ZERO.to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, int_to_word(&number.abs()));
}
match number.to_u64() {
Some(n) if n < FALLBACK_THRESHOLD => bounded_to_word(n),
_ => number.to_string(),
}
}
fn bounded_to_word(number: u64) -> String {
if number < 10 {
return ONES[number as usize].to_string();
}
if number < 100 {
let tens_val = (number / 10) as usize;
let ones_val = (number % 10) as usize;
if ones_val == 0 {
return TENS[tens_val].to_string();
}
return format!("{} {}", TENS[tens_val], ONES[ones_val]);
}
if number < 1_000 {
let hundreds_val = (number / 100) as usize;
let remainder = number % 100;
let mut result = format!("{} {}", ONES[hundreds_val], HUNDRED);
if remainder != 0 {
result.push(' ');
result.push_str(&bounded_to_word(remainder));
}
return result;
}
if number < 1_000_000 {
let thousands_val = number / 1_000;
let remainder = number % 1_000;
let mut result = format!("{} {}", bounded_to_word(thousands_val), THOUSAND);
if remainder != 0 {
result.push(' ');
result.push_str(&bounded_to_word(remainder));
}
return result;
}
let millions_val = number / 1_000_000;
let remainder = number % 1_000_000;
let mut result = format!("{} {}", bounded_to_word(millions_val), MILLION);
if remainder != 0 {
result.push(' ');
result.push_str(&bounded_to_word(remainder));
}
result
}
impl Lang for LangSd {
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> {
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(format!("{}-و", cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", repr_str))
}
fn str_to_number(&self, s: &str) -> Result<crate::strnum::ParsedNumber> {
match crate::strnum::python_decimal_parse(s)? {
crate::strnum::ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
p => Ok(p),
}
}
fn default_currency(&self) -> &str {
"PKR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
POINTWORD
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (prefix, magnitude) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", prefix, int_to_word(&magnitude))
.trim()
.to_string())
}
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))
}
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> {
let negative = match value {
FloatValue::Float { value: f, .. } => {
if !f.is_finite() || (*f != 0.0 && (f.abs() >= 1e16 || f.abs() < 1e-4)) {
return Err(N2WError::Value(
"invalid literal for int() with base 10".to_string(),
));
}
f.is_sign_negative()
}
FloatValue::Decimal { value: d, .. } => {
let (int_val, scale) = d.as_bigint_and_exponent();
let ndigits = int_val.abs().to_string().len() as i64;
if scale < 0 || (ndigits - 1 - scale) < -6 {
return Err(N2WError::Value(
"invalid literal for int() with base 10".to_string(),
));
}
d.is_negative()
}
};
let precision = value.precision() as usize;
let (pre, post) = float2tuple(value);
let mut ret = String::new();
if negative {
ret.push_str(NEGWORD);
}
ret.push_str(&int_to_word(&pre.abs()));
if precision > 0 {
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
let post_str = post.to_string();
let padding = "0".repeat(precision.saturating_sub(post_str.len()));
for ch in format!("{}{}", padding, post_str).chars().take(precision) {
let d = ch.to_digit(10).unwrap_or(0);
ret.push_str(&int_to_word(&BigInt::from(d)));
ret.push(' ');
}
}
Ok(ret.trim().to_string())
}
fn lang_name(&self) -> &str {
"Num2Word_SD"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.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 separator = if separator == SEPARATOR_UNSET {
SD_SEPARATOR
} else {
separator
};
let is_negative = val.is_negative();
let (left, right) = match val {
CurrencyValue::Int(i) => (i.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => {
let d = if is_negative { d.abs() } else { d.clone() };
let whole = d.with_scale(0);
let fraction = &d - &whole;
let cents_val = (fraction * BigDecimal::from(CENTS_DIVISOR)).with_scale(0);
(
whole.as_bigint_and_exponent().0,
cents_val.as_bigint_and_exponent().0,
)
}
};
let forms = self.forms.get(currency).unwrap_or(&self.fallback_forms);
let one = BigInt::one();
let unit = if left != one {
&forms.unit[1]
} else {
&forms.unit[0]
};
let mut result = format!("{} {}", int_to_word(&left), unit);
if cents && !right.is_zero() {
let subunit = if right != one {
&forms.subunit[1]
} else {
&forms.subunit[0]
};
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(subunit);
}
if is_negative {
result.insert_str(0, NEGWORD);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn flt(value: f64, precision: u32) -> String {
LangSd::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn dec(s: &str, precision: u32) -> String {
LangSd::new()
.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
},
None,
)
.unwrap()
}
#[test]
fn corpus_float_rows() {
assert_eq!(flt(0.0, 1), "zero point zero");
assert_eq!(flt(0.5, 1), "zero point پنج");
assert_eq!(flt(1.0, 1), "هڪ point zero");
assert_eq!(flt(1.5, 1), "هڪ point پنج");
assert_eq!(flt(2.25, 2), "ٻه point ٻه پنج");
assert_eq!(flt(3.14, 2), "ٽي point هڪ چار");
assert_eq!(flt(0.01, 2), "zero point zero هڪ");
assert_eq!(flt(0.1, 1), "zero point هڪ");
assert_eq!(flt(0.99, 2), "zero point نو نو");
assert_eq!(flt(1.01, 2), "هڪ point zero هڪ");
assert_eq!(flt(12.34, 2), "ڏهه ٻه point ٽي چار");
assert_eq!(flt(99.99, 2), "نوي نو point نو نو");
assert_eq!(flt(100.5, 1), "هڪ سو point پنج");
assert_eq!(flt(1234.56, 2), "هڪ هزار ٻه سو ٽيهه چار point پنج ڇهه");
assert_eq!(flt(-0.5, 1), "minus zero point پنج");
assert_eq!(flt(-1.5, 1), "minus هڪ point پنج");
assert_eq!(flt(-12.34, 2), "minus ڏهه ٻه point ٽي چار");
assert_eq!(flt(1.005, 3), "هڪ point zero zero پنج");
assert_eq!(flt(2.675, 3), "ٻه point ڇهه ست پنج"); assert_eq!(flt(2.0, 1), "ٻه point zero");
assert_eq!(flt(1000000.5, 1), "هڪ لک point پنج");
}
#[test]
fn corpus_decimal_rows() {
assert_eq!(dec("0.01", 2), "zero point zero هڪ");
assert_eq!(dec("1.10", 2), "هڪ point هڪ zero"); assert_eq!(dec("12.345", 3), "ڏهه ٻه point ٽي چار پنج");
assert_eq!(dec("98746251323029.99", 2), "98746251323029 point نو نو"); assert_eq!(dec("0.001", 3), "zero point zero zero هڪ");
}
#[test]
fn precision_override_ignored() {
let sd = LangSd::new();
let v = FloatValue::Float { value: 2.675, precision: 3 };
assert_eq!(
sd.to_cardinal_float(&v, Some(1)).unwrap(),
"ٻه point ڇهه ست پنج"
);
}
#[test]
fn float_negative_zero_keeps_negword() {
assert_eq!(flt(-0.0, 1), "minus zero point zero");
}
#[test]
fn scientific_and_nonfinite_raise_value() {
let sd = LangSd::new();
for (v, p) in [(1e16, 17u32), (1e-5, 5), (1.5e-5, 6), (f64::INFINITY, 0)] {
assert!(matches!(
sd.to_cardinal_float(&FloatValue::Float { value: v, precision: p }, None),
Err(N2WError::Value(_))
));
}
assert!(matches!(
sd.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str("0.0000001").unwrap(),
precision: 7,
},
None,
),
Err(N2WError::Value(_))
));
}
}