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 num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "negatîv ";
const ONES: [&str; 10] = [
"", "yek", "du", "sê", "çar", "pênc", "şeş", "heft", "heşt", "neh",
];
const TEENS: [&str; 10] = [
"deh", "yanzdeh", "donzdeh", "sêzdeh", "çardeh", "panzdeh", "şanzdeh", "hivdeh", "hijdeh",
"nozdeh",
];
const TENS: [&str; 10] = [
"", "deh", "bîst", "sî", "çil", "pêncî", "şêst", "heftê", "heştê", "not",
];
const HUNDRED: &str = "sed";
const THOUSAND: &str = "hezar";
const MILLION: &str = "milyon";
const ZERO: &str = "sifir";
const AND: &str = " û ";
const FALLBACK_CURRENCY: &str = "TRY";
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("TRY", CurrencyForms::new(&["lîre", "lîre"], &["qurûş", "qurûş"]));
m.insert("IQD", CurrencyForms::new(&["dînar", "dînar"], &["fils", "fils"]));
m.insert("USD", CurrencyForms::new(&["dolar", "dolar"], &["sent", "sent"]));
m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &["sent", "sent"]));
m
}
pub struct LangKu {
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_fallback: CurrencyForms,
}
impl Default for LangKu {
fn default() -> Self {
Self::new()
}
}
impl LangKu {
pub fn new() -> Self {
let currency_forms = build_currency_forms();
let currency_fallback = currency_forms[FALLBACK_CURRENCY].clone();
LangKu {
currency_forms,
currency_fallback,
}
}
fn currency_parts(&self, val: &CurrencyValue) -> Result<(BigInt, u64)> {
let value = match val {
CurrencyValue::Int(v) => return Ok((v.abs(), 0)),
CurrencyValue::Decimal { value, .. } => value,
};
let (digits, scale) = value.abs().as_bigint_and_exponent();
if scale < 0 {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: {} carries an exponent \
in Python's str() form",
value
)));
}
if scale == 0 {
return Ok((digits, 0));
}
let scale = scale as usize;
let s = digits.to_string();
let padded = if s.len() <= scale {
format!("{}{}", "0".repeat(scale + 1 - s.len()), s)
} else {
s
};
let (part0, part1) = padded.split_at(padded.len() - scale);
let left = BigInt::from_str(part0)
.map_err(|e| N2WError::Value(e.to_string()))?;
let mut frac: String = part1.chars().take(2).collect();
while frac.len() < 2 {
frac.push('0');
}
let right = frac
.parse::<u64>()
.map_err(|e| N2WError::Value(e.to_string()))?;
Ok((left, right))
}
fn int_to_word(&self, number: &BigInt) -> String {
let billion = BigInt::from(1_000_000_000u64);
if *number >= billion {
return number.to_string();
}
let n = number
.to_u64()
.expect("0 <= number < 10^9 is representable as u64");
self.int_to_word_small(n)
}
fn int_to_word_small(&self, number: u64) -> String {
if number == 0 {
return ZERO.to_string();
}
if number < 10 {
return ONES[number as usize].to_string();
}
if number < 20 {
return TEENS[(number - 10) as usize].to_string();
}
if number < 100 {
let (t, o) = (number / 10, number % 10);
let mut s = TENS[t as usize].to_string();
if o != 0 {
s.push_str(AND);
s.push_str(ONES[o as usize]);
}
return s;
}
if number < 1_000 {
let (h, r) = (number / 100, number % 100);
let mut s = String::new();
if h > 1 {
s.push_str(ONES[h as usize]);
s.push(' ');
}
s.push_str(HUNDRED);
if r != 0 {
s.push_str(AND);
s.push_str(&self.int_to_word_small(r));
}
return s;
}
if number < 1_000_000 {
let (t, r) = (number / 1_000, number % 1_000);
let mut s = self.int_to_word_small(t);
s.push(' ');
s.push_str(THOUSAND);
if r != 0 {
s.push_str(AND);
s.push_str(&self.int_to_word_small(r));
}
return s;
}
let (m, r) = (number / 1_000_000, number % 1_000_000);
let mut s = self.int_to_word_small(m);
s.push(' ');
s.push_str(MILLION);
if r != 0 {
s.push_str(AND);
s.push_str(&self.int_to_word_small(r));
}
s
}
fn cardinal(&self, value: &BigInt) -> String {
if value.is_negative() {
let inner = self.cardinal(&value.abs());
return format!("{}{}", NEGWORD, inner).trim().to_string();
}
self.int_to_word(value)
}
fn cardinal_float(&self, value: f64, precision: u32) -> Result<String> {
let neg = value.is_sign_negative();
let a = value.abs();
if !a.is_finite() || (a != 0.0 && (a < 1e-4 || a >= 1e16)) {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: str({}) is not a plain decimal",
value
)));
}
let s = format!("{}", a);
let (int_str, frac_str): (String, String) = match s.split_once('.') {
Some((i, f)) => (i.to_string(), f.to_string()),
None => (s, "0".repeat(precision as usize)),
};
let left = BigInt::from_str(&int_str).map_err(|e| N2WError::Value(e.to_string()))?;
let mut ret = self.int_to_word(&left);
if !frac_str.is_empty() {
ret.push(' ');
ret.push_str(self.pointword());
for ch in frac_str.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("non-digit {:?} in fractional part", ch))
})?;
ret.push(' ');
ret.push_str(if d == 0 { ZERO } else { ONES[d as usize] });
}
}
Ok(if neg {
format!("{}{}", NEGWORD, ret).trim().to_string()
} else {
ret.trim().to_string()
})
}
}
fn decimal_sci_value_error(s: &str) -> N2WError {
let n = s.strip_prefix('-').unwrap_or(s);
let lit = if n.contains('.') { "E" } else { n };
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
lit
))
}
impl Lang for LangKu {
fn default_currency(&self) -> &str {
"TRY"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"xal"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
Ok(self.cardinal(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value == &BigInt::from(1u32) {
return Ok("yekem".to_string());
}
Ok(format!("{}em", self.cardinal(value)))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}em", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
Ok(self.cardinal(value))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value, precision } => self.cardinal_float(*value, *precision),
FloatValue::Decimal { .. } => {
crate::floatpath::default_to_cardinal_float(self, value, None)
}
}
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float {
value: f,
precision,
} => self.cardinal_float(*f, *precision),
FloatValue::Decimal { value: d, .. } => {
let s = python_decimal_str(d);
if s.contains('E') {
return Err(decimal_sci_value_error(&s));
}
if s.contains('.') {
return self.to_cardinal_float(value, precision_override);
}
let i = value
.as_whole_int()
.expect("a point-free, non-scientific Decimal is whole");
Ok(self.cardinal(&i))
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if value.as_whole_int() == Some(BigInt::from(1u32)) {
return Ok("yekem".to_string());
}
Ok(format!("{}em", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}em", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.cardinal_float_entry(value, None)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
ParsedNumber::NaN => Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".into(),
)),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_KU"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
if forms.is_empty() {
return Ok(String::new());
}
Ok(if n.is_one() {
forms[0].clone()
} else {
forms[forms.len() - 1].clone()
})
}
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 (left, right) = self.currency_parts(val)?;
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.currency_fallback);
let unit = if left.is_one() {
forms.unit.first()
} else {
forms.unit.get(1)
}
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
let mut result = format!("{} {}", self.int_to_word(&left), unit);
if cents && right != 0 {
let subunit = if right == 1 {
forms.subunit.first()
} else {
forms.subunit.get(1)
}
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
result.push_str(separator);
result.push_str(&format!("{} {}", self.int_to_word_small(right), subunit));
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use bigdecimal::BigDecimal;
fn ku() -> LangKu {
LangKu::new()
}
fn int(s: &str) -> CurrencyValue {
CurrencyValue::parse(s, true, false, false).unwrap()
}
fn flt(s: &str) -> CurrencyValue {
CurrencyValue::parse(s, false, true, true).unwrap()
}
fn cur(l: &LangKu, v: &CurrencyValue, code: &str) -> String {
l.to_currency(v, code, true, None, false).unwrap()
}
#[test]
fn corpus_currency_rows() {
let l = ku();
assert_eq!(cur(&l, &int("0"), "EUR"), "sifir euro");
assert_eq!(cur(&l, &int("1"), "EUR"), "yek euro");
assert_eq!(cur(&l, &int("2"), "EUR"), "du euro");
assert_eq!(cur(&l, &int("100"), "EUR"), "sed euro");
assert_eq!(cur(&l, &flt("12.34"), "EUR"), "donzdeh euro sî û çar sent");
assert_eq!(cur(&l, &flt("0.01"), "EUR"), "sifir euro yek sent");
assert_eq!(cur(&l, &flt("1.0"), "EUR"), "yek euro");
assert_eq!(cur(&l, &flt("99.99"), "EUR"), "not û neh euro not û neh sent");
assert_eq!(
cur(&l, &flt("1234.56"), "EUR"),
"yek hezar û du sed û sî û çar euro pêncî û şeş sent"
);
assert_eq!(cur(&l, &flt("-12.34"), "EUR"), "negatîv donzdeh euro sî û çar sent");
assert_eq!(cur(&l, &int("1000000"), "EUR"), "yek milyon euro");
assert_eq!(cur(&l, &flt("0.5"), "EUR"), "sifir euro pêncî sent");
assert_eq!(cur(&l, &int("0"), "USD"), "sifir dolar");
assert_eq!(cur(&l, &flt("12.34"), "USD"), "donzdeh dolar sî û çar sent");
assert_eq!(cur(&l, &flt("-12.34"), "USD"), "negatîv donzdeh dolar sî û çar sent");
assert_eq!(cur(&l, &int("1000000"), "USD"), "yek milyon dolar");
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
assert_eq!(cur(&l, &int("0"), code), "sifir lîre", "{code}");
assert_eq!(cur(&l, &int("1"), code), "yek lîre", "{code}");
assert_eq!(cur(&l, &int("2"), code), "du lîre", "{code}");
assert_eq!(cur(&l, &int("100"), code), "sed lîre", "{code}");
assert_eq!(cur(&l, &flt("12.34"), code), "donzdeh lîre sî û çar qurûş", "{code}");
assert_eq!(cur(&l, &flt("0.01"), code), "sifir lîre yek qurûş", "{code}");
assert_eq!(cur(&l, &flt("1.0"), code), "yek lîre", "{code}");
assert_eq!(
cur(&l, &flt("99.99"), code),
"not û neh lîre not û neh qurûş",
"{code}"
);
assert_eq!(
cur(&l, &flt("1234.56"), code),
"yek hezar û du sed û sî û çar lîre pêncî û şeş qurûş",
"{code}"
);
assert_eq!(
cur(&l, &flt("-12.34"), code),
"negatîv donzdeh lîre sî û çar qurûş",
"{code}"
);
assert_eq!(cur(&l, &int("1000000"), code), "yek milyon lîre", "{code}");
assert_eq!(cur(&l, &flt("0.5"), code), "sifir lîre pêncî qurûş", "{code}");
}
assert_eq!(cur(&l, &flt("1.234"), "KWD"), "yek lîre bîst û sê qurûş");
}
#[test]
fn corpus_cheque_rows() {
let l = ku();
let v = BigDecimal::from_str("1234.56").unwrap();
assert_eq!(
l.to_cheque(&v, "EUR").unwrap(),
"YEK HEZAR Û DU SED Û SÎ Û ÇAR AND 56/100 EURO"
);
assert_eq!(
l.to_cheque(&v, "USD").unwrap(),
"YEK HEZAR Û DU SED Û SÎ Û ÇAR AND 56/100 DOLAR"
);
for code in ["GBP", "JPY", "KWD", "BHD", "INR", "CNY", "CHF"] {
match l.to_cheque(&v, code) {
Err(N2WError::NotImplemented(m)) => assert_eq!(
m,
format!("Currency code \"{code}\" not implemented for \"Num2Word_KU\"")
),
other => panic!("{code}: expected NotImplemented, got {other:?}"),
}
}
}
#[test]
fn cents_truncate_and_pad() {
let l = ku();
assert_eq!(cur(&l, &flt("12.345"), "USD"), "donzdeh dolar sî û çar sent");
assert_eq!(cur(&l, &flt("12.305"), "USD"), "donzdeh dolar sî sent");
assert_eq!(cur(&l, &flt("12.005"), "USD"), "donzdeh dolar");
assert_eq!(cur(&l, &flt("1.5"), "USD"), "yek dolar pêncî sent");
assert_eq!(cur(&l, &flt("0.099"), "USD"), "sifir dolar neh sent");
assert_eq!(cur(&l, &flt("2.999"), "USD"), "du dolar not û neh sent");
}
#[test]
fn currency_quirks() {
let l = ku();
assert_eq!(
l.to_currency(&flt("12.34"), "USD", false, None, false).unwrap(),
"donzdeh dolar"
);
assert_eq!(
l.to_currency(&flt("12.34"), "USD", true, None, true).unwrap(),
"donzdeh dolar sî û çar sent"
);
assert_eq!(cur(&l, &int("1000000000"), "USD"), "1000000000 dolar");
assert_eq!(
cur(&l, &flt("1234567890.12"), "USD"),
"1234567890 dolar donzdeh sent"
);
assert_eq!(cur(&l, &flt("-0.0"), "USD"), "sifir dolar");
assert_eq!(cur(&l, &flt("-0.5"), "USD"), "negatîv sifir dolar pêncî sent");
assert_eq!(cur(&l, &int("-1"), "USD"), "negatîv yek dolar");
assert_eq!(
l.to_currency(&flt("12.34"), "USD", true, Some(","), false).unwrap(),
"donzdeh dolar,sî û çar sent"
);
}
fn prec(v: f64) -> u32 {
match format!("{}", v.abs()).split_once('.') {
Some((_, f)) => f.len() as u32,
None => 1,
}
}
fn cf(l: &LangKu, v: f64) -> String {
l.to_cardinal_float(&FloatValue::Float { value: v, precision: prec(v) }, None)
.unwrap()
}
#[test]
fn corpus_cardinal_float_rows() {
let l = ku();
assert_eq!(cf(&l, 0.0), "sifir xal sifir");
assert_eq!(cf(&l, 0.5), "sifir xal pênc");
assert_eq!(cf(&l, 1.0), "yek xal sifir");
assert_eq!(cf(&l, 1.5), "yek xal pênc");
assert_eq!(cf(&l, 2.25), "du xal du pênc");
assert_eq!(cf(&l, 3.14), "sê xal yek çar");
assert_eq!(cf(&l, 0.01), "sifir xal sifir yek");
assert_eq!(cf(&l, 0.1), "sifir xal yek");
assert_eq!(cf(&l, 0.99), "sifir xal neh neh");
assert_eq!(cf(&l, 1.01), "yek xal sifir yek");
assert_eq!(cf(&l, 12.34), "donzdeh xal sê çar");
assert_eq!(cf(&l, 99.99), "not û neh xal neh neh");
assert_eq!(cf(&l, 100.5), "sed xal pênc");
assert_eq!(cf(&l, 1234.56), "yek hezar û du sed û sî û çar xal pênc şeş");
assert_eq!(cf(&l, -0.5), "negatîv sifir xal pênc");
assert_eq!(cf(&l, -1.5), "negatîv yek xal pênc");
assert_eq!(cf(&l, -12.34), "negatîv donzdeh xal sê çar");
assert_eq!(cf(&l, 1.005), "yek xal sifir sifir pênc");
assert_eq!(cf(&l, 2.675), "du xal şeş heft pênc");
}
#[test]
fn cardinal_float_edges() {
let l = ku();
assert_eq!(cf(&l, -0.0), "negatîv sifir xal sifir");
assert_eq!(cf(&l, 1234567890.12), "1234567890 xal yek du");
for v in [1e16, 1.5e16, 1e-5, f64::INFINITY, f64::NAN] {
assert!(
matches!(
l.to_cardinal_float(&FloatValue::Float { value: v, precision: 1 }, None),
Err(N2WError::Value(_))
),
"{v} should raise ValueError"
);
}
assert_eq!(cf(&l, 1e-4), "sifir xal sifir sifir sifir yek");
}
#[test]
fn corpus_cardinal_dec_rows() {
let l = ku();
let dec = |s: &str| {
let precision = s.split_once('.').map(|(_, f)| f.len() as u32).unwrap_or(0);
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
}
};
let d = |s: &str| l.to_cardinal_float(&dec(s), None).unwrap();
assert_eq!(d("0.01"), "sifir xal sifir yek");
assert_eq!(d("1.10"), "yek xal yek sifir");
assert_eq!(d("12.345"), "donzdeh xal sê çar pênc");
assert_eq!(d("98746251323029.99"), "98746251323029 xal neh neh");
assert_eq!(d("0.001"), "sifir xal sifir sifir yek");
}
#[test]
fn pluralize_takes_last_form() {
let l = ku();
let three: Vec<String> = ["a", "b", "c"].iter().map(|s| s.to_string()).collect();
assert_eq!(l.pluralize(&BigInt::from(1), &three).unwrap(), "a");
assert_eq!(l.pluralize(&BigInt::from(2), &three).unwrap(), "c");
assert_eq!(l.pluralize(&BigInt::from(2), &[]).unwrap(), "");
}
#[test]
fn exponent_form_raises_value_error() {
let l = ku();
assert!(matches!(
l.to_currency(&flt("1e+21"), "USD", true, None, false),
Err(N2WError::Value(_))
));
assert_eq!(cur(&l, &flt("5"), "USD"), "pênc dolar");
}
}