Coverage Report

Created: 2025-07-18 06:22

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