Skip to content

Commit

Permalink
Implement PluralRulesWithRanges component (unicode-org#4116)
Browse files Browse the repository at this point in the history
  • Loading branch information
jedel1043 authored Oct 10, 2023
1 parent 3810109 commit 9e953c8
Show file tree
Hide file tree
Showing 15 changed files with 986 additions and 22 deletions.
312 changes: 312 additions & 0 deletions components/plurals/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ pub use operands::PluralOperands;
use provider::CardinalV1Marker;
use provider::ErasedPluralRulesV1Marker;
use provider::OrdinalV1Marker;
use provider::PluralRangesV1Marker;
use provider::UnvalidatedPluralRange;
use rules::runtime::test_rule;

#[doc(no_inline)]
Expand Down Expand Up @@ -550,3 +552,313 @@ impl PluralRules {
.chain(Some(PluralCategory::Other))
}
}

/// A [`PluralRules`] that also has the ability to retrieve an appropriate [`Plural Category`] for a
/// range.
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{PluralCategory, PluralOperands};
/// use icu::plurals::{PluralRuleType, PluralRulesWithRanges};
/// use std::convert::TryFrom;
///
/// let ranges = PluralRulesWithRanges::try_new(
/// &locale!("ar").into(),
/// PluralRuleType::Cardinal,
/// )
/// .expect("locale should be present");
///
/// let operands = PluralOperands::from(1_usize);
/// let operands2: PluralOperands =
/// "2.0".parse().expect("parsing to operands should succeed");
///
/// assert_eq!(
/// ranges.category_for_range(operands, operands2),
/// PluralCategory::Other
/// );
/// ```
///
/// [`Plural Category`]: PluralCategory
#[derive(Debug)]
pub struct PluralRulesWithRanges {
rules: PluralRules,
ranges: DataPayload<PluralRangesV1Marker>,
}

