#[cfg(not(feature = "std"))]
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use crate::convert::convert_int_to_words;
use crate::decline::get_declension;
use crate::dictionary::{ORDINAL_HUNDREDS, ORDINAL_ONES, ORDINAL_TEENS, ORDINAL_TENS};
use crate::{Error, Gender};
const FEMININE: usize = 1;
pub fn fraction(numer: i64, denom: u32) -> Result<String, Error> {
if denom < 2 {
return Err(Error::InvalidNumber(format!(
"denominator must be >= 2, got {denom}"
)));
}
let numer_words = convert_int_to_words(numer, Gender::Feminine);
let last_two = (numer.unsigned_abs() % 100) as u32;
let denom_ordinal = feminine_ordinal_of(denom)?;
let denom_decl = if (11..=19).contains(&last_two) {
&denom_ordinal.plural
} else {
match last_two % 10 {
1 => &denom_ordinal.singular,
_ => &denom_ordinal.plural,
}
};
Ok(format!("{numer_words} {denom_decl}"))
}
pub fn mixed_fraction(whole: i64, numer: i64, denom: u32) -> Result<String, Error> {
let whole_words = convert_int_to_words(whole, Gender::Feminine);
let whole_decl = get_declension(whole, "целая", "целых", "целых");
let frac = fraction(numer, denom)?;
Ok(format!("{whole_words} {whole_decl} {frac}"))
}
struct FemOrdinalPair {
singular: String, plural: String, }
fn feminine_ordinal_of(n: u32) -> Result<FemOrdinalPair, Error> {
if !(2..=999).contains(&n) {
return Err(Error::InvalidNumber(format!(
"unsupported denominator: {n}"
)));
}
let h = (n / 100) as usize;
let t = ((n % 100) / 10) as usize;
let o = (n % 10) as usize;
let mut parts: Vec<String> = Vec::new();
let mut parts_plural: Vec<String> = Vec::new();
if t == 1 {
if h > 0 {
parts.push(crate::dictionary::HUNDREDS[h - 1].to_string());
parts_plural.push(crate::dictionary::HUNDREDS[h - 1].to_string());
}
let sing = ORDINAL_TEENS[o][FEMININE];
parts.push(sing.to_string());
parts_plural.push(pluralize_feminine(sing));
} else if o > 0 {
if h > 0 {
parts.push(crate::dictionary::HUNDREDS[h - 1].to_string());
parts_plural.push(crate::dictionary::HUNDREDS[h - 1].to_string());
}
if t > 0 {
parts.push(crate::dictionary::TENS[t].to_string());
parts_plural.push(crate::dictionary::TENS[t].to_string());
}
let sing = ORDINAL_ONES[o - 1][FEMININE];
parts.push(sing.to_string());
parts_plural.push(pluralize_feminine(sing));
} else if t > 0 {
if h > 0 {
parts.push(crate::dictionary::HUNDREDS[h - 1].to_string());
parts_plural.push(crate::dictionary::HUNDREDS[h - 1].to_string());
}
let sing = ORDINAL_TENS[t - 2][FEMININE];
parts.push(sing.to_string());
parts_plural.push(pluralize_feminine(sing));
} else {
let sing = ORDINAL_HUNDREDS[h - 1][FEMININE];
parts.push(sing.to_string());
parts_plural.push(pluralize_feminine(sing));
}
Ok(FemOrdinalPair {
singular: parts.join(" "),
plural: parts_plural.join(" "),
})
}
fn pluralize_feminine(s: &str) -> String {
if let Some(stripped) = s.strip_suffix("ья") {
return format!("{stripped}ьих");
}
if let Some(stripped) = s.strip_suffix("ая") {
return format!("{stripped}ых");
}
s.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fraction_basic() {
let cases: &[(i64, u32, &str)] = &[
(1, 2, "одна вторая"),
(1, 3, "одна третья"),
(1, 4, "одна четвёртая"),
(1, 5, "одна пятая"),
(3, 5, "три пятых"),
(1, 10, "одна десятая"),
(1, 100, "одна сотая"),
(2, 3, "две третьих"),
(5, 7, "пять седьмых"),
(22, 7, "двадцать две седьмых"),
(-1, 2, "минус одна вторая"),
(0, 5, "ноль пятых"),
(11, 13, "одиннадцать тринадцатых"),
];
for &(n, d, expected) in cases {
assert_eq!(fraction(n, d).unwrap(), expected, "fraction({n}, {d})");
}
}
#[test]
fn test_fraction_large_denom() {
assert_eq!(fraction(1, 25).unwrap(), "одна двадцать пятая");
assert_eq!(fraction(1, 200).unwrap(), "одна двухсотая");
assert_eq!(
fraction(7, 999).unwrap(),
"семь девятьсот девяносто девятых"
);
}
#[test]
fn test_fraction_errors() {
assert!(fraction(1, 0).is_err());
assert!(fraction(1, 1).is_err());
assert!(fraction(1, 1000).is_err());
}
#[test]
fn test_mixed_fraction() {
assert_eq!(mixed_fraction(1, 1, 2).unwrap(), "одна целая одна вторая");
assert_eq!(mixed_fraction(3, 2, 5).unwrap(), "три целых две пятых");
assert_eq!(mixed_fraction(2, 1, 4).unwrap(), "две целых одна четвёртая");
}
}