use super::common::{HUNDREDS, TENS};
#[derive(Copy, Clone)]
pub enum OrdinalGender {
Masculine,
Feminine,
Neuter,
}
struct OrdinalForms {
masculine: &'static str,
feminine: &'static str,
neuter: &'static str,
}
impl OrdinalForms {
fn get(&self, gender: OrdinalGender) -> &'static str {
match gender {
OrdinalGender::Masculine => self.masculine,
OrdinalGender::Feminine => self.feminine,
OrdinalGender::Neuter => self.neuter,
}
}
}
const ORDINAL_UNDER_TWENTY: [OrdinalForms; 20] = [
OrdinalForms {
masculine: "нульовий",
feminine: "нульова",
neuter: "нульове",
},
OrdinalForms {
masculine: "перший",
feminine: "перша",
neuter: "перше",
},
OrdinalForms {
masculine: "другий",
feminine: "друга",
neuter: "друге",
},
OrdinalForms {
masculine: "третій",
feminine: "третя",
neuter: "третє",
},
OrdinalForms {
masculine: "четвертий",
feminine: "четверта",
neuter: "четверте",
},
OrdinalForms {
masculine: "п'ятий",
feminine: "п'ята",
neuter: "п'яте",
},
OrdinalForms {
masculine: "шостий",
feminine: "шоста",
neuter: "шосте",
},
OrdinalForms {
masculine: "сьомий",
feminine: "сьома",
neuter: "сьоме",
},
OrdinalForms {
masculine: "восьмий",
feminine: "восьма",
neuter: "восьме",
},
OrdinalForms {
masculine: "дев'ятий",
feminine: "дев'ята",
neuter: "дев'яте",
},
OrdinalForms {
masculine: "десятий",
feminine: "десята",
neuter: "десяте",
},
OrdinalForms {
masculine: "одинадцятий",
feminine: "одинадцята",
neuter: "одинадцяте",
},
OrdinalForms {
masculine: "дванадцятий",
feminine: "дванадцята",
neuter: "дванадцяте",
},
OrdinalForms {
masculine: "тринадцятий",
feminine: "тринадцята",
neuter: "тринадцяте",
},
OrdinalForms {
masculine: "чотирнадцятий",
feminine: "чотирнадцята",
neuter: "чотирнадцяте",
},
OrdinalForms {
masculine: "п'ятнадцятий",
feminine: "п'ятнадцята",
neuter: "п'ятнадцяте",
},
OrdinalForms {
masculine: "шістнадцятий",
feminine: "шістнадцята",
neuter: "шістнадцяте",
},
OrdinalForms {
masculine: "сімнадцятий",
feminine: "сімнадцята",
neuter: "сімнадцяте",
},
OrdinalForms {
masculine: "вісімнадцятий",
feminine: "вісімнадцята",
neuter: "вісімнадцяте",
},
OrdinalForms {
masculine: "дев'ятнадцятий",
feminine: "дев'ятнадцята",
neuter: "дев'ятнадцяте",
},
];
const ORDINAL_TENS: [Option<OrdinalForms>; 10] = [
None,
None,
Some(OrdinalForms {
masculine: "двадцятий",
feminine: "двадцята",
neuter: "двадцяте",
}),
Some(OrdinalForms {
masculine: "тридцятий",
feminine: "тридцята",
neuter: "тридцяте",
}),
Some(OrdinalForms {
masculine: "сороковий",
feminine: "сорокова",
neuter: "сорокове",
}),
Some(OrdinalForms {
masculine: "п'ятдесятий",
feminine: "п'ятдесята",
neuter: "п'ятдесяте",
}),
Some(OrdinalForms {
masculine: "шістдесятий",
feminine: "шістдесята",
neuter: "шістдесяте",
}),
Some(OrdinalForms {
masculine: "сімдесятий",
feminine: "сімдесята",
neuter: "сімдесяте",
}),
Some(OrdinalForms {
masculine: "вісімдесятий",
feminine: "вісімдесята",
neuter: "вісімдесяте",
}),
Some(OrdinalForms {
masculine: "дев'яностий",
feminine: "дев'яноста",
neuter: "дев'яносте",
}),
];
const ORDINAL_HUNDREDS: [Option<OrdinalForms>; 10] = [
None,
Some(OrdinalForms {
masculine: "сотий",
feminine: "сота",
neuter: "соте",
}),
Some(OrdinalForms {
masculine: "двохсотий",
feminine: "двохсота",
neuter: "двохсоте",
}),
Some(OrdinalForms {
masculine: "трьохсотий",
feminine: "трьохсота",
neuter: "трьохсоте",
}),
Some(OrdinalForms {
masculine: "чотирьохсотий",
feminine: "чотирьохсота",
neuter: "чотирьохсоте",
}),
Some(OrdinalForms {
masculine: "п'ятисотий",
feminine: "п'ятисота",
neuter: "п'ятисоте",
}),
Some(OrdinalForms {
masculine: "шістсотий",
feminine: "шістсота",
neuter: "шістсоте",
}),
Some(OrdinalForms {
masculine: "сімсотий",
feminine: "сімсота",
neuter: "сімсоте",
}),
Some(OrdinalForms {
masculine: "вісімсотий",
feminine: "вісімсота",
neuter: "вісімсоте",
}),
Some(OrdinalForms {
masculine: "дев'ятсотий",
feminine: "дев'ятсота",
neuter: "дев'ятсоте",
}),
];
const LINK_UNITS: [&str; 10] = [
"",
"",
"двох",
"трьох",
"чотирьох",
"п'яти",
"шести",
"семи",
"восьми",
"дев'яти",
];
const LINK_UNITS_COMPOUND: [&str; 10] = [
"",
"одно",
"двох",
"трьох",
"чотирьох",
"п'яти",
"шести",
"семи",
"восьми",
"дев'яти",
];
const LINK_TEENS: [&str; 10] = [
"десяти",
"одинадцяти",
"дванадцяти",
"тринадцяти",
"чотирнадцяти",
"п'ятнадцяти",
"шістнадцяти",
"сімнадцяти",
"вісімнадцяти",
"дев'ятнадцяти",
];
const LINK_TENS: [&str; 10] = [
"",
"",
"двадцяти",
"тридцяти",
"сорока",
"п'ятдесяти",
"шістдесяти",
"сімдесяти",
"вісімдесяти",
"дев'яносто",
];
const LINK_HUNDREDS: [&str; 10] = [
"",
"сто",
"двохсот",
"трьохсот",
"чотирьохсот",
"п'ятисот",
"шестисот",
"семисот",
"восьмисот",
"дев'ятисот",
];
const SCALE_STEMS: [&str; 5] = ["", "тисяч", "мільйон", "мільярд", "трильйон"];
const SCALE_ENDINGS: OrdinalForms = OrdinalForms {
masculine: "ний",
feminine: "на",
neuter: "не",
};
pub const MAX_ORDINAL: u64 = 999_999_999_999_999;
pub fn try_to_ordinal_words(number: u64, gender: OrdinalGender) -> Option<String> {
if number > MAX_ORDINAL {
return None;
}
if number == 0 {
return Some(ordinal_under_thousand(0, gender));
}
let remainder = number % 1000;
if remainder != 0 {
let tail = ordinal_under_thousand(remainder as u16, gender);
let head = number - remainder;
if head == 0 {
return Some(tail);
}
return Some(format!("{} {tail}", cardinal_head(head)));
}
let mut scale = 0usize;
let mut rest = number;
#[allow(clippy::manual_is_multiple_of)]
while rest % 1000 == 0 {
rest /= 1000;
scale += 1;
}
let group = (rest % 1000) as u16;
let above = number - (group as u64) * 1000u64.pow(scale as u32);
let fused = fused_scale_ordinal(group, scale, gender)?;
if above == 0 {
Some(fused)
} else {
Some(format!("{} {fused}", cardinal_head(above)))
}
}
pub fn to_ordinal_words(number: u64, gender: OrdinalGender) -> String {
try_to_ordinal_words(number, gender)
.unwrap_or_else(|| panic!("{number} is too large to express as an ordinal"))
}
fn cardinal_head(value: u64) -> String {
let words = super::cardinal::to_words(value);
for lone in ["одна ", "один "] {
if let Some(rest) = words.strip_prefix(lone) {
return rest.to_string();
}
}
words
}
fn fused_scale_ordinal(group: u16, scale: usize, gender: OrdinalGender) -> Option<String> {
let stem = SCALE_STEMS.get(scale)?;
if stem.is_empty() {
return None;
}
let mut out = String::with_capacity(32);
out.push_str(&linking_form(group));
out.push_str(stem);
out.push_str(SCALE_ENDINGS.get(gender));
Some(out)
}
fn linking_form(number: u16) -> String {
let hundreds = (number / 100) as usize;
let below_hundred = number % 100;
let mut out = String::with_capacity(24);
out.push_str(LINK_HUNDREDS[hundreds]);
if (10..20).contains(&below_hundred) {
out.push_str(LINK_TEENS[(below_hundred - 10) as usize]);
return out;
}
let tens = (below_hundred / 10) as usize;
let units = (below_hundred % 10) as usize;
out.push_str(LINK_TENS[tens]);
let table = if hundreds > 0 || tens > 0 {
&LINK_UNITS_COMPOUND
} else {
&LINK_UNITS
};
out.push_str(table[units]);
out
}
fn ordinal_under_thousand(number: u16, gender: OrdinalGender) -> String {
if number < 20 {
return ORDINAL_UNDER_TWENTY[number as usize]
.get(gender)
.to_string();
}
if number < 100 {
let tens = number / 10;
let units = number % 10;
if units == 0 {
if let Some(forms) = &ORDINAL_TENS[tens as usize] {
return forms.get(gender).to_string();
}
}
let mut parts: Vec<&str> = Vec::new();
if tens > 0 {
let tens_word = TENS[tens as usize];
if !tens_word.is_empty() {
parts.push(tens_word);
}
}
if units > 0 {
parts.push(ORDINAL_UNDER_TWENTY[units as usize].get(gender));
}
return parts.join(" ");
}
let hundreds = number / 100;
let remainder = number % 100;
if remainder == 0 {
if let Some(forms) = &ORDINAL_HUNDREDS[hundreds as usize] {
return forms.get(gender).to_string();
}
}
let mut parts: Vec<&str> = Vec::new();
if hundreds > 0 {
let hundreds_word = HUNDREDS[hundreds as usize];
if !hundreds_word.is_empty() {
parts.push(hundreds_word);
}
}
if remainder < 20 {
parts.push(ORDINAL_UNDER_TWENTY[remainder as usize].get(gender));
} else {
let tens = remainder / 10;
let units = remainder % 10;
if units == 0 {
if let Some(forms) = &ORDINAL_TENS[tens as usize] {
parts.push(forms.get(gender));
}
} else {
if tens > 0 {
let tens_word = TENS[tens as usize];
if !tens_word.is_empty() {
parts.push(tens_word);
}
}
parts.push(ORDINAL_UNDER_TWENTY[units as usize].get(gender));
}
}
parts.join(" ")
}
#[cfg(test)]
mod tests {
use super::{try_to_ordinal_words, OrdinalGender::*, MAX_ORDINAL};
fn m(n: u64) -> String {
try_to_ordinal_words(n, Masculine).unwrap()
}
#[test]
fn under_a_thousand_is_unchanged() {
assert_eq!(m(0), "нульовий");
assert_eq!(m(1), "перший");
assert_eq!(m(345), "триста сорок п'ятий");
assert_eq!(m(999), "дев'ятсот дев'яносто дев'ятий");
}
#[test]
fn exact_multiples_fuse() {
for (n, expected) in [
(1_000, "тисячний"),
(2_000, "двохтисячний"),
(3_000, "трьохтисячний"),
(4_000, "чотирьохтисячний"),
(5_000, "п'ятитисячний"),
(9_000, "дев'ятитисячний"),
(10_000, "десятитисячний"),
(11_000, "одинадцятитисячний"),
(19_000, "дев'ятнадцятитисячний"),
(20_000, "двадцятитисячний"),
(25_000, "двадцятип'ятитисячний"),
(40_000, "сорокатисячний"),
(90_000, "дев'яностотисячний"),
(100_000, "стотисячний"),
(200_000, "двохсоттисячний"),
(500_000, "п'ятисоттисячний"),
(1_000_000, "мільйонний"),
(2_000_000, "двохмільйонний"),
(5_000_000, "п'ятимільйонний"),
(1_000_000_000, "мільярдний"),
] {
assert_eq!(m(n), expected, "{n}");
}
}
#[test]
fn compounds_keep_their_thousands_cardinal() {
for (n, expected) in [
(1_001, "тисяча перший"),
(1_010, "тисяча десятий"),
(2_345, "дві тисячі триста сорок п'ятий"),
(5_010, "п'ять тисяч десятий"),
(21_001, "двадцять одна тисяча перший"),
(1_000_001, "мільйон перший"),
(2_000_345, "два мільйони триста сорок п'ятий"),
] {
assert_eq!(m(n), expected, "{n}");
}
}
#[test]
fn the_boundary_between_the_two_mechanisms() {
assert_eq!(m(1_999), "тисяча дев'ятсот дев'яносто дев'ятий");
assert_eq!(m(2_000), "двохтисячний");
assert_eq!(m(2_001), "дві тисячі перший");
assert_eq!(m(999_999), "дев'ятсот дев'яносто дев'ять тисяч дев'ятсот дев'яносто дев'ятий");
assert_eq!(m(1_000_000), "мільйонний");
assert_eq!(m(1_000_001), "мільйон перший");
}
#[test]
fn one_behaves_differently_alone_and_in_a_compound() {
assert_eq!(m(1_000), "тисячний");
assert_eq!(m(21_000), "двадцятиоднотисячний");
assert_eq!(m(101_000), "стооднотисячний");
}
#[test]
fn a_fused_group_can_have_cardinal_words_above_it() {
assert_eq!(m(1_234_000), "мільйон двохсоттридцятичотирьохтисячний");
assert_eq!(m(2_001_000), "два мільйони тисячний");
}
#[test]
fn gender_agrees() {
assert_eq!(try_to_ordinal_words(2_000, Masculine).unwrap(), "двохтисячний");
assert_eq!(try_to_ordinal_words(2_000, Feminine).unwrap(), "двохтисячна");
assert_eq!(try_to_ordinal_words(2_000, Neuter).unwrap(), "двохтисячне");
assert_eq!(try_to_ordinal_words(1_000_000, Feminine).unwrap(), "мільйонна");
assert_eq!(try_to_ordinal_words(345, Neuter).unwrap(), "триста сорок п'яте");
}
#[test]
fn nothing_panics_across_the_range() {
for n in [0u64, 1, 999, 1_000, 1_000_000_000_000, MAX_ORDINAL] {
assert!(try_to_ordinal_words(n, Masculine).is_some(), "{n}");
}
for n in [MAX_ORDINAL + 1, u64::MAX] {
assert!(try_to_ordinal_words(n, Masculine).is_none(), "{n}");
}
}
#[test]
fn every_step_across_a_wide_range_produces_words() {
let mut n = 0u64;
while n < 3_000_000 {
let words = try_to_ordinal_words(n, Masculine).expect("in range");
assert!(!words.is_empty(), "{n}");
assert!(!words.contains(" "), "{n} → {words:?} has a double space");
assert!(!words.starts_with(' ') && !words.ends_with(' '), "{n} → {words:?}");
n += 997; }
}
}