impl PluralRulesWithRanges {
icu_provider::gen_any_buffer_data_constructors!(
locale: include,
rule_type: PluralRuleType,
error: PluralsError,
/// Constructs a new `PluralRulesWithRanges` for a given locale using compiled data.
///
/// ✨ *Enabled with the `compiled_data` Cargo feature.*
///
/// [📚 Help choosing a constructor](icu_provider::constructors)
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{PluralRuleType, PluralRulesWithRanges};
///
/// let _ = PluralRulesWithRanges::try_new(
/// &locale!("en").into(),
/// PluralRuleType::Cardinal,
/// ).expect("locale should be present");
/// ```
);

#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
pub fn try_new_unstable(
provider: &(impl DataProvider<PluralRangesV1Marker>
+ DataProvider<CardinalV1Marker>
+ DataProvider<OrdinalV1Marker>
+ ?Sized),
locale: &DataLocale,
rule_type: PluralRuleType,
) -> Result<Self, PluralsError> {
match rule_type {
PluralRuleType::Cardinal => Self::try_new_cardinal_unstable(provider, locale),
PluralRuleType::Ordinal => Self::try_new_ordinal_unstable(provider, locale),
}
}

icu_provider::gen_any_buffer_data_constructors!(
locale: include,
options: skip,
error: PluralsError,
/// Constructs a new `PluralRulesWithRanges` for a given locale for cardinal numbers using
/// compiled data.
///
/// See [`PluralRules::try_new_cardinal`] for more information.
///
/// ✨ *Enabled with the `compiled_data` Cargo feature.*
///
/// [📚 Help choosing a constructor](icu_provider::constructors)
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{PluralCategory, PluralRulesWithRanges};
///
/// let rules = PluralRulesWithRanges::try_new_cardinal(&locale!("ru").into())
/// .expect("locale should be present");
///
/// assert_eq!(rules.category_for_range(0_usize, 2_usize), PluralCategory::Few);
/// ```
functions: [
try_new_cardinal,
try_new_cardinal_with_any_provider,
try_new_cardinal_with_buffer_provider,
try_new_cardinal_unstable,
Self,
]
);

#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new_cardinal)]
pub fn try_new_cardinal_unstable(
provider: &(impl DataProvider<CardinalV1Marker> + DataProvider<PluralRangesV1Marker> + ?Sized),
locale: &DataLocale,
) -> Result<Self, PluralsError> {
let rules = PluralRules::try_new_cardinal_unstable(provider, locale)?;
let ranges = provider
.load(DataRequest {
locale,
metadata: Default::default(),
})?
.take_payload()?;

Ok(Self { rules, ranges })
}

icu_provider::gen_any_buffer_data_constructors!(
locale: include,
options: skip,
error: PluralsError,
/// Constructs a new `PluralRulesWithRanges` for a given locale for ordinal numbers using
/// compiled data.
///
/// See [`PluralRules::try_new_ordinal`] for more information.
///
/// ✨ *Enabled with the `compiled_data` Cargo feature.*
///
/// [📚 Help choosing a constructor](icu_provider::constructors)
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{PluralCategory, PluralRulesWithRanges};
///
/// let rules = PluralRulesWithRanges::try_new_ordinal(
/// &locale!("ru").into(),
/// )
/// .expect("locale should be present");
///
/// assert_eq!(rules.category_for_range(0_usize, 2_usize), PluralCategory::Other);
/// ```
functions: [
try_new_ordinal,
try_new_ordinal_with_any_provider,
try_new_ordinal_with_buffer_provider,
try_new_ordinal_unstable,
Self,
]
);

#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new_ordinal)]
pub fn try_new_ordinal_unstable(
provider: &(impl DataProvider<OrdinalV1Marker> + DataProvider<PluralRangesV1Marker> + ?Sized),
locale: &DataLocale,
) -> Result<Self, PluralsError> {
let rules = PluralRules::try_new_ordinal_unstable(provider, locale)?;
let ranges = provider
.load(DataRequest {
locale,
metadata: Default::default(),
})?
.take_payload()?;

Ok(Self { rules, ranges })
}

/// Returns the [`Plural Category`] appropriate for the given number.
///
/// See [`PluralRules::category_for`] for more information.
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{PluralCategory, PluralRuleType, PluralRulesWithRanges};
///
/// let pr = PluralRulesWithRanges::try_new(
/// &locale!("en").into(),
/// PluralRuleType::Cardinal,
/// )
/// .expect("locale should be present");
///
/// match pr.category_for(1_usize) {
/// PluralCategory::One => "One item",
/// PluralCategory::Other => "Many items",
/// _ => unreachable!(),
/// };
/// ```
///
/// [`Plural Category`]: PluralCategory
pub fn category_for<I: Into<PluralOperands>>(&self, input: I) -> PluralCategory {
self.rules.category_for(input)
}

/// Returns all [`Plural Categories`] appropriate for a [`PluralRulesWithRanges`] object
/// based on the [`LanguageIdentifier`](icu::locid::{LanguageIdentifier}) and [`PluralRuleType`].
///
/// See [`PluralRules::categories`] for more information.
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{PluralCategory, PluralRuleType, PluralRulesWithRanges};
///
/// let pr = PluralRulesWithRanges::try_new(
/// &locale!("es").into(),
/// PluralRuleType::Cardinal,
/// )
/// .expect("locale should be present");
///
/// let mut categories = pr.categories();
/// assert_eq!(categories.next(), Some(PluralCategory::One));
/// assert_eq!(categories.next(), Some(PluralCategory::Many));
/// assert_eq!(categories.next(), Some(PluralCategory::Other));
/// assert_eq!(categories.next(), None);
/// ```
///
/// [`Plural Categories`]: PluralCategory
pub fn categories(&self) -> impl Iterator<Item = PluralCategory> + '_ {
self.rules.categories()
}

