use crate::currency::vocab::HE as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{Number, parse_cardinal, parse_currency, parse_ordinal, split_groups};
use crate::options::options;
use crate::{Error, Result};
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
const ONES: [&str; 10] = [
"",
"אחת",
"שתים",
"שלש",
"ארבע",
"חמש",
"שש",
"שבע",
"שמונה",
"תשע",
];
const TEENS: [&str; 10] = [
"עשר",
"אחת עשרה",
"שתים עשרה",
"שלש עשרה",
"ארבע עשרה",
"חמש עשרה",
"שש עשרה",
"שבע עשרה",
"שמונה עשרה",
"תשע עשרה",
];
const TENS: [&str; 10] = [
"",
"",
"עשרים",
"שלשים",
"ארבעים",
"חמישים",
"ששים",
"שבעים",
"שמונים",
"תשעים",
];
const HUNDREDS: [&str; 10] = [
"",
"מאה",
"מאתיים",
"שלש מאות",
"ארבע מאות",
"חמש מאות",
"שש מאות",
"שבע מאות",
"שמונה מאות",
"תשע מאות",
];
const THOUSANDS_SPECIAL: [&str; 10] = [
"",
"אלף",
"אלפיים",
"שלשת אלפים",
"ארבעת אלפים",
"חמשת אלפים",
"ששת אלפים",
"שבעת אלפים",
"שמונת אלפים",
"תשעת אלפים",
];
const SCALE: &[&str] = &[
"",
"אלף",
"מיליון",
"מיליארד",
"טריליון",
"קוודרליון",
"קווינטיליון",
];
const SCALE_PLURAL: &[&str] = &[
"",
"אלפים",
"מיליונים",
"מיליארדים",
"טריליונים",
"קוודרליונים",
"קווינטיליונים",
];
pub const CARDINAL_MAX: Max = Max::western(SCALE.len() as u32 - 1);
pub const ORDINAL_MAX: Max = Max::bounded(9);
pub const CURRENCY_MAX: Max = Max::western(SCALE.len() as u32 - 1);
const ZERO: &str = "אפס";
const NEGATIVE: &str = "מינוס";
const DECIMAL_SEP: &str = "נקודה";
const AND_WORD: &str = "ו";
const ONES_MASC: [&str; 10] = [
"",
"אחד",
"שניים",
"שלשה",
"ארבעה",
"חמשה",
"ששה",
"שבעה",
"שמונה",
"תשעה",
];
const ORDINAL_ONES: [&str; 10] = [
"",
"ראשון",
"שני",
"שלישי",
"רביעי",
"חמישי",
"שישי",
"שביעי",
"שמיני",
"תשיעי",
];
const ORDINAL_TEENS: [&str; 10] = [
"עשירי",
"אחד עשר",
"שנים עשר",
"שלשה עשר",
"ארבעה עשר",
"חמשה עשר",
"ששה עשר",
"שבעה עשר",
"שמונה עשר",
"תשעה עשר",
];
const ORDINAL_TENS: [&str; 10] = [
"",
"",
"עשרים",
"שלשים",
"ארבעים",
"חמישים",
"ששים",
"שבעים",
"שמונים",
"תשעים",
];
const ORDINAL_HUNDRED: &str = "מאה";
fn append(result: &mut String, joiner: &str, word: &str) {
if !result.is_empty() {
result.push_str(joiner);
}
result.push_str(word);
}
fn build_scale_segment(n: u32, and_word: &str) -> String {
if n == 0 {
return String::new();
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let mut result = String::new();
if hundreds > 0 {
result.push_str(HUNDREDS[hundreds]);
}
let joiner = format!(" {and_word}");
if tens == 1 {
append(&mut result, &joiner, TEENS[ones]);
} else {
if tens >= 2 {
append(&mut result, &joiner, TENS[tens]);
}
if ones > 0 {
append(&mut result, &joiner, ONES[ones]);
}
}
result
}
fn build_units_segment(n: u32, and_word: &str) -> String {
if n == 0 {
return String::new();
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let mut result = String::new();
if hundreds > 0 {
result.push_str(HUNDREDS[hundreds]);
}
if tens == 1 {
append(&mut result, " ", TEENS[ones]);
} else {
if tens >= 2 {
append(&mut result, " ", TENS[tens]);
}
if ones > 0 {
append(&mut result, &format!(" {and_word}"), ONES[ones]);
}
}
result
}
fn integer_to_words(n: &BigUint, and_word: &str) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_units_segment(small as u32, and_word);
}
}
let segments = split_groups(n, 1000);
let mut result = String::new();
for (i, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if i == 0 {
let word = build_units_segment(segment, and_word);
if result.is_empty() {
result = word;
} else {
result.push(' ');
if segment <= 9 {
result.push_str(and_word);
}
result.push_str(&word);
}
} else if i == 1 {
if !result.is_empty() {
result.push(' ');
}
if segment <= 9 {
result.push_str(THOUSANDS_SPECIAL[segment as usize]);
} else {
result.push_str(&build_scale_segment(segment, and_word));
result.push(' ');
result.push_str(SCALE[1]);
}
} else {
if !result.is_empty() {
result.push(' ');
}
if segment == 1 {
result.push_str(SCALE[i]);
} else {
result.push_str(&build_scale_segment(segment, and_word));
result.push(' ');
result.push_str(SCALE_PLURAL[i]);
}
}
}
result
}
fn decimal_part_to_words(decimal: &str) -> String {
let mut result = String::new();
for digit in decimal.chars() {
let d = digit.to_digit(10).unwrap_or(0) as usize;
append(&mut result, " ", if d == 0 { ZERO } else { ONES[d] });
}
result
}
options! {
pub struct CardinalOptions {
and_word: String = AND_WORD.to_string();
}
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_cardinal_with(value, &CardinalOptions::default())
}
pub fn to_cardinal_with<'a>(
value: impl Into<Number<'a>>,
options: &CardinalOptions,
) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, None)?;
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer, &options.and_word));
if let Some(fraction) = &number.fraction {
result.push(' ');
result.push_str(DECIMAL_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction));
}
Ok(result)
}
fn build_ordinal_tens_ones(n: u32) -> String {
let n = n as usize;
if n == 0 {
return String::new();
}
if n < 10 {
return ORDINAL_ONES[n].to_string();
}
if n < 20 {
return ORDINAL_TEENS[n - 10].to_string();
}
let ones = n % 10;
let tens = n / 10;
let last = if ones == 0 {
ORDINAL_ONES[1]
} else {
ORDINAL_ONES[ones]
};
format!("{} ו{last}", ORDINAL_TENS[tens])
}
fn integer_to_ordinal(n: u64) -> String {
if n == 0 {
return String::new();
}
if n == 1 {
return ORDINAL_ONES[1].to_string();
}
if n < 100 {
return build_ordinal_tens_ones(n as u32);
}
if n < 1000 {
let hundreds = (n / 100) as usize;
let remainder = (n % 100) as u32;
if remainder == 0 {
let word = if hundreds == 1 {
ORDINAL_HUNDRED
} else {
HUNDREDS[hundreds]
};
return word.to_string();
}
return format!(
"{} {}",
HUNDREDS[hundreds],
build_ordinal_tens_ones(remainder)
);
}
if n < 1_000_000 {
let thousands = (n / 1000) as u32;
let remainder = (n % 1000) as u32;
let mut result = if thousands <= 9 {
THOUSANDS_SPECIAL[thousands as usize].to_string()
} else {
format!("{} {}", build_scale_segment(thousands, AND_WORD), SCALE[1])
};
if remainder == 0 {
return result;
}
if remainder < 100 {
result.push(' ');
result.push_str(&build_ordinal_tens_ones(remainder));
return result;
}
let rem_hundreds = (remainder / 100) as usize;
let rem_tens_ones = remainder % 100;
result.push(' ');
result.push_str(HUNDREDS[rem_hundreds]);
if rem_tens_ones > 0 {
result.push(' ');
result.push_str(&build_ordinal_tens_ones(rem_tens_ones));
}
return result;
}
let millions = (n / 1_000_000) as u32;
let remainder = n % 1_000_000;
let mut result = if millions == 1 {
SCALE[2].to_string()
} else {
format!(
"{} {}",
build_scale_segment(millions, AND_WORD),
SCALE_PLURAL[2]
)
};
if remainder > 0 {
result.push(' ');
result.push_str(&integer_to_ordinal(remainder));
}
result
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
let n = n.to_u64().ok_or(Error::OutOfRange { max: ORDINAL_MAX })?;
Ok(integer_to_ordinal(n))
}
fn build_currency_segment(n: u32) -> String {
if n == 0 {
return String::new();
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let mut result = String::new();
if hundreds > 0 {
result.push_str(HUNDREDS[hundreds]);
}
if tens == 1 {
append(&mut result, " ", TEENS[ones]);
} else {
if tens >= 2 {
append(&mut result, " ", TENS[tens]);
}
if ones > 0 {
append(&mut result, " ו", ONES_MASC[ones]);
}
}
result
}
fn integer_to_currency_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_currency_segment(small as u32);
}
}
integer_to_words(n, AND_WORD)
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::ILS, table = CURRENCY_VOCAB;
}
}
pub fn to_currency<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_currency_with(value, &CurrencyOptions::default())
}
pub fn to_currency_with<'a>(
value: impl Into<Number<'a>>,
options: &CurrencyOptions,
) -> Result<String> {
let words = CURRENCY_VOCAB.words(options.currency)?;
let money = parse_currency(&value.into(), options.currency.minor_unit_digits())?;
CURRENCY_MAX.check(&money.major, None)?;
assert_minor_unit(money.minor, options.currency)?;
let mut parts: Vec<String> = Vec::new();
if money.negative {
parts.push(NEGATIVE.to_string());
}
if !money.major.is_zero() || money.minor == 0 {
parts.push(match money.major.to_u64() {
Some(1) => format!("{} {}", words.major_at(0), ONES_MASC[1]),
Some(2) => format!("{} {}", ONES_MASC[2], words.major_at(1)),
_ => format!(
"{} {}",
integer_to_currency_words(&money.major),
words.major_at(1)
),
});
}
if money.minor > 0 {
parts.push(if money.minor == 1 {
format!("{} {}", words.minor_at(0), ONES[1])
} else {
format!(
"{} {}",
integer_to_words(&BigUint::from(money.minor), AND_WORD),
words.minor_at(1)
)
});
}
Ok(parts.join(" "))
}
pub static LANGUAGE: Language = Language {
code: "he-IL",
name: "Hebrew (Israel)",
kind: Kind::Full,
cardinal: Some(form!(with_options to_cardinal_with, CardinalOptions, CARDINAL_MAX)),
ordinal: Some(form!(plain to_ordinal, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};