icu_plurals/
operands.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use fixed_decimal::{CompactDecimal, Decimal};
6
7/// A full plural operands representation of a number. See [CLDR Plural Rules](http://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules) for complete operands description.
8///
9/// Plural operands in compliance with [CLDR Plural Rules](http://unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules).
10///
11/// See [full operands description](http://unicode.org/reports/tr35/tr35-numbers.html#Operands).
12///
13/// # Data Types
14///
15/// The following types can be converted to [`PluralOperands`]:
16///
17/// - Integers, signed and unsigned
18/// - Strings representing an arbitrary-precision decimal
19/// - [`Decimal`]
20///
21/// This crate does not support selection from a floating-point number, because floats are not
22/// capable of carrying trailing zeros, which are required for proper plural rule selection. For
23/// example, in English, "1 star" has a different plural form than "1.0 stars", but this
24/// distinction cannot be represented using a float. Clients should use [`Decimal`] instead.
25///
26/// # Examples
27///
28/// From int
29///
30/// ```
31/// use icu::plurals::PluralOperands;
32/// use icu::plurals::RawPluralOperands;
33///
34/// assert_eq!(
35///     PluralOperands::from(RawPluralOperands {
36///         i: 2,
37///         v: 0,
38///         w: 0,
39///         f: 0,
40///         t: 0,
41///         c: 0,
42///     }),
43///     PluralOperands::from(2_usize)
44/// );
45/// ```
46///
47/// From &str
48///
49/// ```
50/// use icu::plurals::PluralOperands;
51/// use icu::plurals::RawPluralOperands;
52///
53/// assert_eq!(
54///     Ok(PluralOperands::from(RawPluralOperands {
55///         i: 123,
56///         v: 2,
57///         w: 2,
58///         f: 45,
59///         t: 45,
60///         c: 0,
61///     })),
62///     "123.45".parse()
63/// );
64/// ```
65///
66/// From [`Decimal`]
67///
68/// ```
69/// use fixed_decimal::Decimal;
70/// use icu::plurals::PluralOperands;
71/// use icu::plurals::RawPluralOperands;
72///
73/// assert_eq!(
74///     PluralOperands::from(RawPluralOperands {
75///         i: 123,
76///         v: 2,
77///         w: 2,
78///         f: 45,
79///         t: 45,
80///         c: 0,
81///     }),
82///     (&{
83///         let mut decimal = Decimal::from(12345);
84///         decimal.multiply_pow10(-2);
85///         decimal
86///     })
87///         .into()
88/// );
89/// ```
90#[derive(Debug, Clone, Copy, PartialEq, Default)]
91#[allow(clippy::exhaustive_structs)] // mostly stable, new operands may be added at the cadence of ICU's release cycle
92pub struct PluralOperands {
93    /// Integer value of input
94    pub(crate) i: u64,
95    /// Number of visible fraction digits with trailing zeros
96    pub(crate) v: usize,
97    /// Number of visible fraction digits without trailing zeros
98    pub(crate) w: usize,
99    /// Visible fraction digits with trailing zeros
100    pub(crate) f: u64,
101    /// Visible fraction digits without trailing zeros
102    pub(crate) t: u64,
103    /// Exponent of the power of 10 used in compact decimal formatting
104    pub(crate) c: usize,
105}
106
107#[derive(displaydoc::Display, Debug, PartialEq, Eq)]
108#[non_exhaustive]
109#[cfg(feature = "datagen")]
110pub enum OperandsError {
111    /// Input to the Operands parsing was empty.
112    #[displaydoc("Input to the Operands parsing was empty")]
113    Empty,
114    /// Input to the Operands parsing was invalid.
115    #[displaydoc("Input to the Operands parsing was invalid")]
116    Invalid,
117}
118
119#[cfg(feature = "datagen")]
120impl core::error::Error for OperandsError {}
121
122#[cfg(feature = "datagen")]
123impl From<core::num::ParseIntError> for OperandsError {
124    fn from(_: core::num::ParseIntError) -> Self {
125        Self::Invalid
126    }
127}
128
129#[cfg(feature = "datagen")]
130impl core::str::FromStr for PluralOperands {
131    type Err = OperandsError;
132
133    fn from_str(input: &str) -> Result<Self, Self::Err> {
134        fn get_exponent(input: &str) -> Result<(&str, usize), OperandsError> {
135            if let Some((base, exponent)) = input.split_once('e') {
136                Ok((base, exponent.parse()?))
137            } else {
138                Ok((input, 0))
139            }
140        }
141
142        if input.is_empty() {
143            return Err(OperandsError::Empty);
144        }
145
146        let abs_str = input.strip_prefix('-').unwrap_or(input);
147
148        let (
149            integer_digits,
150            num_fraction_digits0,
151            num_fraction_digits,
152            fraction_digits0,
153            fraction_digits,
154            exponent,
155        ) = if let Some((int_str, rest)) = abs_str.split_once('.') {
156            let (dec_str, exponent) = get_exponent(rest)?;
157
158            let integer_digits = u64::from_str(int_str)?;
159
160            let dec_str_no_zeros = dec_str.trim_end_matches('0');
161
162            let num_fraction_digits0 = dec_str.len();
163            let num_fraction_digits = dec_str_no_zeros.len();
164
165            let fraction_digits0 = u64::from_str(dec_str)?;
166            let fraction_digits =
167                if num_fraction_digits == 0 || num_fraction_digits == num_fraction_digits0 {
168                    fraction_digits0
169                } else {
170                    u64::from_str(dec_str_no_zeros)?
171                };
172
173            (
174                integer_digits,
175                num_fraction_digits0,
176                num_fraction_digits,
177                fraction_digits0,
178                fraction_digits,
179                exponent,
180            )
181        } else {
182            let (abs_str, exponent) = get_exponent(abs_str)?;
183            let integer_digits = u64::from_str(abs_str)?;
184            (integer_digits, 0, 0, 0, 0, exponent)
185        };
186
187        Ok(Self {
188            i: integer_digits,
189            v: num_fraction_digits0,
190            w: num_fraction_digits,
191            f: fraction_digits0,
192            t: fraction_digits,
193            c: exponent,
194        })
195    }
196}
197
198macro_rules! impl_integer_type {
199    ($ty:ident) => {
200        impl From<$ty> for PluralOperands {
201            #[inline]
202            fn from(input: $ty) -> Self {
203                Self {
204                    i: input as u64,
205                    v: 0,
206                    w: 0,
207                    f: 0,
208                    t: 0,
209                    c: 0,
210                }
211            }
212        }
213    };
214    ($($ty:ident)+) => {
215        $(impl_integer_type!($ty);)+
216    };
217}
218
219macro_rules! impl_signed_integer_type {
220    ($ty:ident) => {
221        impl From<$ty> for PluralOperands {
222            #[inline]
223            fn from(input: $ty) -> Self {
224                input.unsigned_abs().into()
225            }
226        }
227    };
228    ($($ty:ident)+) => {
229        $(impl_signed_integer_type!($ty);)+
230    };
231}
232
233impl_integer_type!(u8 u16 u32 u64 u128 usize);
234impl_signed_integer_type!(i8 i16 i32 i64 i128 isize);
235
236impl PluralOperands {
237    fn from_significand_and_exponent(dec: &Decimal, exp: u8) -> PluralOperands {
238        let exp_i16 = i16::from(exp);
239
240        let mag_range = dec.absolute.magnitude_range();
241        let mag_high = core::cmp::min(17, *mag_range.end() + exp_i16);
242        let mag_low = core::cmp::max(-18, *mag_range.start() + exp_i16);
243
244        let mut i: u64 = 0;
245        for magnitude in (0..=mag_high).rev() {
246            i *= 10;
247            i += dec.absolute.digit_at(magnitude - exp_i16) as u64;
248        }
249
250        let mut f: u64 = 0;
251        let mut t: u64 = 0;
252        let mut w: usize = 0;
253        for magnitude in (mag_low..=-1).rev() {
254            let digit = dec.absolute.digit_at(magnitude - exp_i16) as u64;
255            f *= 10;
256            f += digit;
257            if digit != 0 {
258                t = f;
259                w = (-magnitude) as usize;
260            }
261        }
262
263        Self {
264            i,
265            v: (-mag_low) as usize,
266            w,
267            f,
268            t,
269            c: usize::from(exp),
270        }
271    }
272
273    /// Whether these [`PluralOperands`] are exactly the number 0, which might be a special case.
274    pub(crate) fn is_exactly_zero(self) -> bool {
275        self == Self {
276            i: 0,
277            v: 0,
278            w: 0,
279            f: 0,
280            t: 0,
281            c: 0,
282        }
283    }
284
285    /// Whether these [`PluralOperands`] are exactly the number 1, which might be a special case.
286    pub(crate) fn is_exactly_one(self) -> bool {
287        self == Self {
288            i: 1,
289            v: 0,
290            w: 0,
291            f: 0,
292            t: 0,
293            c: 0,
294        }
295    }
296}
297
298impl From<&Decimal> for PluralOperands {
299    /// Converts a [`fixed_decimal::Decimal`] to [`PluralOperands`]. Retains at most 18
300    /// digits each from the integer and fraction parts.
301    fn from(dec: &Decimal) -> Self {
302        Self::from_significand_and_exponent(dec, 0)
303    }
304}
305
306impl From<&CompactDecimal> for PluralOperands {
307    /// Converts a [`fixed_decimal::CompactDecimal`] to [`PluralOperands`]. Retains at most 18
308    /// digits each from the integer and fraction parts.
309    ///
310    /// # Examples
311    ///
312    /// ```
313    /// use fixed_decimal::CompactDecimal;
314    /// use fixed_decimal::Decimal;
315    /// use icu::locale::locale;
316    /// use icu::plurals::PluralCategory;
317    /// use icu::plurals::PluralOperands;
318    /// use icu::plurals::PluralRules;
319    /// use icu::plurals::RawPluralOperands;
320    ///
321    /// let fixed_decimal = "1000000.20".parse::<Decimal>().unwrap();
322    /// let compact_decimal = "1.00000020c6".parse::<CompactDecimal>().unwrap();
323    ///
324    /// assert_eq!(
325    ///     PluralOperands::from(RawPluralOperands {
326    ///         i: 1000000,
327    ///         v: 2,
328    ///         w: 1,
329    ///         f: 20,
330    ///         t: 2,
331    ///         c: 0,
332    ///     }),
333    ///     PluralOperands::from(&fixed_decimal)
334    /// );
335    ///
336    /// assert_eq!(
337    ///     PluralOperands::from(RawPluralOperands {
338    ///         i: 1000000,
339    ///         v: 2,
340    ///         w: 1,
341    ///         f: 20,
342    ///         t: 2,
343    ///         c: 6,
344    ///     }),
345    ///     PluralOperands::from(&compact_decimal)
346    /// );
347    ///
348    /// let rules = PluralRules::try_new_cardinal(locale!("fr").into()).unwrap();
349    /// assert_eq!(rules.category_for(&fixed_decimal), PluralCategory::Other);
350    /// assert_eq!(rules.category_for(&compact_decimal), PluralCategory::Many);
351    /// ```
352    fn from(compact: &CompactDecimal) -> Self {
353        Self::from_significand_and_exponent(compact.significand(), compact.exponent())
354    }
355}