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;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "point";
const ONES: [&str; 10] = [
"", "еден", "два", "три", "четири", "пет", "шест", "седум", "осум", "девет",
];
const TENS: [&str; 10] = [
"",
"десет",
"дваесет",
"триесет",
"четириесет",
"педесет",
"шеесет",
"седумдесет",
"осумдесет",
"деведесет",
];
const HUNDRED: &str = "сто";
const THOUSAND: &str = "илјада";
const MILLION: &str = "милион";
const ZERO_WORD: &str = "zero";
fn plain_decimal_string(value: &BigDecimal) -> String {
let (mantissa, scale) = value.as_bigint_and_exponent();
let digits = mantissa.abs().to_string();
if scale <= 0 {
let mut s = digits;
for _ in 0..(-scale) {
s.push('0');
}
return s;
}
let scale = scale as usize;
let n = digits.chars().count();
if n <= scale {
format!("0.{}{}", "0".repeat(scale - n), digits)
} else {
let int_part: String = digits.chars().take(n - scale).collect();
let frac_part: String = digits.chars().skip(n - scale).collect();
format!("{}.{}", int_part, frac_part)
}
}
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 })
}
pub struct LangMk {
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl Default for LangMk {
fn default() -> Self {
Self::new()
}
}
impl LangMk {
pub fn new() -> Self {
let table = [
("MKD", &["денар", "денари"][..], &["дени", "дени"][..]),
("USD", &["dollar", "dollars"][..], &["cent", "cents"][..]),
("EUR", &["euro", "euros"][..], &["cent", "cents"][..]),
];
let (_, fallback_unit, fallback_subunit) = table[0];
let fallback_forms = CurrencyForms::new(fallback_unit, fallback_subunit);
let currency_forms: HashMap<&'static str, CurrencyForms> = table
.into_iter()
.map(|(k, u, s)| (k, CurrencyForms::new(u, s)))
.collect();
LangMk {
currency_forms,
fallback_forms,
}
}
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()));
}
let ten = BigInt::from(10);
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
let million = BigInt::from(1_000_000);
let billion = BigInt::from(1_000_000_000);
if number < &ten {
let d = number.to_usize().expect("number < 10 fits usize");
return ONES[d].to_string();
}
if number < &hundred {
let (tens_val, ones_val) = number.div_mod_floor(&ten);
let t = tens_val.to_usize().expect("number < 100 => tens fit usize");
let o = ones_val.to_usize().expect("mod 10 fits usize");
if o == 0 {
return TENS[t].to_string();
}
return format!("{} {}", TENS[t], ONES[o]);
}
if number < &thousand {
let (hundreds_val, remainder) = number.div_mod_floor(&hundred);
let h = hundreds_val
.to_usize()
.expect("number < 1000 => hundreds fit usize");
let mut result = format!("{} {}", ONES[h], HUNDRED);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
return result;
}
if number < &million {
let (thousands_val, remainder) = number.div_mod_floor(&thousand);
let mut result = format!("{} {}", self.int_to_word(&thousands_val), THOUSAND);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
return result;
}
if number < &billion {
let (millions_val, remainder) = number.div_mod_floor(&million);
let mut result = format!("{} {}", self.int_to_word(&millions_val), MILLION);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&self.int_to_word(&remainder));
}
return result;
}
number.to_string()
}
}
impl Lang for LangMk {
fn default_currency(&self) -> &str {
"MKD"
}
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 = if value.is_negative() { NEGWORD } else { "" };
let magnitude = value.abs();
Ok(format!("{}{}", ret, self.int_to_word(&magnitude))
.trim()
.to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-ти", self.to_cardinal(value)?))
}
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, ret) = match n_full.strip_prefix('-') {
Some(rest) => (rest, NEGWORD),
None => (n_full, ""),
};
match n.split_once('.') {
Some((left, right)) => {
let left_int = py_int(left)?;
let mut out = String::from(ret);
out.push_str(&self.int_to_word(&left_int));
out.push(' ');
out.push_str(POINTWORD);
out.push(' ');
for ch in right.chars() {
let mut buf = [0u8; 4];
let d = py_int(ch.encode_utf8(&mut buf))?;
out.push_str(&self.int_to_word(&d));
out.push(' ');
}
Ok(out.trim().to_string())
}
None => {
let int = py_int(n)?;
Ok(format!("{}{}", ret, self.int_to_word(&int))
.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)?))
}
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> {
let parsed = python_decimal_parse(s)?;
Ok(parsed)
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok(format!(
"{}Infinity.",
if negative { "-" } else { "" }
)),
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".to_string(),
)),
}
}
fn nan_result(&self, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok("NaN.".to_string()),
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".to_string(),
)),
}
}
fn lang_name(&self) -> &str {
"Num2Word_MK"
}
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 is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(v) => v.abs().to_string(),
CurrencyValue::Decimal { value: d, .. } => plain_decimal_string(&d.abs()),
};
let mut parts = s.split('.');
let int_part = parts.next().unwrap_or("");
let frac_part = parts.next();
let left = if int_part.is_empty() {
BigInt::zero()
} else {
BigInt::from_str(int_part).map_err(|e| N2WError::Value(e.to_string()))?
};
let right = match frac_part {
Some(frac) if !frac.is_empty() => {
let mut two: String = frac.chars().take(2).collect();
while two.chars().count() < 2 {
two.push('0');
}
BigInt::from_str(&two).map_err(|e| N2WError::Value(e.to_string()))?
}
_ => BigInt::zero(),
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.fallback_forms);
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let one = BigInt::one();
let left_str = self.int_to_word(&left);
let mut result = format!("{} {}", left_str, if left != one { &cr1[1] } else { &cr1[0] });
if cents && !right.is_zero() {
let cents_str = self.int_to_word(&right);
result.push_str(separator);
result.push_str(&format!(
"{} {}",
cents_str,
if right != one { &cr2[1] } else { &cr2[0] }
));
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}