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::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "point";
const ZERO_WORD: &str = "zero";
const ONES: [&str; 10] = [
"", "یو", "دوه", "درې", "څلور", "پنځه", "شپږ", "اووه", "اته", "نهه",
];
const TENS: [&str; 10] = [
"", "لس", "شل", "دېرش", "څلوېښت", "پنځوس", "شپېته", "اویا", "اتیا", "نوي",
];
const HUNDRED: &str = "سل";
const THOUSAND: &str = "زره";
const MILLION: &str = "میلیون";
const ORDINAL_SUFFIX: &str = "-م";
fn billion() -> BigInt {
BigInt::from(1_000_000_000u32)
}
const PS_SEPARATOR: &str = " ";
const SEPARATOR_UNSET: &str = ",";
const FALLBACK_CURRENCY: &str = "AFN";
pub struct LangPs {
currency_forms: HashMap<&'static str, CurrencyForms>,
afn: CurrencyForms,
}
impl Default for LangPs {
fn default() -> Self {
Self::new()
}
}
impl LangPs {
pub fn new() -> Self {
let afn = CurrencyForms::new(&["افغانۍ", "افغانۍ"], &["پول", "پول"]);
let currency_forms: HashMap<&'static str, CurrencyForms> = [
(FALLBACK_CURRENCY, afn.clone()),
(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
),
(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["cent", "cents"]),
),
]
.into_iter()
.collect();
LangPs { currency_forms, afn }
}
fn split_value(val: &CurrencyValue) -> (BigInt, u32, bool) {
match val {
CurrencyValue::Int(v) => (v.abs(), 0, v.is_negative()),
CurrencyValue::Decimal { value: d, .. } => {
let is_negative = d.is_negative();
let (digits, scale) = d.as_bigint_and_exponent();
let digits = digits.abs();
if scale <= 0 {
let pow = BigInt::from(10u32).pow((-scale) as u32);
return (digits * pow, 0, is_negative);
}
let pow = BigInt::from(10u32).pow(scale as u32);
let (left, frac) = digits.div_rem(&pow);
let right = (frac * BigInt::from(100u32) / &pow).to_u32().unwrap_or(0);
(left, right, is_negative)
}
}
}
fn select_form(n: &BigInt, forms: &[String]) -> String {
if n.is_one() {
forms[0].clone()
} else {
forms[1].clone()
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&number.abs()));
}
if number >= &billion() {
return number.to_string();
}
let n = number
.to_u32()
.expect("0 < number < 10^9 was proven above, so u32 conversion cannot fail");
self.pos_to_word(n)
}
fn pos_to_word(&self, n: u32) -> String {
if n < 10 {
ONES[n as usize].to_string()
} else if n < 100 {
let tens_val = (n / 10) as usize;
let ones_val = (n % 10) as usize;
if ones_val == 0 {
TENS[tens_val].to_string()
} else {
format!("{} {}", TENS[tens_val], ONES[ones_val])
}
} else if n < 1000 {
let hundreds_val = (n / 100) as usize;
let remainder = n % 100;
let mut result = format!("{} {}", ONES[hundreds_val], HUNDRED);
if remainder != 0 {
result.push(' ');
result.push_str(&self.pos_to_word(remainder));
}
result
} else if n < 1_000_000 {
let thousands_val = n / 1000;
let remainder = n % 1000;
let mut result = format!("{} {}", self.pos_to_word(thousands_val), THOUSAND);
if remainder != 0 {
result.push(' ');
result.push_str(&self.pos_to_word(remainder));
}
result
} else {
let millions_val = n / 1_000_000;
let remainder = n % 1_000_000;
let mut result = format!("{} {}", self.pos_to_word(millions_val), MILLION);
if remainder != 0 {
result.push(' ');
result.push_str(&self.pos_to_word(remainder));
}
result
}
}
}
fn python_repr_f64(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f < 0.0 { "-inf" } else { "inf" }.to_string();
}
let neg = f.is_sign_negative();
let s = format!("{:e}", f.abs()); let (mantissa, exp_s) = match s.split_once('e') {
Some(parts) => parts,
None => return s, };
let exp: i32 = exp_s.parse().unwrap_or(0);
let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
let ndigits = digits.len() as i32;
let decpt = exp + 1;
let body = if decpt <= -4 || decpt > 16 {
let mut m = String::new();
m.push_str(&digits[..1]);
if ndigits > 1 {
m.push('.');
m.push_str(&digits[1..]);
}
let e = decpt - 1;
let (esign, eabs) = if e < 0 { ('-', -e) } else { ('+', e) };
format!("{}e{}{:02}", m, esign, eabs)
} else if decpt <= 0 {
format!("0.{}{}", "0".repeat((-decpt) as usize), digits)
} else if decpt >= ndigits {
format!("{}{}.0", digits, "0".repeat((decpt - ndigits) as usize))
} else {
let dp = decpt as usize;
format!("{}.{}", &digits[..dp], &digits[dp..])
};
if neg {
format!("-{}", body)
} else {
body
}
}
fn python_str_decimal(bd: &BigDecimal) -> String {
let (coeff, scale) = bd.as_bigint_and_exponent();
let py_exp: i64 = -scale; let neg = coeff.is_negative(); let int_str = coeff.abs().to_string(); let ndigits = int_str.len() as i64;
let leftdigits = py_exp + ndigits;
let dotplace: i64 = if py_exp <= 0 && leftdigits > -6 {
leftdigits
} else {
1
};
let (intpart, fracpart) = if dotplace <= 0 {
(
"0".to_string(),
format!(".{}{}", "0".repeat((-dotplace) as usize), int_str),
)
} else if dotplace >= ndigits {
(
format!("{}{}", int_str, "0".repeat((dotplace - ndigits) as usize)),
String::new(),
)
} else {
let dp = dotplace as usize;
(int_str[..dp].to_string(), format!(".{}", &int_str[dp..]))
};
let exp = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
let sign = if neg { "-" } else { "" };
format!("{}{}{}{}", sign, intpart, fracpart, exp)
}
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.chars().all(|c| c.is_ascii_digit()) {
return Err(err());
}
let n: BigInt = body.parse().map_err(|_| err())?;
Ok(if negative { -n } else { n })
}
impl Lang for LangPs {
fn default_currency(&self) -> &str {
"AFN"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"point"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (ret, n) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, self.int_to_word(&n)).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 n_full = match value {
FloatValue::Float { value, .. } => python_repr_f64(*value),
FloatValue::Decimal { value, .. } => python_str_decimal(value),
};
let n_full = n_full.trim();
let (n, neg) = match n_full.strip_prefix('-') {
Some(rest) => (rest, true),
None => (n_full, false),
};
let mut ret = String::new();
if neg {
ret.push_str(NEGWORD);
}
if let Some((left, right)) = n.split_once('.') {
ret.push_str(&self.int_to_word(&py_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
for ch in right.chars() {
let mut buf = [0u8; 4];
let d = py_int(ch.encode_utf8(&mut buf))?;
ret.push_str(&self.int_to_word(&d));
ret.push(' ');
}
Ok(ret.trim().to_string())
} else {
ret.push_str(&self.int_to_word(&py_int(n)?));
Ok(ret.trim().to_string())
}
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> 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))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.to_cardinal_float(value, None)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
python_decimal_parse(s)
}
fn inf_result(&self, _negative: bool, _to: &str) -> Result<String> {
Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
))
}
fn nan_result(&self, _to: &str) -> Result<String> {
Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".into(),
))
}
fn lang_name(&self) -> &str {
"Num2Word_PS"
}
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 separator = if separator == SEPARATOR_UNSET {
PS_SEPARATOR
} else {
separator
};
let (left, right, is_negative) = Self::split_value(val);
let forms = self.currency_forms.get(currency).unwrap_or(&self.afn);
let left_str = self.int_to_word(&left);
let currency_word = Self::select_form(&left, &forms.unit);
let mut result = format!("{} {}", left_str, currency_word);
if cents && right != 0 {
let right_big = BigInt::from(right);
let cents_str = self.int_to_word(&right_big);
let cents_word = Self::select_form(&right_big, &forms.subunit);
result.push_str(separator);
result.push_str(¢s_str);
result.push(' ');
result.push_str(¢s_word);
}
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 f(value: f64, precision: u32) -> String {
LangPs::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(s: &str) -> String {
let value = BigDecimal::from_str(s).unwrap();
let precision = (-value.as_bigint_and_exponent().1).unsigned_abs() as u32;
LangPs::new()
.to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
.unwrap()
}
#[test]
fn corpus_float_rows() {
assert_eq!(f(0.0, 1), "zero point zero");
assert_eq!(f(0.5, 1), "zero point پنځه");
assert_eq!(f(1.0, 1), "یو point zero");
assert_eq!(f(1.5, 1), "یو point پنځه");
assert_eq!(f(2.25, 2), "دوه point دوه پنځه");
assert_eq!(f(3.14, 2), "درې point یو څلور");
assert_eq!(f(0.01, 2), "zero point zero یو");
assert_eq!(f(0.1, 1), "zero point یو");
assert_eq!(f(0.99, 2), "zero point نهه نهه");
assert_eq!(f(1.01, 2), "یو point zero یو");
assert_eq!(f(12.34, 2), "لس دوه point درې څلور");
assert_eq!(f(99.99, 2), "نوي نهه point نهه نهه");
assert_eq!(f(100.5, 1), "یو سل point پنځه");
assert_eq!(f(1234.56, 2), "یو زره دوه سل دېرش څلور point پنځه شپږ");
assert_eq!(f(-0.5, 1), "minus zero point پنځه");
assert_eq!(f(-1.5, 1), "minus یو point پنځه");
assert_eq!(f(-12.34, 2), "minus لس دوه point درې څلور");
assert_eq!(f(1.005, 3), "یو point zero zero پنځه");
assert_eq!(f(2.675, 3), "دوه point شپږ اووه پنځه");
}
#[test]
fn corpus_decimal_rows() {
assert_eq!(d("0.01"), "zero point zero یو");
assert_eq!(d("1.10"), "یو point یو zero");
assert_eq!(d("12.345"), "لس دوه point درې څلور پنځه");
assert_eq!(
d("98746251323029.99"),
"98746251323029 point نهه نهه"
);
assert_eq!(d("0.001"), "zero point zero zero یو");
}
#[test]
fn negative_zero_float() {
assert_eq!(f(-0.0, 1), "minus zero point zero");
}
#[test]
fn precision_override_ignored() {
let v = FloatValue::Float {
value: 2.675,
precision: 3,
};
assert_eq!(
LangPs::new().to_cardinal_float(&v, Some(1)).unwrap(),
"دوه point شپږ اووه پنځه"
);
assert_eq!(
LangPs::new().to_cardinal_float(&v, Some(5)).unwrap(),
"دوه point شپږ اووه پنځه"
);
}
}