use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, python_decimal_str, 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 ONES: [&str; 10] = [
"",
"бір",
"екі",
"үш",
"төрт",
"бес",
"алты",
"жеті",
"сегіз",
"тоғыз",
];
const TENS: [&str; 10] = [
"",
"он",
"жиырма",
"отыз",
"қырық",
"елу",
"алпыс",
"жетпіс",
"сексен",
"тоқсан",
];
const HUNDRED: &str = "жүз";
const THOUSAND: &str = "мың";
const MILLION: &str = "миллион";
const ORDINAL_SUFFIX: &str = "-інші";
const LANG_NAME: &str = "Num2Word_KK";
const SEPARATOR_DEFAULT: &str = " ";
const SEPARATOR_UNSET: &str = ",";
const FALLBACK_CURRENCY: &str = "KZT";
const CENT_DIVISOR: i64 = 100;
fn int_value_error(s: &str) -> N2WError {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
}
fn py_float_sci_repr(a: f64) -> String {
let sci = format!("{:e}", a); let (mant, exp_str) = sci.split_once('e').expect("{:e} always emits an 'e'");
let exp: i64 = exp_str.parse().expect("{:e} exponent is a base-10 integer");
format!(
"{}e{}{:02}",
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
)
}
enum StrRoute {
Point,
WholeDigits(BigInt),
}
fn route_by_str(v: &FloatValue) -> Result<StrRoute> {
match v {
FloatValue::Float { value, .. } => {
let a = value.abs();
if !a.is_finite() {
return Err(int_value_error(if a.is_nan() { "nan" } else { "inf" }));
}
if a >= 1e16 || (a > 0.0 && a < 1e-4) {
return Err(int_value_error(&py_float_sci_repr(a)));
}
Ok(StrRoute::Point)
}
FloatValue::Decimal { value, .. } => {
let s = python_decimal_str(&value.abs());
if s.contains('.') {
Ok(StrRoute::Point)
} else if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {
Ok(StrRoute::WholeDigits(s.parse().expect("digits only")))
} else {
Err(int_value_error(&s))
}
}
}
}
pub struct LangKk {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangKk {
fn default() -> Self {
Self::new()
}
}
impl LangKk {
pub fn new() -> Self {
let currency_forms: HashMap<&'static str, CurrencyForms> = [
("KZT", &["теңге", "теңге"][..], &["тиын", "тиын"][..]),
("USD", &["dollar", "dollars"][..], &["cent", "cents"][..]),
("EUR", &["euro", "euros"][..], &["cent", "cents"][..]),
]
.into_iter()
.map(|(k, u, s)| (k, CurrencyForms::new(u, s)))
.collect();
LangKk { currency_forms }
}
fn split_currency(val: &CurrencyValue) -> (BigInt, BigInt) {
match val {
CurrencyValue::Int(i) => (i.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => {
let abs = d.abs();
let left = abs.with_scale(0).as_bigint_and_exponent().0;
let frac = &abs - BigDecimal::from(left.clone());
let right = (frac * BigDecimal::from(CENT_DIVISOR))
.with_scale(0)
.as_bigint_and_exponent()
.0;
(left, right)
}
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return if ONES[0].is_empty() {
"zero".to_string()
} else {
ONES[0].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(1_000);
let million = BigInt::from(1_000_000);
let billion = BigInt::from(1_000_000_000);
if number < &ten {
return ONES[number.to_usize().unwrap()].to_string();
}
if number < &hundred {
let (tens_val, ones_val) = number.div_rem(&ten);
let t = TENS[tens_val.to_usize().unwrap()]; let o = ones_val.to_usize().unwrap(); return if o == 0 {
t.to_string()
} else {
format!("{} {}", t, ONES[o])
};
}
if number < &thousand {
let (hundreds_val, remainder) = number.div_rem(&hundred);
let mut result = format!("{} {}", ONES[hundreds_val.to_usize().unwrap()], 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_rem(&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_rem(&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 LangKk {
fn default_currency(&self) -> &str {
"KZT"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"point"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let n = value.to_string(); let (ret, digits) = match n.strip_prefix('-') {
Some(rest) => (NEGWORD, rest),
None => ("", n.as_str()),
};
let parsed: BigInt = digits.parse().unwrap_or_else(|_| BigInt::zero());
Ok(format!("{}{}", ret, self.int_to_word(&parsed))
.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 (neg, left, right_str) = match value {
FloatValue::Float { value, precision } => {
let neg = value.is_sign_negative();
let prec = *precision as usize;
let s = format!("{:.p$}", value.abs(), p = prec);
match s.split_once('.') {
Some((l, r)) => (
neg,
l.parse::<BigInt>().unwrap_or_else(|_| BigInt::zero()),
r.to_string(),
),
None => (
neg,
s.parse::<BigInt>().unwrap_or_else(|_| BigInt::zero()),
String::new(),
),
}
}
FloatValue::Decimal { value, precision } => {
let neg = value.is_negative();
let abs = value.abs();
let left = abs.with_scale(0).as_bigint_and_exponent().0;
if *precision == 0 {
(neg, left, String::new())
} else {
let frac = &abs - BigDecimal::from(left.clone());
let scale = BigInt::from(10).pow(*precision);
let right = (frac * BigDecimal::from(scale))
.with_scale(0)
.as_bigint_and_exponent()
.0;
let rs = right.to_string();
let pad = (*precision as usize).saturating_sub(rs.len());
(neg, left, format!("{}{}", "0".repeat(pad), rs))
}
}
};
let mut tokens: Vec<String> = vec![self.int_to_word(&left)];
if !right_str.is_empty() {
tokens.push(self.pointword().to_string());
for ch in right_str.chars() {
let d = ch.to_digit(10).unwrap_or(0);
tokens.push(self.int_to_word(&BigInt::from(d)));
}
}
let joined = tokens.join(" ");
let result = if neg {
format!("{}{}", NEGWORD, joined)
} else {
joined
};
Ok(result.trim().to_string())
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
match route_by_str(value)? {
StrRoute::Point => self.to_cardinal_float(value, precision_override),
StrRoute::WholeDigits(abs_int) => {
let body = self.int_to_word(&abs_int);
if value.is_negative() {
Ok(format!("{}{}", NEGWORD, body).trim().to_string())
} else {
Ok(body)
}
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!(
"{}{}",
self.cardinal_float_entry(value, None)?,
ORDINAL_SUFFIX
))
}
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<ParsedNumber> {
python_decimal_parse(s)
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
match to {
"ordinal_num" => {
let token = if negative { "-Infinity" } else { "Infinity" };
Ok(format!("{}.", token))
}
_ => Err(int_value_error("Infinity")),
}
}
fn nan_result(&self, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok("NaN.".to_string()),
_ => Err(int_value_error("NaN")),
}
}
fn lang_name(&self) -> &str {
LANG_NAME
}
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 {
SEPARATOR_DEFAULT
} else {
separator
};
let is_negative = val.is_negative();
let (left, right) = Self::split_currency(val);
let forms = match self.currency_forms.get(currency) {
Some(f) => f,
None => &self.currency_forms[FALLBACK_CURRENCY],
};
let one = BigInt::one();
let left_str = self.int_to_word(&left);
let mut result = format!(
"{} {}",
left_str,
if left != one {
&forms.unit[1]
} else {
&forms.unit[0]
}
);
if cents && !right.is_zero() {
let cents_str = self.int_to_word(&right);
result.push_str(separator);
result.push_str(¢s_str);
result.push(' ');
result.push_str(if right != one {
&forms.subunit[1]
} else {
&forms.subunit[0]
});
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}