use crate::base::{KwVal, Kwargs, Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, Zero};
use std::collections::HashMap;
use std::sync::OnceLock;
const ZERO: &str = "אפס";
const AND: &str = "ו";
const DEF: &str = "ה";
const NEGWORD: &str = "מינוס";
const POINTWORD: &str = "נקודה";
const THOUSANDS1: &str = "אלף";
const THOUSANDS2: &str = "אלפיים";
const THOUSANDS3: [&str; 2] = ["אלפים", "אלפי"];
const ONES: [[&str; 8]; 10] = [
["", "", "", "", "", "", "", ""], [
"אחת",
"אחד",
"אחת",
"אחד",
"ראשונה",
"ראשון",
"ראשונות",
"ראשונים",
],
[
"שתיים",
"שניים",
"שתי",
"שני",
"שנייה",
"שני",
"שניות",
"שניים",
],
[
"שלוש",
"שלושה",
"שלוש",
"שלושת",
"שלישית",
"שלישי",
"שלישיות",
"שלישיים",
],
[
"ארבע",
"ארבעה",
"ארבע",
"ארבעת",
"רביעית",
"רביעי",
"רביעיות",
"רביעיים",
],
[
"חמש",
"חמישה",
"חמש",
"חמשת",
"חמישית",
"חמישי",
"חמישיות",
"חמישיים",
],
[
"שש",
"שישה",
"שש",
"ששת",
"שישית",
"שישי",
"שישיות",
"שישיים",
],
[
"שבע",
"שבעה",
"שבע",
"שבעת",
"שביעית",
"שביעי",
"שביעיות",
"שביעיים",
],
[
"שמונה",
"שמונה",
"שמונה",
"שמונת",
"שמינית",
"שמיני",
"שמיניות",
"שמיניים",
],
[
"תשע",
"תשעה",
"תשע",
"תשעת",
"תשיעית",
"תשיעי",
"תשיעיות",
"תשיעיים",
],
];
const TENS0: [&str; 8] = [
"עשר",
"עשרה",
"עשר",
"עשרת",
"עשירית",
"עשירי",
"עשיריות",
"עשיריים",
];
const TENS1: [&str; 2] = ["עשרה", "עשר"];
const TENS2: [&str; 2] = ["שתים עשרה", "שנים עשר"];
const TWENTIES: [&str; 10] = [
"",
"", "עשרים",
"שלושים",
"ארבעים",
"חמישים",
"שישים",
"שבעים",
"שמונים",
"תשעים",
];
const LARGE: [(&str, &str); 21] = [
("", ""), ("מיליון", "מיליוני"),
("מיליארד", "מיליארדי"),
("טריליון", "טריליוני"),
("קוודריליון", "קוודריליוני"),
("קווינטיליון", "קווינטיליוני"),
("סקסטיליון", "סקסטיליוני"),
("ספטיליון", "ספטיליוני"),
("אוקטיליון", "אוקטיליוני"),
("נוניליון", "נוניליוני"),
("דסיליון", "דסיליוני"),
("אונדסיליון", "אונדסיליוני"),
("דואודסיליון", "דואודסיליוני"),
("טרדסיליון", "טרדסיליוני"),
("קווטואורדסיליון", "קווטואורדסיליוני"),
("קווינדסיליון", "קווינדסיליוני"),
("סקסדסיליון", "סקסדסיליוני"),
("ספטנדסיליון", "ספטנדסיליוני"),
("אוקטודסיליון", "אוקטודסיליוני"),
("נובמדסיליון", "נובמדסיליוני"),
("ויגינטיליון", "ויגינטיליוני"),
];
fn hundreds(key: u32, idx: usize) -> Result<&'static str> {
let row: &[&str] = match key {
1 => &["מאה", "מאת"],
2 => &["מאתיים"],
3 => &["מאות"],
_ => return Err(N2WError::Key(format!("{}", key))),
};
row.get(idx)
.copied()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn thousands_small(key: u32) -> Result<&'static str> {
match key {
1 => Ok(THOUSANDS1),
2 => Ok(THOUSANDS2),
3 => Ok(THOUSANDS3[0]),
_ => Err(N2WError::Key(format!("{}", key))),
}
}
fn large(key: usize, idx: usize) -> Result<&'static str> {
if key == 0 || key >= LARGE.len() {
return Err(N2WError::Key(format!("{}", key)));
}
let (abs, construct) = LARGE[key];
match idx {
0 => Ok(abs),
1 => Ok(construct),
_ => Err(N2WError::Index("tuple index out of range".into())),
}
}
fn last_index(words: &[String]) -> Result<usize> {
words
.len()
.checked_sub(1)
.ok_or_else(|| N2WError::Index("list index out of range".into()))
}
fn splitbyx3(s: &str) -> Vec<u32> {
let length = s.len();
let x = 3usize;
let mut out = Vec::new();
if length > x {
let start = length % x;
if start > 0 {
out.push(s[..start].parse::<u32>().unwrap_or(0));
}
let mut i = start;
while i < length {
let end = (i + x).min(length);
out.push(s[i..end].parse::<u32>().unwrap_or(0));
i += x;
}
} else {
out.push(s.parse::<u32>().unwrap_or(0));
}
out
}
fn get_digits(x: u32) -> (u32, u32, u32) {
(x % 10, (x / 10) % 10, (x / 100) % 10)
}
#[allow(clippy::too_many_arguments)]
fn chunk2word(
n: &BigInt,
i: usize,
x: u32,
gender_m: bool,
construct: bool,
ordinal: bool,
plural: bool,
) -> Result<Vec<String>> {
let mut words: Vec<String> = Vec::new();
let (n1, n2, n3) = get_digits(x);
if n3 > 0 {
if construct && n == &BigInt::from(100) {
words.push(hundreds(n3, 1)?.to_string());
} else if n3 <= 2 {
words.push(hundreds(n3, 0)?.to_string());
} else {
words.push(format!("{} {}", ONES[n3 as usize][0], hundreds(3, 0)?));
}
}
if n2 > 1 {
words.push(TWENTIES[n2 as usize].to_string());
}
if i == 0 || x >= 11 {
let male = usize::from(gender_m || i > 0);
let cop = (2 * usize::from(construct && i == 0)
+ 4 * usize::from(ordinal)
+ 2 * usize::from(plural))
* usize::from(n < &BigInt::from(11));
if n2 == 1 {
if n1 == 0 {
words.push(TENS0[male + cop].to_string());
} else if n1 == 2 {
words.push(TENS2[male].to_string());
} else {
words.push(format!("{} {}", ONES[n1 as usize][male], TENS1[male]));
}
} else if n1 > 0 {
words.push(ONES[n1 as usize][male + cop].to_string());
}
}
let construct_last = construct && (n % BigInt::from(1000u32).pow(i as u32)).is_zero();
let cl = usize::from(construct_last);
if i == 1 {
if x >= 11 {
let last = last_index(&words)?;
words[last].push(' ');
words[last].push_str(THOUSANDS1);
} else if n1 == 0 {
words.push(format!("{} {}", TENS0[3], THOUSANDS3[cl]));
} else if n1 <= 2 {
words.push(thousands_small(n1)?.to_string());
} else {
words.push(format!("{} {}", ONES[n1 as usize][3], THOUSANDS3[cl]));
}
} else if i > 1 {
if x >= 11 {
let suffix = large(i - 1, cl)?;
let last = last_index(&words)?;
words[last].push(' ');
words[last].push_str(suffix);
} else if n1 == 0 {
words.push(format!("{} {}", TENS0[1 + 2 * cl], large(i - 1, cl)?));
} else if n1 == 1 {
words.push(large(i - 1, 0)?.to_string());
} else {
let idx = 1 + 2 * usize::from(construct_last || x == 2);
words.push(format!("{} {}", ONES[n1 as usize][idx], large(i - 1, cl)?));
}
}
Ok(words)
}
fn int2word(
n: &BigInt,
gender_m: bool,
construct: bool,
ordinal: bool,
definite: bool,
plural: bool,
) -> Result<String> {
if n >= maxval_ref() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
n,
maxval_ref()
)));
}
if n.is_zero() {
return Ok(if ordinal {
format!("{}{}", DEF, ZERO)
} else {
ZERO.to_string()
});
}
let mut words: Vec<String> = Vec::new();
let chunks = splitbyx3(&n.to_string());
let mut i = chunks.len();
for x in chunks {
i -= 1;
if x == 0 {
continue;
}
words.extend(chunk2word(n, i, x, gender_m, construct, ordinal, plural)?);
if words.len() > 1 {
let last = last_index(&words)?;
words[last].insert_str(0, AND);
}
}
if ordinal && (n >= &BigInt::from(11) || definite) {
if words.is_empty() {
return Err(N2WError::Index("list index out of range".into()));
}
words[0].insert_str(0, DEF);
}
Ok(words.join(" "))
}
fn py_float_str(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 a = f.abs();
if a != 0.0 && !(1e-4..1e16).contains(&a) {
let s = format!("{:e}", f);
if let Some(pos) = s.find('e') {
let exp: i32 = s[pos + 1..].parse().unwrap_or(0);
let sign = if exp < 0 { '-' } else { '+' };
return format!("{}e{}{:02}", &s[..pos], sign, exp.abs());
}
s
} else if f.fract() == 0.0 {
format!("{:.1}", f)
} else {
format!("{}", f)
}
}
fn float_value_str(v: &crate::floatpath::FloatValue) -> String {
match v {
FloatValue::Float { value, .. } => py_float_str(*value),
FloatValue::Decimal { value, .. } => crate::strnum::python_decimal_str(value),
}
}
fn kw_gender_m(kw: &Kwargs, default_m: bool) -> bool {
match kw.get("gender") {
None => default_m,
Some(KwVal::Str(s)) => s == "m",
Some(_) => false,
}
}
fn kw_flag(kw: &Kwargs, key: &str) -> Option<bool> {
match kw.get(key) {
None => Some(false),
Some(KwVal::Bool(b)) => Some(*b),
Some(_) => None,
}
}
fn maxval_ref() -> &'static BigInt {
static MAXVAL: OnceLock<BigInt> = OnceLock::new();
MAXVAL.get_or_init(|| BigInt::from(10u32).pow(66))
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const CENTS: [&str; 2] = ["סנט", "סנטים"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"ILS",
CurrencyForms::new(&["שקל", "שקלים"], &["אגורה", "אגורות"]),
);
m.insert("EUR", CurrencyForms::new(&["אירו", "אירו"], &CENTS));
m.insert("USD", CurrencyForms::new(&["דולר", "דולרים"], &CENTS));
m
}
const ZERO_PATTERNS: [&str; 21] = [
"zero cent",
"nul cent",
"null cent",
"sıfır kuruş",
"אפס אגורות",
"zero sen",
"ศูนย์สตางค์",
"không xu",
"शून्य पैसे",
"শূন্য পয়সা",
"nula lipa",
"нула пара",
"ноль копеек",
"нула стотинки",
"零分",
"ዜሮ ሳንቲም",
"صفر",
"sero sent",
"dim ceiniog",
"ნულოვანი თეთრი",
"нула стотинки",
];
const CONJUNCTIONS: [&str; 24] = [
"and",
"və",
"և",
"და",
"ir",
"და",
"და",
"و",
"و",
"与",
"ja",
"और",
"এবং",
"i",
"и",
"и",
"と",
"그리고",
"และ",
"và",
"dan",
"a",
"e",
"და",
];
fn simple_lower(c: char) -> char {
let mut it = c.to_lowercase();
match (it.next(), it.next()) {
(Some(l), None) => l,
_ => c,
}
}
fn match_ci_at(hay: &str, at: usize, needle: &str) -> Option<usize> {
let mut h = hay[at..].chars();
let mut consumed = 0usize;
for nc in needle.chars() {
let hc = h.next()?;
if simple_lower(hc) != simple_lower(nc) {
return None;
}
consumed += hc.len_utf8();
}
Some(consumed)
}
fn find_ci(hay: &str, needle: &str, from: usize) -> Option<(usize, usize)> {
let mut i = from;
while i <= hay.len() {
if hay.is_char_boundary(i) {
if let Some(len) = match_ci_at(hay, i, needle) {
return Some((i, i + len));
}
}
i += 1;
}
None
}
fn skip_ws1(hay: &str, at: usize) -> Option<usize> {
let mut i = at;
for c in hay[at..].chars() {
if !c.is_whitespace() {
break;
}
i += c.len_utf8();
}
if i > at {
Some(i)
} else {
None
}
}
fn remove_all_ci(hay: &str, needle: &str) -> String {
if needle.is_empty() {
return hay.to_string();
}
let mut out = String::with_capacity(hay.len());
let mut i = 0usize;
while let Some((s, e)) = find_ci(hay, needle, i) {
out.push_str(&hay[i..s]);
i = e;
}
out.push_str(&hay[i..]);
out
}
fn match_conjoined(hay: &str, at: usize, pattern: &str) -> Option<usize> {
let a = skip_ws1(hay, at)?;
for conj in CONJUNCTIONS {
let Some(clen) = match_ci_at(hay, a, conj) else {
continue;
};
let Some(b) = skip_ws1(hay, a + clen) else {
continue;
};
if let Some(plen) = match_ci_at(hay, b, pattern) {
return Some(b + plen);
}
}
None
}
fn remove_conjoined(hay: &str, pattern: &str) -> String {
let mut out = String::with_capacity(hay.len());
let mut i = 0usize;
let mut cut = 0usize;
while i < hay.len() {
if !hay.is_char_boundary(i) {
i += 1;
continue;
}
if let Some(end) = match_conjoined(hay, i, pattern) {
out.push_str(&hay[cut..i]);
cut = end;
i = end;
continue;
}
i += 1;
}
out.push_str(&hay[cut..]);
out
}
fn strip_zero_cents(result: &str) -> String {
let mut result = result.to_string();
for pattern in ZERO_PATTERNS {
if find_ci(&result, pattern, 0).is_some() {
result = remove_conjoined(&result, pattern);
result = remove_all_ci(&result, pattern);
result = result.split_whitespace().collect::<Vec<_>>().join(" ");
}
}
result.trim().to_string()
}
pub struct LangHe {
#[allow(dead_code)]
makaf: String,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangHe {
fn default() -> Self {
Self::new()
}
}
impl LangHe {
pub fn new() -> Self {
LangHe {
makaf: "-".to_string(),
currency_forms: build_currency_forms(),
}
}
}
impl Lang for LangHe {
fn default_currency(&self) -> &str {
"ILS"
}
fn default_separator(&self) -> &str {
"ו"
}
fn maxval(&self) -> &BigInt {
maxval_ref()
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"נקודה"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let mut out = String::new();
let mut v = value.clone();
if v.is_negative() {
v = v.abs();
out = format!("{} ", NEGWORD.trim());
}
if &v >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
v,
self.maxval()
)));
}
Ok(format!(
"{}{}",
out,
int2word(&v, false, false, false, false, false)?
))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
if value >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
value,
self.maxval()
)));
}
int2word(value, true, false, true, false, false)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { .. } => {
crate::floatpath::default_to_cardinal_float(self, value, None)
}
FloatValue::Decimal { value: d, .. } => {
crate::floatpath::cardinal_from_bigdecimal(self, d)
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if let FloatValue::Float { value: f, .. } = value {
if f.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
if f.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".into(),
));
}
}
let Some(n) = value.as_whole_int() else {
return Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
float_value_str(value)
)));
};
if n.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
float_value_str(value)
)));
}
if &n >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
float_value_str(value),
self.maxval()
)));
}
int2word(&n, true, false, true, false, false)
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["gender", "construct"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let gender_m = kw_gender_m(kw, false);
let Some(construct) = kw_flag(kw, "construct") else {
return Err(N2WError::Fallback("kwargs".into()));
};
let mut out = String::new();
let mut v = value.clone();
if v.is_negative() {
v = v.abs();
out = format!("{} ", NEGWORD.trim());
}
if &v >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
v,
self.maxval()
)));
}
Ok(format!(
"{}{}",
out,
int2word(&v, gender_m, construct, false, false, false)?
))
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["gender", "definite", "plural"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let gender_m = kw_gender_m(kw, true);
let (Some(definite), Some(plural)) = (kw_flag(kw, "definite"), kw_flag(kw, "plural"))
else {
return Err(N2WError::Fallback("kwargs".into()));
};
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
if value >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
value,
self.maxval()
)));
}
int2word(value, gender_m, false, true, definite, plural)
}
fn to_currency_kw(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
kw: &Kwargs,
) -> Result<String> {
if !kw.only(&["prefer_singular", "prefer_singular_cents"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
self.to_currency(val, currency, cents, separator, adjective)
}
fn lang_name(&self) -> &str {
"Num2Word_HE"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let form = if n.is_one() { 0 } else { 1 };
forms
.get(form)
.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> {
let separator = separator.unwrap_or(self.default_separator());
let CurrencyValue::Int(v) = val else {
return crate::currency::default_to_currency(
self, val, currency, cents, separator, adjective,
);
};
let as_float = CurrencyValue::Decimal {
value: BigDecimal::from(v.clone()),
has_decimal: true,
is_float: true,
};
let result = crate::currency::default_to_currency(
self, &as_float, currency, cents, separator, adjective,
)?;
Ok(strip_zero_cents(&result))
}
}