/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 | | } |