use crate::Result;
use crate::currency::vocab::LV 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, options};
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
const ONES_MASC: [&str; 10] = [
"", "viens", "divi", "trīs", "četri", "pieci", "seši", "septiņi", "astoņi", "deviņi",
];
const ONES_FEM: [&str; 10] = [
"",
"viena",
"divas",
"trīs",
"četras",
"piecas",
"sešas",
"septiņas",
"astoņas",
"deviņas",
];
const TEENS: [&str; 10] = [
"desmit",
"vienpadsmit",
"divpadsmit",
"trīspadsmit",
"četrpadsmit",
"piecpadsmit",
"sešpadsmit",
"septiņpadsmit",
"astoņpadsmit",
"deviņpadsmit",
];
const TENS: [&str; 10] = [
"",
"",
"divdesmit",
"trīsdesmit",
"četrdesmit",
"piecdesmit",
"sešdesmit",
"septiņdesmit",
"astoņdesmit",
"deviņdesmit",
];
const HUNDRED_SINGULAR: &str = "simts";
const HUNDRED_PLURAL: &str = "simti";
const HUNDRED_GENITIVE: &str = "simtu";
const ZERO: &str = "nulle";
const NEGATIVE: &str = "mīnus";
const DECIMAL_SEP: &str = "komats";
const ORDINAL_ONES: [&str; 10] = [
"",
"pirmais",
"otrais",
"trešais",
"ceturtais",
"piektais",
"sestais",
"septītais",
"astotais",
"devītais",
];
const ORDINAL_TEENS: [&str; 10] = [
"desmitais",
"vienpadsmitais",
"divpadsmitais",
"trīspadsmitais",
"četrpadsmitais",
"piecpadsmitais",
"sešpadsmitais",
"septiņpadsmitais",
"astoņpadsmitais",
"deviņpadsmitais",
];
const ORDINAL_TENS: [&str; 10] = [
"",
"",
"divdesmitais",
"trīsdesmitais",
"četrdesmitais",
"piecdesmitais",
"sešdesmitais",
"septiņdesmitais",
"astoņdesmitais",
"deviņdesmitais",
];
const ORDINAL_HUNDREDS: [&str; 10] = [
"",
"simtais",
"divsimtais",
"trīssimtais",
"četrsimtais",
"piecsimtais",
"sešsimtais",
"septiņsimtais",
"astoņsimtais",
"deviņsimtais",
];
const ORDINAL_SCALES: &[&str] = &["tūkstošais", "miljonais", "miljardais", "triljonais"];
const SCALE_FORMS: &[[&str; 3]] = &[
["tūkstotis", "tūkstoši", "tūkstošu"],
["miljons", "miljoni", "miljonu"],
["miljards", "miljardi", "miljardu"],
["triljons", "triljoni", "triljonu"],
["kvadriljons", "kvadriljoni", "kvadriljonu"],
["kvintiljons", "kvintiljoni", "kvintiljonu"],
["sikstiljons", "sikstiljoni", "sikstiljonu"],
["septiljons", "septiljoni", "septiljonu"],
["oktiljons", "oktiljoni", "oktiljonu"],
];
pub const CARDINAL_MAX: Max = Max::western(SCALE_FORMS.len() as u32);
pub const ORDINAL_MAX: Max = Max::western(ORDINAL_SCALES.len() as u32);
pub const CURRENCY_MAX: Max = Max::western(SCALE_FORMS.len() as u32);
fn build_segment(n: u32, gender: Gender) -> 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 ones_words = match gender {
Gender::Masculine => &ONES_MASC,
Gender::Feminine => &ONES_FEM,
};
let mut parts: Vec<&str> = Vec::with_capacity(4);
if hundreds > 0 {
if hundreds == 1 && tens == 0 && ones > 0 {
parts.push(HUNDRED_GENITIVE);
} else if hundreds > 1 {
parts.push(ONES_MASC[hundreds]);
parts.push(HUNDRED_PLURAL);
} else {
parts.push(HUNDRED_SINGULAR);
}
}
if tens > 1 {
parts.push(TENS[tens]);
}
if tens == 1 {
parts.push(TEENS[ones]);
} else if ones > 0 {
parts.push(ones_words[ones]);
}
parts.join(" ")
}
fn plural_index(n: u32) -> usize {
if n == 0 {
return 2;
}
let last_digit = n % 10;
let last_two_digits = n % 100;
if last_digit == 1 && last_two_digits != 11 {
return 0;
}
1
}
fn plural_index_currency(n: u32) -> usize {
if n == 0 {
return 2;
}
let last_digit = n % 10;
let last_two_digits = n % 100;
if (10..=19).contains(&last_two_digits) {
return 2;
}
if last_digit == 0 {
return 2;
}
if last_digit == 1 {
return 0;
}
1
}
fn integer_to_words(n: &BigUint, gender: Gender) -> 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, gender);
}
}
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::with_capacity(segments.len());
for (scale_index, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
let segment_word = build_segment(segment, Gender::Masculine);
if scale_index == 0 {
parts.push(segment_word);
} else {
let scale_word = SCALE_FORMS[scale_index - 1][plural_index(segment)];
if segment == 1 {
parts.push(scale_word.to_string());
} else {
parts.push(format!("{segment_word} {scale_word}"));
}
}
}
parts.join(" ")
}
fn decimal_part_to_words(decimal: &str, gender: Gender) -> 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), gender));
}
result
}
options! {
pub struct CardinalOptions {
gender: Gender = Gender::Masculine;
}
}
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 mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer, options.gender));
if let Some(fraction) = &number.fraction {
result.push(' ');
result.push_str(DECIMAL_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction, options.gender));
}
Ok(result)
}
fn build_ordinal_tens_ones(n: u32) -> String {
if n == 0 {
return String::new();
}
let ones_digit = (n % 10) as usize;
let tens_digit = (n / 10) as usize;
if tens_digit == 0 {
return ORDINAL_ONES[ones_digit].to_string();
}
if tens_digit == 1 {
return ORDINAL_TEENS[ones_digit].to_string();
}
if ones_digit == 0 {
return ORDINAL_TENS[tens_digit].to_string();
}
format!("{} {}", TENS[tens_digit], ORDINAL_ONES[ones_digit])
}
fn build_ordinal_segment(n: u32) -> String {
if n < 100 {
return build_ordinal_tens_ones(n);
}
let hundreds_digit = (n / 100) as usize;
let remainder = n % 100;
if remainder == 0 {
return ORDINAL_HUNDREDS[hundreds_digit].to_string();
}
let hundred_word = if hundreds_digit == 1 {
HUNDRED_SINGULAR.to_string()
} else {
format!("{} {HUNDRED_PLURAL}", ONES_MASC[hundreds_digit])
};
format!("{hundred_word} {}", build_ordinal_tens_ones(remainder))
}
fn integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_ordinal_segment(small as u32);
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let scale_word = SCALE_FORMS[0][plural_index(thousands)];
if remainder == 0 {
if thousands == 1 {
return ORDINAL_SCALES[0].to_string();
}
return format!(
"{} {scale_word} {}",
build_segment(thousands, Gender::Masculine),
ORDINAL_SCALES[0]
);
}
let thousands_word = if thousands == 1 {
scale_word.to_string()
} else {
format!(
"{} {scale_word}",
build_segment(thousands, Gender::Masculine)
)
};
return format!("{thousands_word} {}", build_ordinal_segment(remainder));
}
}
build_large_ordinal(n)
}
fn build_large_ordinal(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let lowest_non_zero = segments.iter().position(|&s| s != 0).unwrap_or(0);
let mut parts: Vec<String> = Vec::with_capacity(segments.len());
for (scale_index, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
let is_last_non_zero = scale_index == lowest_non_zero;
if scale_index == 0 {
parts.push(build_ordinal_segment(segment));
} else if is_last_non_zero {
if segment == 1 {
parts.push(ORDINAL_SCALES[scale_index - 1].to_string());
} else {
let cardinal_scale_word = SCALE_FORMS[scale_index - 1][plural_index(segment)];
parts.push(format!(
"{} {cardinal_scale_word} {}",
build_segment(segment, Gender::Masculine),
ORDINAL_SCALES[scale_index - 1]
));
}
} else {
let scale_word = SCALE_FORMS[scale_index - 1][plural_index(segment)];
if segment == 1 {
parts.push(scale_word.to_string());
} else {
parts.push(format!(
"{} {scale_word}",
build_segment(segment, Gender::Masculine)
));
}
}
}
parts.join(" ")
}
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::EUR, 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 major_gender = words.major_gender.unwrap_or(Gender::Masculine);
let minor_gender = words.minor_gender.unwrap_or(Gender::Masculine);
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
if !money.major.is_zero() || money.minor == 0 {
result.push_str(&integer_to_words(&money.major, major_gender));
result.push(' ');
result.push_str(words.major_at(0));
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push(' ');
}
result.push_str(&integer_to_words(&BigUint::from(money.minor), minor_gender));
result.push(' ');
result.push_str(words.minor_at(plural_index_currency(money.minor)));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "lv-LV",
name: "Latvian (Latvia)",
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)),
};