use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PluralCategory {
Zero,
One,
Two,
Few,
Many,
Other,
}
impl fmt::Display for PluralCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Zero => write!(f, "zero"),
Self::One => write!(f, "one"),
Self::Two => write!(f, "two"),
Self::Few => write!(f, "few"),
Self::Many => write!(f, "many"),
Self::Other => write!(f, "other"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PluralForms {
pub zero: Option<String>,
pub one: String,
pub two: Option<String>,
pub few: Option<String>,
pub many: Option<String>,
pub other: String,
}
impl PluralForms {
#[must_use]
pub fn select(&self, category: PluralCategory) -> &str {
match category {
PluralCategory::Zero => self.zero.as_deref().unwrap_or(&self.other),
PluralCategory::One => &self.one,
PluralCategory::Two => self.two.as_deref().unwrap_or(&self.other),
PluralCategory::Few => self.few.as_deref().unwrap_or(&self.other),
PluralCategory::Many => self.many.as_deref().unwrap_or(&self.other),
PluralCategory::Other => &self.other,
}
}
}
#[derive(Clone)]
pub enum PluralRule {
English,
Russian,
Arabic,
French,
CJK,
Polish,
Custom(fn(i64) -> PluralCategory),
}
impl PluralRule {
#[must_use]
pub fn categorize(&self, count: i64) -> PluralCategory {
let n = count.unsigned_abs();
match self {
Self::English => english_rule(n),
Self::Russian => russian_rule(n),
Self::Arabic => arabic_rule(n),
Self::French => french_rule(n),
Self::CJK => PluralCategory::Other,
Self::Polish => polish_rule(n),
Self::Custom(f) => f(count),
}
}
#[must_use]
pub fn for_locale(lang: &str) -> Self {
let primary = lang.split(['-', '_']).next().unwrap_or(lang);
match primary.to_ascii_lowercase().as_str() {
"en" | "de" | "nl" | "sv" | "da" | "no" | "nb" | "nn" | "it" | "es" | "pt" | "el"
| "hu" | "fi" | "et" | "he" | "tr" | "bg" => Self::English,
"fr" | "hi" | "bn" => Self::French,
"ru" | "uk" | "hr" | "sr" | "bs" => Self::Russian,
"pl" => Self::Polish,
"ar" => Self::Arabic,
"zh" | "ja" | "ko" | "th" | "vi" | "id" | "ms" => Self::CJK,
_ => Self::English,
}
}
}
impl fmt::Debug for PluralRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::English => write!(f, "PluralRule::English"),
Self::Russian => write!(f, "PluralRule::Russian"),
Self::Arabic => write!(f, "PluralRule::Arabic"),
Self::French => write!(f, "PluralRule::French"),
Self::CJK => write!(f, "PluralRule::CJK"),
Self::Polish => write!(f, "PluralRule::Polish"),
Self::Custom(_) => write!(f, "PluralRule::Custom(...)"),
}
}
}
fn english_rule(n: u64) -> PluralCategory {
if n == 1 {
PluralCategory::One
} else {
PluralCategory::Other
}
}
fn french_rule(n: u64) -> PluralCategory {
if n <= 1 {
PluralCategory::One
} else {
PluralCategory::Other
}
}
fn russian_rule(n: u64) -> PluralCategory {
let mod10 = n % 10;
let mod100 = n % 100;
if mod10 == 1 && mod100 != 11 {
PluralCategory::One
} else if (2..=4).contains(&mod10) && !(12..=14).contains(&mod100) {
PluralCategory::Few
} else if mod10 == 0 || (5..=9).contains(&mod10) || (11..=14).contains(&mod100) {
PluralCategory::Many
} else {
PluralCategory::Other
}
}
fn polish_rule(n: u64) -> PluralCategory {
let mod10 = n % 10;
let mod100 = n % 100;
if n == 1 {
PluralCategory::One
} else if (2..=4).contains(&mod10) && !(12..=14).contains(&mod100) {
PluralCategory::Few
} else {
PluralCategory::Many
}
}
fn arabic_rule(n: u64) -> PluralCategory {
let mod100 = n % 100;
match n {
0 => PluralCategory::Zero,
1 => PluralCategory::One,
2 => PluralCategory::Two,
_ if (3..=10).contains(&mod100) => PluralCategory::Few,
_ if (11..=99).contains(&mod100) => PluralCategory::Many,
_ => PluralCategory::Other,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn english_singular_plural() {
let rule = PluralRule::English;
assert_eq!(rule.categorize(0), PluralCategory::Other);
assert_eq!(rule.categorize(1), PluralCategory::One);
assert_eq!(rule.categorize(2), PluralCategory::Other);
assert_eq!(rule.categorize(100), PluralCategory::Other);
}
#[test]
fn french_zero_is_singular() {
let rule = PluralRule::French;
assert_eq!(rule.categorize(0), PluralCategory::One);
assert_eq!(rule.categorize(1), PluralCategory::One);
assert_eq!(rule.categorize(2), PluralCategory::Other);
}
#[test]
fn russian_complex_rules() {
let rule = PluralRule::Russian;
assert_eq!(rule.categorize(1), PluralCategory::One);
assert_eq!(rule.categorize(2), PluralCategory::Few);
assert_eq!(rule.categorize(3), PluralCategory::Few);
assert_eq!(rule.categorize(4), PluralCategory::Few);
assert_eq!(rule.categorize(5), PluralCategory::Many);
assert_eq!(rule.categorize(11), PluralCategory::Many);
assert_eq!(rule.categorize(12), PluralCategory::Many);
assert_eq!(rule.categorize(21), PluralCategory::One);
assert_eq!(rule.categorize(22), PluralCategory::Few);
assert_eq!(rule.categorize(25), PluralCategory::Many);
}
#[test]
fn arabic_full_categories() {
let rule = PluralRule::Arabic;
assert_eq!(rule.categorize(0), PluralCategory::Zero);
assert_eq!(rule.categorize(1), PluralCategory::One);
assert_eq!(rule.categorize(2), PluralCategory::Two);
assert_eq!(rule.categorize(5), PluralCategory::Few);
assert_eq!(rule.categorize(11), PluralCategory::Many);
assert_eq!(rule.categorize(100), PluralCategory::Other);
}
#[test]
fn cjk_always_other() {
let rule = PluralRule::CJK;
for n in [0, 1, 2, 5, 100, 1000] {
assert_eq!(rule.categorize(n), PluralCategory::Other);
}
}
#[test]
fn polish_rules() {
let rule = PluralRule::Polish;
assert_eq!(rule.categorize(1), PluralCategory::One);
assert_eq!(rule.categorize(2), PluralCategory::Few);
assert_eq!(rule.categorize(3), PluralCategory::Few);
assert_eq!(rule.categorize(4), PluralCategory::Few);
assert_eq!(rule.categorize(5), PluralCategory::Many);
assert_eq!(rule.categorize(12), PluralCategory::Many);
assert_eq!(rule.categorize(22), PluralCategory::Few);
}
#[test]
fn locale_detection() {
assert!(matches!(PluralRule::for_locale("en"), PluralRule::English));
assert!(matches!(
PluralRule::for_locale("en-US"),
PluralRule::English
));
assert!(matches!(PluralRule::for_locale("ru"), PluralRule::Russian));
assert!(matches!(PluralRule::for_locale("fr"), PluralRule::French));
assert!(matches!(PluralRule::for_locale("ar"), PluralRule::Arabic));
assert!(matches!(PluralRule::for_locale("zh"), PluralRule::CJK));
assert!(matches!(PluralRule::for_locale("ja"), PluralRule::CJK));
assert!(matches!(
PluralRule::for_locale("unknown"),
PluralRule::English
));
}
#[test]
fn plural_forms_select() {
let forms = PluralForms {
zero: Some("no items".into()),
one: "1 item".into(),
two: None,
few: Some("a few items".into()),
many: None,
other: "many items".into(),
};
assert_eq!(forms.select(PluralCategory::Zero), "no items");
assert_eq!(forms.select(PluralCategory::One), "1 item");
assert_eq!(forms.select(PluralCategory::Two), "many items"); assert_eq!(forms.select(PluralCategory::Few), "a few items");
assert_eq!(forms.select(PluralCategory::Many), "many items"); assert_eq!(forms.select(PluralCategory::Other), "many items");
}
#[test]
fn custom_rule() {
let rule = PluralRule::Custom(|n| {
if n == 42 {
PluralCategory::Few
} else {
PluralCategory::Other
}
});
assert_eq!(rule.categorize(42), PluralCategory::Few);
assert_eq!(rule.categorize(1), PluralCategory::Other);
}
#[test]
fn negative_counts() {
let rule = PluralRule::English;
assert_eq!(rule.categorize(-1), PluralCategory::One);
assert_eq!(rule.categorize(-2), PluralCategory::Other);
}
#[test]
fn plural_category_display() {
assert_eq!(PluralCategory::Zero.to_string(), "zero");
assert_eq!(PluralCategory::One.to_string(), "one");
assert_eq!(PluralCategory::Other.to_string(), "other");
}
}