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}