use crate::Result;
use crate::currency::vocab::KA 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::options;
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
const ONES: [&str; 10] = [
"ნული",
"ერთი",
"ორი",
"სამი",
"ოთხი",
"ხუთი",
"ექვსი",
"შვიდი",
"რვა",
"ცხრა",
];
const TEENS: [&str; 10] = [
"ათი",
"თერთმეტი",
"თორმეტი",
"ცამეტი",
"თოთხმეტი",
"თხუთმეტი",
"თექვსმეტი",
"ჩვიდმეტი",
"თვრამეტი",
"ცხრამეტი",
];
const VIGESIMAL: [&str; 5] = ["", "ოცი", "ორმოცი", "სამოცი", "ოთხმოცი"];
const VIGESIMAL_DA: [&str; 5] = ["", "ოცდა", "ორმოცდა", "სამოცდა", "ოთხმოცდა"];
const HUNDRED_PREFIXES: [&str; 10] = [
"",
"",
"ორ",
"სამ",
"ოთხ",
"ხუთ",
"ექვს",
"შვიდ",
"რვა",
"ცხრა",
];
const HUNDRED: &str = "ასი";
const HUNDRED_STEM: &str = "ას"; const THOUSAND: &str = "ათასი";
const THOUSAND_STEM: &str = "ათას";
const SCALES: [&str; 8] = [
"",
"",
"მილიონი",
"მილიარდი",
"ტრილიონი",
"კვადრილიონი",
"კვინტილიონი",
"სექსტილიონი",
];
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 ORDINAL_PREFIX: &str = "მე";
const ORDINAL_SUFFIX: &str = "ე";
const ORDINAL_ONES: [&str; 10] = [
"",
"პირველი",
"მეორე",
"მესამე",
"მეოთხე",
"მეხუთე",
"მეექვსე",
"მეშვიდე",
"მერვე",
"მეცხრე",
];
fn stem_of(word: &str) -> &str {
word.strip_suffix('ი')
.or_else(|| word.strip_suffix('ა'))
.unwrap_or(word)
}
fn build_tens(n: u32) -> String {
if n < 10 {
return ONES[n as usize].to_string();
}
if n < 20 {
return TEENS[(n - 10) as usize].to_string();
}
let vigesimal_group = (n / 20) as usize;
let remainder = n % 20;
if remainder == 0 {
return VIGESIMAL[vigesimal_group].to_string();
}
let base = VIGESIMAL_DA[vigesimal_group];
if remainder < 10 {
return format!("{base}{}", ONES[remainder as usize]);
}
format!("{base}{}", TEENS[(remainder - 10) as usize])
}
fn build_segment(n: u32) -> (String, String) {
if n == 0 {
return (String::new(), String::new());
}
if n < 100 {
let word = build_tens(n);
let stem = stem_of(&word).to_string();
return (word, stem);
}
let hundreds = (n / 100) as usize;
let remainder = n % 100;
let hundred_form = if remainder > 0 { HUNDRED_STEM } else { HUNDRED };
let hundred_word = if hundreds == 1 {
hundred_form.to_string()
} else {
format!("{}{hundred_form}", HUNDRED_PREFIXES[hundreds])
};
if remainder > 0 {
let remainder_word = build_tens(remainder);
let stem = format!("{hundred_word} {}", stem_of(&remainder_word));
return (format!("{hundred_word} {remainder_word}"), stem);
}
let stem = hundred_word[..hundred_word.len() - 'ი'.len_utf8()].to_string();
(hundred_word, stem)
}
fn integer_to_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_segment(small as u32).0;
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let thousand_word = if remainder > 0 {
THOUSAND_STEM
} else {
THOUSAND
};
let mut result = if thousands == 1 {
thousand_word.to_string()
} else {
format!("{} {thousand_word}", build_segment(thousands).1)
};
if remainder > 0 {
result.push(' ');
result.push_str(&build_segment(remainder).0);
}
return result;
}
}
build_large_number_words(n)
}
fn build_large_number_words(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let mut parts: Vec<String> = Vec::new();
for (scale_index, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if scale_index == 0 {
parts.push(build_segment(segment).0);
} else if scale_index == 1 {
let has_remainder = segments[0] != 0;
let thousand_word = if has_remainder {
THOUSAND_STEM
} else {
THOUSAND
};
if segment == 1 {
parts.push(thousand_word.to_string());
} else {
parts.push(format!("{} {thousand_word}", build_segment(segment).1));
}
} else {
let scale_word = SCALES
.get(scale_index)
.copied()
.unwrap_or(SCALES[SCALES.len() - 1]);
if segment == 1 {
parts.push(format!("{} {scale_word}", ONES[1]));
} else {
parts.push(format!("{} {scale_word}", build_segment(segment).0));
}
}
}
parts.join(" ")
}
fn decimal_part_to_words(decimal: &str) -> String {
let mut result = String::new();
let zeros = decimal.bytes().take_while(|&b| b == b'0').count();
for _ in 0..zeros {
if !result.is_empty() {
result.push(' ');
}
result.push_str(ZERO);
}
let remainder = &decimal[zeros..];
if !remainder.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&integer_to_words(&digits_to_biguint(remainder)));
}
result
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, number.fraction.as_deref())?;
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer));
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 integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64().filter(|&small| small <= 9) {
return ORDINAL_ONES[small as usize].to_string();
}
let cardinal = integer_to_words(n);
format!("{ORDINAL_PREFIX}{}{ORDINAL_SUFFIX}", stem_of(&cardinal))
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
Ok(integer_to_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::GEL, 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(format!(
"{} {}",
integer_to_words(&money.major),
words.major_at(0)
));
}
if money.minor > 0 {
parts.push(format!(
"{} {}",
integer_to_words(&BigUint::from(money.minor)),
words.minor_at(0)
));
}
Ok(parts.join(" "))
}
pub static LANGUAGE: Language = Language {
code: "ka-GE",
name: "Georgian (Georgia)",
kind: Kind::Full,
cardinal: Some(form!(plain to_cardinal, CARDINAL_MAX)),
ordinal: Some(form!(plain to_ordinal, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};