use icu::plurals as ipr;
pub(crate) mod internal {
use core::str::FromStr;
use ecma402_traits::pluralrules::options::Type;
use ecma402_traits::pluralrules::Options;
use fixed_decimal::SignedFixedDecimal;
use icu::plurals::{PluralCategory, PluralOperands, PluralRuleType};
use std::cmp::{max, min};
fn clamp_diff(first: usize, second: usize) -> usize {
match first > second {
true => first - second,
false => 0,
}
}
pub fn to_icu4x_type(style: &Type) -> PluralRuleType {
match style {
Type::Ordinal => PluralRuleType::Ordinal,
Type::Cardinal => PluralRuleType::Cardinal,
}
}
pub fn fixed_format(n: f64, opts: &Options) -> String {
let int_part = format!("{}", n.abs().floor());
let raw_frac_part = format!("{}", n.fract()); let frac_part = if n.fract() == 0.0 {
""
} else {
#[allow(clippy::indexing_slicing)] &raw_frac_part[2..]
};
dbg!("--> frac={}, fracf={}", &frac_part, &raw_frac_part);
dbg!("int_part='{}'; frac_part='{}'", &int_part, &frac_part);
dbg!("opts={:?}", opts);
let display_integer_digits = max(int_part.len(), opts.minimum_integer_digits as usize);
let display_fraction_digits = max(
min(frac_part.len(), opts.maximum_fraction_digits as usize),
opts.minimum_fraction_digits as usize,
);
let total_significant_digits = max(
opts.minimum_significant_digits as usize,
min(
opts.maximum_significant_digits as usize,
frac_part.len() + int_part.len(),
),
);
let significant_digits_in_fraction = clamp_diff(total_significant_digits, int_part.len());
dbg!(
"did={}; dfd={}; sd={}; rsd={}",
display_integer_digits,
display_fraction_digits,
total_significant_digits,
significant_digits_in_fraction
);
let leading_zeros = clamp_diff(display_integer_digits, int_part.len());
let trailing_zeros_in_int_part = clamp_diff(int_part.len(), total_significant_digits);
let i = std::iter::repeat('0')
.take(leading_zeros)
.chain(int_part.chars().take(total_significant_digits))
.chain(std::iter::repeat('0').take(trailing_zeros_in_int_part));
let dd = match display_fraction_digits == 0 {
true => "",
false => ".",
}
.chars();
let f = frac_part
.chars()
.take(significant_digits_in_fraction)
.chain(std::iter::repeat('0'))
.take(display_fraction_digits);
let nstr = i.chain(dd).chain(f).collect::<String>();
dbg!("nstr={}", &nstr);
nstr
}
pub fn to_icu4x_operands(n: f64, opts: Options) -> PluralOperands {
dbg!("n={}", n);
let nstr = fixed_format(n, &opts);
#[allow(clippy::unwrap_used)] let ret = PluralOperands::from(&SignedFixedDecimal::from_str(&nstr).unwrap());
dbg!("ret={:?}\n---\n", &ret);
ret
}
pub fn as_str(c: PluralCategory) -> &'static str {
match c {
PluralCategory::Few => "few",
PluralCategory::Many => "many",
PluralCategory::One => "one",
PluralCategory::Other => "other",
PluralCategory::Two => "two",
PluralCategory::Zero => "zero",
}
}
#[cfg(test)]
mod testing {
use super::*;
use ecma402_traits::pluralrules::options::Type;
use icu::plurals::rules::RawPluralOperands;
fn opt(
minimum_integer_digits: u8,
minimum_fraction_digits: u8,
maximum_fraction_digits: u8,
minimum_significant_digits: u8,
maximum_significant_digits: u8,
) -> Options {
Options {
in_type: Type::Cardinal,
minimum_integer_digits,
minimum_fraction_digits,
maximum_fraction_digits,
minimum_significant_digits,
maximum_significant_digits,
}
}
#[test]
fn string_conversion() {
#[derive(Debug)]
struct TestCase {
n: f64,
opts: Options,
expected: &'static str,
}
let tests = [
TestCase {
n: 0.0,
opts: opt(3, 2, 3, 3, 4),
expected: "000.00",
},
TestCase {
n: 1.5,
opts: opt(3, 2, 3, 3, 4),
expected: "001.50",
},
TestCase {
n: 1.5,
opts: opt(5, 5, 8, 3, 4),
expected: "00001.50000",
},
TestCase {
n: 123456.5,
opts: opt(5, 5, 8, 3, 4),
expected: "123400.00000",
},
TestCase {
n: 123456.5432112345,
opts: opt(5, 5, 8, 3, 4),
expected: "123400.00000000",
},
];
for test in tests {
let actual = fixed_format(test.n, &test.opts);
assert_eq!(test.expected, actual.as_str(), "test: {:?}", &test);
}
}
#[test]
fn format_conversion() {
#[derive(Debug)]
struct TestCase {
n: f64,
opts: Options,
expected: PluralOperands,
}
let tests = [TestCase {
n: 1.5,
opts: Options {
in_type: Type::Cardinal,
minimum_integer_digits: 3,
minimum_fraction_digits: 2,
maximum_fraction_digits: 3,
minimum_significant_digits: 3,
maximum_significant_digits: 4,
},
expected: RawPluralOperands {
i: 1,
v: 2,
w: 1,
f: 50,
t: 5,
c: 0,
}
.into(),
}];
for test in tests {
let actual = to_icu4x_operands(test.n, test.opts.clone());
assert_eq!(test.expected, actual, "test: {:?}", &test);
}
}
}
}
#[derive(Debug)]
pub struct PluralRules {
opts: ecma402_traits::pluralrules::Options,
rep: ipr::PluralRules,
}
impl ecma402_traits::pluralrules::PluralRules for PluralRules {
type Error = icu_provider::DataError;
fn try_new<L>(l: L, opts: ecma402_traits::pluralrules::Options) -> Result<Self, Self::Error>
where
L: ecma402_traits::Locale,
Self: Sized,
{
#[allow(clippy::unwrap_used)] let locale = icu::locale::Locale::try_from_str(&l.to_string()).unwrap();
let prefs = icu::plurals::PluralRulesPreferences::from(&locale);
let rule_type = internal::to_icu4x_type(&opts.in_type);
let rep = ipr::PluralRules::try_new(prefs, rule_type.into())?;
Ok(Self { opts, rep })
}
fn select<W>(&self, number: f64, writer: &mut W) -> std::fmt::Result
where
W: std::fmt::Write,
{
let op = internal::to_icu4x_operands(number, self.opts.clone());
let result = self.rep.category_for(op);
write!(writer, "{}", internal::as_str(result))
}
}
#[cfg(test)]
mod testing {
use crate::testing::TestLocale;
use ecma402_traits::pluralrules;
use ecma402_traits::pluralrules::PluralRules;
use icu_provider::DataError;
#[test]
fn plurals_per_locale() -> Result<(), DataError> {
#[derive(Debug, Clone)]
struct TestCase {
locale: TestLocale,
opts: pluralrules::Options,
numbers: &'static [f64],
expected: &'static [&'static str],
}
let tests = [
TestCase {
locale: TestLocale("ar"),
opts: Default::default(),
numbers: &[0.0, 1.0, 2.0, 5.0, 6.0, 18.0],
expected: &["zero", "one", "two", "few", "few", "many"],
},
TestCase {
locale: TestLocale("ar"),
opts: pluralrules::Options {
in_type: pluralrules::options::Type::Ordinal,
..Default::default()
},
numbers: &[0.0, 1.0, 2.0, 5.0, 6.0, 18.0],
expected: &["other", "other", "other", "other", "other", "other"],
},
TestCase {
locale: TestLocale("sr"),
opts: Default::default(),
numbers: &[0.0, 1.0, 2.0, 5.0, 6.0, 18.0],
expected: &["other", "one", "few", "other", "other", "other"],
},
TestCase {
locale: TestLocale("sr"),
opts: pluralrules::Options {
in_type: pluralrules::options::Type::Ordinal,
..Default::default()
},
numbers: &[0.0, 1.0, 2.0, 5.0, 6.0, 18.0],
expected: &["other", "other", "other", "other", "other", "other"],
},
];
for (i, test) in tests.into_iter().enumerate() {
let plr = super::PluralRules::try_new(test.locale, test.opts)?;
assert_eq!(
test.numbers
.iter()
.map(|n| {
let mut result = String::new();
plr.select(*n, &mut result).unwrap();
result
})
.collect::<Vec<_>>(),
test.expected,
"for test case: {}",
i
);
}
Ok(())
}
}