use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, 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 FARSI_ONES: [&str; 20] = [
"", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه", "ده", "یازده", "دوازده",
"سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده",
];
const FARSI_TENS: [&str; 10] = [
"", "ده", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود",
];
const FARSI_HUNDREDS: [&str; 10] = [
"", "صد", "دویست", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد", "نهصد",
];
const FARSI_BIG: [&str; 6] = [
"", " هزار", " میلیون", " میلیارد", " تریلیون", " تریلیارد",
];
const FARSI_FRAC: [&str; 3] = ["", "دهم", "صدم"];
const FARSI_FRAC_BIG: [&str; 4] = ["", "هزارم", "میلیونیم", "میلیاردیم"];
const HALF_WORD: &str = "نیم";
const SEPARATOR: &str = " و ";
const ZERO_WORD: &str = "صفر";
const NEGWORD: &str = "منفی ";
const ORDINAL_SUFFIX: &str = "م";
const SOOM_SUFFIX: &str = "وم";
const HEH: char = 'ه';
const SEEN: char = 'س';
fn index_error(msg: &str) -> N2WError {
N2WError::Index(msg.to_string())
}
fn negword_attribute_error() -> N2WError {
N2WError::Attribute("'Num2Word_FA' object has no attribute 'negword'".to_string())
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const CENTS: [&str; 2] = ["سنت", "سنت"];
const NO_SUBUNIT: [&str; 2] = ["", ""];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("IRR", CurrencyForms::new(&["ریال", "ریال"], &NO_SUBUNIT));
m.insert("IRT", CurrencyForms::new(&["تومان", "تومان"], &NO_SUBUNIT));
m.insert("EUR", CurrencyForms::new(&["یورو", "یورو"], &CENTS));
m.insert("USD", CurrencyForms::new(&["دلار", "دلار"], &CENTS));
m
}
fn cardinal3(number: u32) -> String {
debug_assert!(number <= 999, "cardinal3 invariant: chunk is taken mod 1000");
if number <= 19 {
return FARSI_ONES[number as usize].to_string();
}
if number < 100 {
let (x, y) = (number / 10, number % 10);
if y == 0 {
return FARSI_TENS[x as usize].to_string();
}
return format!("{}{}{}", FARSI_TENS[x as usize], SEPARATOR, FARSI_ONES[y as usize]);
}
let (x, y) = (number / 100, number % 100);
if y == 0 {
return FARSI_HUNDREDS[x as usize].to_string();
}
format!("{}{}{}", FARSI_HUNDREDS[x as usize], SEPARATOR, cardinal3(y))
}
fn cardinal_pos(number: &BigInt) -> String {
let thousand = BigInt::from(1000u32);
let mut x = number.clone();
let mut res = String::new();
for big in FARSI_BIG.iter() {
let (quot, rem) = x.div_mod_floor(&thousand);
x = quot;
let y = rem
.to_u32()
.expect("n mod 1000 always fits in u32");
if y == 0 {
continue;
}
let mut yx = format!("{}{}", cardinal3(y), big);
if *big == FARSI_BIG[1] && y == 1 {
yx = FARSI_BIG[1].trim_start().to_string();
}
res = if res.is_empty() {
yx
} else {
format!("{}{}{}", yx, SEPARATOR, res)
};
}
res
}
fn fractional(number: &BigInt, level: u32) -> Result<String> {
if number == &BigInt::from(5) {
return Ok(HALF_WORD.to_string());
}
let x = cardinal_pos(number);
let ld3 = (level / 3) as usize;
let lm3 = (level % 3) as usize;
let frac = FARSI_FRAC[lm3];
let frac_big = *FARSI_FRAC_BIG
.get(ld3)
.ok_or_else(|| index_error("list index out of range"))?;
let ltext = format!("{} {}", frac, frac_big);
let ltext = ltext.trim();
Ok(format!("{} {}", x, ltext))
}
fn fa_negate(v: &FloatValue) -> FloatValue {
match v {
FloatValue::Float { value, precision } => FloatValue::Float {
value: -*value,
precision: *precision,
},
FloatValue::Decimal { value, precision } => FloatValue::Decimal {
value: -value.clone(),
precision: *precision,
},
}
}
fn fa_is_zero(v: &FloatValue) -> bool {
match v {
FloatValue::Float { value, .. } => *value == 0.0,
FloatValue::Decimal { value, .. } => value.is_zero(),
}
}
fn fa_cardinal_float(v: &FloatValue) -> Result<String> {
if v.is_negative() {
let neg = fa_negate(v);
return Ok(format!("{}{}", NEGWORD, fa_cardinal_float(&neg)?));
}
if fa_is_zero(v) {
return Ok(ZERO_WORD.to_string());
}
let (pre, post) = float2tuple(v);
let level = v.precision();
if post.is_zero() {
return Ok(cardinal_pos(&pre));
}
if pre.is_zero() {
return fractional(&post, level);
}
Ok(format!(
"{}{}{}",
cardinal_pos(&pre),
SEPARATOR,
fractional(&post, level)?
))
}
fn float_repr_precision(f: f64) -> u32 {
let s = format!("{}", f);
match s.split_once('.') {
Some((_, frac)) if !frac.contains('e') => frac.len() as u32,
_ => 0,
}
}
pub struct LangFa {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangFa {
fn default() -> Self {
Self::new()
}
}
impl LangFa {
pub fn new() -> Self {
LangFa {
currency_forms: build_currency_forms(),
}
}
}
impl Lang for LangFa {
fn default_currency(&self) -> &str {
"IRT"
}
fn default_separator(&self) -> &str {
" و "
}
fn negword(&self) -> &str {
NEGWORD
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
return Ok(format!("{}{}", NEGWORD, self.to_cardinal(&(-value))?));
}
if value.is_zero() {
return Ok(ZERO_WORD.to_string());
}
Ok(cardinal_pos(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
let r = self.to_cardinal(value)?;
let chars: Vec<char> = r.chars().collect();
let last = match chars.last() {
Some(c) => *c,
None => return Err(index_error("string index out of range")),
};
if last == HEH {
if chars.len() < 2 {
return Err(index_error("string index out of range"));
}
if chars[chars.len() - 2] == SEEN {
let head: String = chars[..chars.len() - 1].iter().collect();
return Ok(format!("{}{}", head, SOOM_SUFFIX));
}
}
Ok(format!("{}{}", r, ORDINAL_SUFFIX))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", value, ORDINAL_SUFFIX))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let r = self.cardinal_float_entry(value, None)?;
let chars: Vec<char> = r.chars().collect();
let last = match chars.last() {
Some(c) => *c,
None => return Err(index_error("string index out of range")),
};
if last == HEH {
if chars.len() < 2 {
return Err(index_error("string index out of range"));
}
if chars[chars.len() - 2] == SEEN {
let head: String = chars[..chars.len() - 1].iter().collect();
return Ok(format!("{}{}", head, SOOM_SUFFIX));
}
}
Ok(format!("{}{}", r, ORDINAL_SUFFIX))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}{}", repr_str, ORDINAL_SUFFIX))
}
fn to_fraction(&self, numerator: &BigInt, denominator: &BigInt) -> Result<String> {
if denominator.is_zero() {
return Err(N2WError::ZeroDivision(
"denominator must not be zero".into(),
));
}
if denominator == &BigInt::one() || numerator.is_zero() {
return self.to_cardinal(numerator);
}
let is_negative = numerator.is_negative() ^ denominator.is_negative();
if is_negative {
return Err(negword_attribute_error());
}
let abs_n = numerator.abs();
let abs_d = denominator.abs();
let num_word = self.to_cardinal(&abs_n)?;
let mut den_word = self.to_ordinal(&abs_d)?;
if !abs_n.is_one() {
den_word.push('s');
}
Ok(format!("{} {}", num_word, den_word))
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::NaN => Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.InvalidOperation'>]".into(),
}),
other => Ok(other),
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
fa_cardinal_float(value)
}
fn lang_name(&self) -> &str {
"Num2Word_FA"
}
fn cardinal_from_decimal(&self, value: &BigDecimal) -> Result<String> {
let f = value
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", value)))?;
let precision = float_repr_precision(f);
fa_cardinal_float(&FloatValue::Float { value: f, precision })
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, _n: &BigInt, forms: &[String]) -> Result<String> {
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> {
if self.currency_forms(currency).is_none() {
return Err(N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
)));
}
if val.is_negative() {
return Err(negword_attribute_error());
}
crate::currency::default_to_currency(
self,
val,
currency,
cents,
separator.unwrap_or(self.default_separator()),
adjective,
)
}
}