/// Returns the [`Plural Category`] appropriate for a range.
///
/// Note that the returned category is correct only if the range fulfills the following requirements:
/// - The start value is strictly less than the end value.
/// - Both values are positive.
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{
/// PluralCategory, PluralOperands, PluralRuleType, PluralRulesWithRanges,
/// };
///
/// let ranges = PluralRulesWithRanges::try_new(
/// &locale!("ro").into(),
/// PluralRuleType::Cardinal,
/// )
/// .expect("locale should be present");
/// let operands: PluralOperands =
/// "1.5".parse().expect("parsing to operands should succeed");
/// let operands2 = PluralOperands::from(1_usize);
///
/// assert_eq!(
/// ranges.category_for_range(operands, operands2),
/// PluralCategory::Few
/// );
/// ```
///
/// [`Plural Category`]: PluralCategory
pub fn category_for_range<S: Into<PluralOperands>, E: Into<PluralOperands>>(
&self,
start: S,
end: E,
) -> PluralCategory {
let start = self.rules.category_for(start);
let end = self.rules.category_for(end);

self.resolve_range(start, end)
}

/// Returns the [`Plural Category`] appropriate for a range from the categories of its endpoints.
///
/// Note that the returned category is correct only if the original numeric range fulfills the
/// following requirements:
/// - The start value is strictly less than the end value.
/// - Both values are positive.
///
/// # Examples
///
/// ```
/// use icu::locid::locale;
/// use icu::plurals::{PluralCategory, PluralRuleType, PluralRulesWithRanges};
///
/// let ranges = PluralRulesWithRanges::try_new(
/// &locale!("sl").into(),
/// PluralRuleType::Ordinal,
/// )
/// .expect("locale should be present");
///
/// assert_eq!(
/// ranges.resolve_range(PluralCategory::Other, PluralCategory::One),
/// PluralCategory::Few
/// );
/// ```
///
/// [`Plural Category`]: PluralCategory
pub fn resolve_range(&self, start: PluralCategory, end: PluralCategory) -> PluralCategory {
self.ranges
.get()
.ranges
.get_copied(&UnvalidatedPluralRange::from_range(
start.into(),
end.into(),
))
.map(PluralCategory::from)
.unwrap_or(end)
}
}
13 changes: 13 additions & 0 deletions components/plurals/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,19 @@ impl RawPluralCategory {
}
}

impl From<RawPluralCategory> for PluralCategory {
fn from(value: RawPluralCategory) -> Self {
match value {
RawPluralCategory::Other => PluralCategory::Other,
RawPluralCategory::Zero => PluralCategory::Zero,
RawPluralCategory::One => PluralCategory::One,
RawPluralCategory::Two => PluralCategory::Two,
RawPluralCategory::Few => PluralCategory::Few,
RawPluralCategory::Many => PluralCategory::Many,
}
}
}

impl From<PluralCategory> for RawPluralCategory {
fn from(value: PluralCategory) -> Self {
match value {
Expand Down
46 changes: 46 additions & 0 deletions components/plurals/tests/ranges.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use icu_locid::locale;
use icu_plurals::{PluralCategory, PluralOperands, PluralRuleType, PluralRulesWithRanges};

#[test]
fn test_plural_ranges_raw() {
assert_eq!(
PluralRulesWithRanges::try_new_cardinal(&locale!("he").into())
.unwrap()
.resolve_range(PluralCategory::One, PluralCategory::Two),
PluralCategory::Other
);
}

#[test]
fn test_plural_ranges_optimized_data() {
assert_eq!(
PluralRulesWithRanges::try_new_ordinal(&locale!("en").into())
.unwrap()
.resolve_range(PluralCategory::One, PluralCategory::Other),
PluralCategory::Other
);
}

#[test]
fn test_plural_ranges_missing_data_fallback() {
assert_eq!(
PluralRulesWithRanges::try_new_cardinal(&locale!("nl").into())
.unwrap()
.resolve_range(PluralCategory::Two, PluralCategory::Many),
PluralCategory::Many
);
}

#[test]
fn test_plural_ranges_full() {
let ranges =
PluralRulesWithRanges::try_new(&locale!("sl").into(), PluralRuleType::Cardinal).unwrap();
let start: PluralOperands = "0.5".parse().unwrap(); // PluralCategory::Other
let end: PluralOperands = PluralOperands::try_from(1).unwrap(); // PluralCategory::One

assert_eq!(ranges.category_for_range(start, end), PluralCategory::Few)
}
Loading

0 comments on commit 9e953c8

Please sign in to comment.