use crate::currency::vocab::AR as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{
Number, digits_to_biguint, parse_cardinal, parse_currency, parse_ordinal, split_groups,
};
use crate::options::{Gender, option_enum, options};
use crate::{Error, Result};
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
use std::borrow::Cow;
const ONES_MASC: [&str; 20] = [
"",
"واحد",
"اثنان",
"ثلاثة",
"أربعة",
"خمسة",
"ستة",
"سبعة",
"ثمانية",
"تسعة",
"عشرة",
"أحد عشر",
"اثنا عشر",
"ثلاثة عشر",
"أربعة عشر",
"خمسة عشر",
"ستة عشر",
"سبعة عشر",
"ثمانية عشر",
"تسعة عشر",
];
const ONES_FEM: [&str; 20] = [
"",
"واحدة",
"اثنتان",
"ثلاث",
"أربع",
"خمس",
"ست",
"سبع",
"ثمان",
"تسع",
"عشر",
"إحدى عشرة",
"اثنتا عشرة",
"ثلاث عشرة",
"أربع عشرة",
"خمس عشرة",
"ست عشرة",
"سبع عشرة",
"ثماني عشرة",
"تسع عشرة",
];
const fn one_in_compound(gender: Gender) -> &'static str {
match gender {
Gender::Masculine => "واحد",
Gender::Feminine => "إحدى",
}
}
const EIGHT_FEM_CONSTRUCT: &str = "ثماني";
const fn teen_ten(gender: Gender) -> &'static str {
match gender {
Gender::Masculine => "عشر",
Gender::Feminine => "عشرة",
}
}
const TENS_STEM: [&str; 10] = [
"", "", "عشر", "ثلاث", "أربع", "خمس", "ست", "سبع", "ثمان", "تسع",
];
const fn tens_ending(case: Case) -> &'static str {
match case {
Case::Nominative => "ون",
Case::Accusative | Case::Genitive => "ين",
}
}
const HUNDREDS_UNIT: [&str; 10] = ["", "", "", "ثلاث", "أربع", "خمس", "ست", "سبع", "ثمان", "تسع"];
const SCALES: &[&str] = &[
"",
"ألف",
"مليون",
"مليار",
"تريليون",
"كوادريليون",
"كوينتليون",
"سكستيليون",
];
const SCALE_PLURALS: &[&str] = &[
"",
"آلاف",
"ملايين",
"مليارات",
"تريليونات",
"كوادريليونات",
"كوينتليونات",
"سكستيليونات",
];
pub const CARDINAL_MAX: Max = Max::western(SCALES.len() as u32 - 1);
pub const ORDINAL_MAX: Max = Max::western(SCALES.len() as u32 - 1);
pub const CURRENCY_MAX: Max = Max::western(SCALES.len() as u32 - 1);
const ZERO: &str = "صفر";
const NEGATIVE: &str = "ناقص";
const DECIMAL_SEP: &str = "فاصلة";
const AND: &str = "و";
const AFTER: &str = "بعد";
const DEFINITE: &str = "ال";
const ORDINAL_MASC: [&str; 11] = [
"",
"الأول",
"الثاني",
"الثالث",
"الرابع",
"الخامس",
"السادس",
"السابع",
"الثامن",
"التاسع",
"العاشر",
];
const ORDINAL_FEM: [&str; 11] = [
"",
"الأولى",
"الثانية",
"الثالثة",
"الرابعة",
"الخامسة",
"السادسة",
"السابعة",
"الثامنة",
"التاسعة",
"العاشرة",
];
const fn ordinal_first_compound(gender: Gender) -> &'static str {
match gender {
Gender::Masculine => "الحادي",
Gender::Feminine => "الحادية",
}
}
option_enum! {
pub enum Case {
Nominative = "nominative",
Accusative = "accusative",
Genitive = "genitive",
}
}
option_enum! {
pub enum HundredSpelling {
Mia = "مئة",
Miaa = "مائة",
}
}
#[derive(Debug, Clone, Copy)]
struct Style {
case: Case,
hundred: HundredSpelling,
}
struct Noun {
singular: &'static str,
dual: Cow<'static, str>,
plural: &'static str,
tamyiz: Cow<'static, str>,
gender: Gender,
counts_with_one: bool,
}
fn scale_noun(level: usize) -> Noun {
let singular = SCALES[level];
Noun {
singular,
dual: Cow::Owned(format!("{singular}ان")),
plural: SCALE_PLURALS[level],
tamyiz: Cow::Owned(format!("{singular}اً")),
gender: Gender::Masculine,
counts_with_one: false,
}
}
fn currency_noun(forms: &'static [&'static str], gender: Gender) -> Noun {
let at = |index: usize| forms.get(index).or(forms.last()).copied().unwrap_or("");
Noun {
singular: at(0),
dual: Cow::Borrowed(at(1)),
plural: at(2),
tamyiz: Cow::Borrowed(at(3)),
gender,
counts_with_one: true,
}
}
fn drop_last(s: &str, count: usize) -> &str {
let keep = s.chars().count().saturating_sub(count);
let end = s.char_indices().nth(keep).map_or(s.len(), |(i, _)| i);
&s[..end]
}
fn inflect_dual(dual: &str, case: Case) -> String {
if case == Case::Nominative {
dual.to_string()
} else {
format!("{}ين", drop_last(dual, 2))
}
}
fn construct_dual(dual: &str, case: Case) -> String {
let ending = if case == Case::Nominative { "ا" } else { "ي" };
format!("{}{ending}", drop_last(dual, 2))
}
fn noun_for_one(noun: &Noun) -> String {
if !noun.counts_with_one {
return noun.singular.to_string();
}
let one = match noun.gender {
Gender::Feminine => ONES_FEM[1],
Gender::Masculine => ONES_MASC[1],
};
format!("{} {one}", noun.singular)
}
fn tens_and_ones(r: u32, gender: Gender, style: Style, construct: bool) -> Vec<String> {
let case = style.case;
let feminine = gender == Gender::Feminine;
let ones: &[&str; 20] = if feminine { &ONES_FEM } else { &ONES_MASC };
let unit = (r % 10) as usize;
if r < 20 {
if r == 2 {
return vec![inflect_dual(ones[2], case)];
}
if r == 12 {
return vec![format!(
"{} {}",
construct_dual(ones[2], case),
teen_ten(gender)
)];
}
if r == 8 && feminine && (construct || case == Case::Accusative) {
return vec![EIGHT_FEM_CONSTRUCT.to_string()];
}
return vec![ones[r as usize].to_string()];
}
let tens_word = format!("{}{}", TENS_STEM[(r / 10) as usize], tens_ending(case));
if unit == 0 {
return vec![tens_word];
}
let unit_word = if unit == 1 {
one_in_compound(gender).to_string()
} else if unit == 2 {
inflect_dual(ones[2], case)
} else if unit == 8 && feminine && case == Case::Accusative {
EIGHT_FEM_CONSTRUCT.to_string()
} else {
ones[unit].to_string()
};
vec![unit_word, tens_word]
}
fn hundreds_word(h: u32, style: Style, construct: bool) -> String {
let hundred = style.hundred.as_str();
if h == 1 {
return hundred.to_string();
}
if h != 2 {
return format!("{}{hundred}", HUNDREDS_UNIT[h as usize]);
}
let dual = format!("{}تان", drop_last(hundred, 1));
if construct {
construct_dual(&dual, style.case)
} else {
inflect_dual(&dual, style.case)
}
}
fn group_words(g: u32, gender: Gender, style: Style) -> Vec<String> {
let h = g / 100;
let r = g % 100;
let mut parts = if h > 0 {
vec![hundreds_word(h, style, false)]
} else {
Vec::new()
};
if r > 0 {
parts.extend(tens_and_ones(r, gender, style, false));
}
parts
}
fn group_with_noun(g: u32, noun: &Noun, style: Style, construct: bool) -> Vec<String> {
let h = g / 100;
let r = g % 100;
let dual = if construct {
construct_dual(&noun.dual, style.case)
} else {
inflect_dual(&noun.dual, style.case)
};
if g == 1 {
return vec![noun_for_one(noun)];
}
if g == 2 {
return vec![dual];
}
let hundreds_of_noun = if h > 0 {
format!("{} {}", hundreds_word(h, style, true), noun.singular)
} else {
String::new()
};
if r == 0 {
return vec![hundreds_of_noun];
}
if h > 0 && r == 1 {
return vec![hundreds_of_noun, noun_for_one(noun)];
}
if h > 0 && r == 2 {
return vec![hundreds_of_noun, dual];
}
let noun_form: &str = if r <= 10 {
noun.plural
} else if construct {
noun.singular
} else {
&noun.tamyiz
};
let mut parts = if h > 0 {
vec![hundreds_word(h, style, false)]
} else {
Vec::new()
};
parts.extend(tens_and_ones(r, noun.gender, style, r <= 10));
if let Some(last) = parts.last_mut() {
last.push(' ');
last.push_str(noun_form);
}
parts
}
fn integer_to_conjuncts(
n: &BigUint,
gender: Gender,
style: Style,
noun: Option<&Noun>,
) -> Vec<String> {
let groups = split_groups(n, 1000);
let units_repeat_noun = noun.is_some() && groups.len() > 1 && groups[0] > 0 && groups[0] <= 2;
let head_level = if noun.is_none() {
None
} else {
groups
.iter()
.enumerate()
.position(|(level, &g)| g > 0 && (level > 0 || !units_repeat_noun))
};
let mut parts = Vec::new();
for level in (0..groups.len()).rev() {
let g = groups[level];
if g == 0 {
continue;
}
if level == 0 {
match noun {
None => parts.extend(group_words(g, gender, style)),
Some(noun) if units_repeat_noun => parts.push(if g == 1 {
noun_for_one(noun)
} else {
inflect_dual(&noun.dual, style.case)
}),
Some(noun) => parts.extend(group_with_noun(g, noun, style, false)),
}
continue;
}
let heads = head_level == Some(level);
parts.extend(group_with_noun(g, &scale_noun(level), style, heads));
if let (true, Some(noun), Some(last)) = (heads, noun, parts.last_mut()) {
last.push(' ');
last.push_str(noun.singular);
}
}
parts
}
fn join_conjuncts(parts: &[String]) -> String {
parts.join(&format!(" {AND}"))
}
fn integer_to_words(n: &BigUint, gender: Gender, style: Style) -> String {
if n.is_zero() {
return ZERO.to_string();
}
join_conjuncts(&integer_to_conjuncts(n, gender, style, None))
}
fn decimal_part_to_words(decimal: &str, gender: Gender, style: Style) -> String {
let mut parts = Vec::new();
let zeros = decimal.bytes().take_while(|&b| b == b'0').count();
for _ in 0..zeros {
parts.push(ZERO.to_string());
}
let remainder = &decimal[zeros..];
if !remainder.is_empty() {
parts.push(integer_to_words(
&digits_to_biguint(remainder),
gender,
style,
));
}
parts.join(" ")
}
options! {
pub struct CardinalOptions {
gender: Gender = Gender::Masculine;
case: Case = Case::Nominative;
hundred_spelling: HundredSpelling = HundredSpelling::Mia;
negative_word: String = NEGATIVE.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, number.fraction.as_deref())?;
let style = Style {
case: options.case,
hundred: options.hundred_spelling,
};
let mut parts = Vec::new();
if number.negative {
parts.push(options.negative_word.clone());
}
parts.push(integer_to_words(&number.integer, options.gender, style));
if let Some(fraction) = &number.fraction {
parts.push(DECIMAL_SEP.to_string());
parts.push(decimal_part_to_words(fraction, options.gender, style));
}
Ok(parts.join(" "))
}
fn small_ordinal(r: u32, gender: Gender, case: Case, compound: bool) -> String {
let ordinals: &[&str; 11] = if gender == Gender::Feminine {
&ORDINAL_FEM
} else {
&ORDINAL_MASC
};
let unit = (r % 10) as usize;
let unit_word = if unit == 1 {
ordinal_first_compound(gender)
} else {
ordinals[unit]
};
if r <= 10 {
return if compound && r == 1 {
unit_word
} else {
ordinals[r as usize]
}
.to_string();
}
if r < 20 {
return format!("{unit_word} {}", teen_ten(gender));
}
let tens_word = format!(
"{DEFINITE}{}{}",
TENS_STEM[(r / 10) as usize],
tens_ending(case)
);
if unit == 0 {
tens_word
} else {
format!("{unit_word} {AND}{tens_word}")
}
}
fn definite_cardinal(n: &BigUint, style: Style) -> String {
let parts: Vec<String> = integer_to_conjuncts(n, Gender::Masculine, style, None)
.into_iter()
.map(|part| format!("{DEFINITE}{part}"))
.collect();
join_conjuncts(&parts)
}
fn integer_to_ordinal(n: &BigUint, gender: Gender, style: Style) -> String {
let r = (n % 100u32).to_u32().expect("remainder below 100");
let base = n - BigUint::from(r);
if base.is_zero() {
return small_ordinal(r, gender, style.case, false);
}
if r == 0 {
return definite_cardinal(&base, style);
}
let genitive = Style {
case: Case::Genitive,
..style
};
format!(
"{} {AFTER} {}",
small_ordinal(r, gender, style.case, true),
definite_cardinal(&base, genitive)
)
}
options! {
pub struct OrdinalOptions {
gender: Gender = Gender::Masculine;
case: Case = Case::Nominative;
hundred_spelling: HundredSpelling = HundredSpelling::Mia;
}
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_ordinal_with(value, &OrdinalOptions::default())
}
pub fn to_ordinal_with<'a>(
value: impl Into<Number<'a>>,
options: &OrdinalOptions,
) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
let style = Style {
case: options.case,
hundred: options.hundred_spelling,
};
Ok(integer_to_ordinal(&n, options.gender, style))
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::SAR, table = CURRENCY_VOCAB;
case: Case = Case::Nominative;
hundred_spelling: HundredSpelling = HundredSpelling::Mia;
}
}
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 style = Style {
case: options.case,
hundred: options.hundred_spelling,
};
let mut parts = Vec::new();
if !money.major.is_zero() || money.minor == 0 {
let noun = currency_noun(words.major, words.major_gender.unwrap_or(Gender::Masculine));
if money.major.is_zero() {
parts.push(format!("{ZERO} {}", noun.singular));
} else {
parts.extend(integer_to_conjuncts(
&money.major,
noun.gender,
style,
Some(&noun),
));
}
}
if money.minor > 0 {
let forms = words.minor.ok_or(Error::NoMinorUnit {
currency: options.currency,
})?;
let noun = currency_noun(forms, words.minor_gender.unwrap_or(Gender::Masculine));
parts.extend(integer_to_conjuncts(
&BigUint::from(money.minor),
noun.gender,
style,
Some(&noun),
));
}
let words = join_conjuncts(&parts);
Ok(if money.negative {
format!("{NEGATIVE} {words}")
} else {
words
})
}
pub static LANGUAGE: Language = Language {
code: "ar-SA",
name: "Arabic (Saudi Arabia)",
kind: Kind::Full,
cardinal: Some(form!(with_options to_cardinal_with, CardinalOptions, CARDINAL_MAX)),
ordinal: Some(form!(with_options to_ordinal_with, OrdinalOptions, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};