icu_experimental/compactdecimal/
formatter.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 crate::compactdecimal::{
6    format::FormattedCompactDecimal,
7    options::CompactDecimalFormatterOptions,
8    provider::{
9        CompactDecimalPatternData, Count, LongCompactDecimalFormatDataV1, PatternULE,
10        ShortCompactDecimalFormatDataV1,
11    },
12    ExponentError,
13};
14use alloc::borrow::Cow;
15use core::convert::TryFrom;
16use fixed_decimal::{CompactDecimal, Decimal};
17use icu_decimal::{DecimalFormatter, DecimalFormatterPreferences};
18use icu_locale_core::preferences::{define_preferences, prefs_convert};
19use icu_plurals::{PluralRules, PluralRulesPreferences};
20use icu_provider::DataError;
21use icu_provider::{marker::ErasedMarker, prelude::*};
22use zerovec::maps::ZeroMap2dCursor;
23
24define_preferences!(
25    /// The preferences for compact decimal formatting.
26    [Copy]
27    CompactDecimalFormatterPreferences,
28    {
29        /// The user's preferred numbering system.
30        ///
31        /// Corresponds to the `-u-nu` in Unicode Locale Identifier.
32        numbering_system: super::preferences::NumberingSystem
33    }
34);
35
36prefs_convert!(
37    CompactDecimalFormatterPreferences,
38    DecimalFormatterPreferences,
39    { numbering_system }
40);
41prefs_convert!(CompactDecimalFormatterPreferences, PluralRulesPreferences);
42
43/// A formatter that renders locale-sensitive compact numbers.
44///
45/// # Examples
46///
47/// ```
48/// use icu::experimental::compactdecimal::CompactDecimalFormatter;
49/// use icu::locale::locale;
50/// use writeable::assert_writeable_eq;
51///
52/// let short_french = CompactDecimalFormatter::try_new_short(
53///    locale!("fr").into(),
54///    Default::default(),
55/// ).unwrap();
56///
57/// let [long_french, long_japanese, long_bangla] = [locale!("fr"), locale!("ja"), locale!("bn")]
58///     .map(|locale| {
59///         CompactDecimalFormatter::try_new_long(
60///             locale.into(),
61///             Default::default(),
62///         )
63///         .unwrap()
64///     });
65///
66/// /// Supports short and long notations:
67/// # // The following line contains U+00A0 NO-BREAK SPACE.
68/// assert_writeable_eq!(short_french.format_i64(35_357_670), "35 M");
69/// assert_writeable_eq!(long_french.format_i64(35_357_670), "35 millions");
70/// /// The powers of ten used are locale-dependent:
71/// assert_writeable_eq!(long_japanese.format_i64(3535_7670), "3536万");
72/// /// So are the digits:
73/// assert_writeable_eq!(long_bangla.format_i64(3_53_57_670), "৩.৫ কোটি");
74///
75/// /// The output does not always contain digits:
76/// assert_writeable_eq!(long_french.format_i64(1000), "mille");
77/// ```
78#[derive(Debug)]
79pub struct CompactDecimalFormatter {
80    pub(crate) plural_rules: PluralRules,
81    pub(crate) decimal_formatter: DecimalFormatter,
82    pub(crate) compact_data: DataPayload<ErasedMarker<CompactDecimalPatternData<'static>>>,
83}
84
85impl CompactDecimalFormatter {
86    /// Constructor that takes a selected locale and a list of preferences,
87    /// then collects all compiled data necessary to format numbers in short compact
88    /// decimal notation for the given locale.
89    ///
90    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
91    ///
92    /// [📚 Help choosing a constructor](icu_provider::constructors)
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use icu::experimental::compactdecimal::CompactDecimalFormatter;
98    /// use icu::locale::locale;
99    ///
100    /// CompactDecimalFormatter::try_new_short(
101    ///     locale!("sv").into(),
102    ///     Default::default(),
103    /// );
104    /// ```
105    #[cfg(feature = "compiled_data")]
106    pub fn try_new_short(
107        prefs: CompactDecimalFormatterPreferences,
108        options: CompactDecimalFormatterOptions,
109    ) -> Result<Self, DataError> {
110        let locale = ShortCompactDecimalFormatDataV1::make_locale(prefs.locale_preferences);
111        Ok(Self {
112            decimal_formatter: DecimalFormatter::try_new(
113                (&prefs).into(),
114                options.decimal_formatter_options,
115            )?,
116            plural_rules: PluralRules::try_new_cardinal((&prefs).into())?,
117            compact_data: DataProvider::<ShortCompactDecimalFormatDataV1>::load(
118                &crate::provider::Baked,
119                DataRequest {
120                    id: DataIdentifierBorrowed::for_locale(&locale),
121                    ..Default::default()
122                },
123            )?
124            .payload
125            .cast(),
126        })
127    }
128
129    icu_provider::gen_buffer_data_constructors!(
130        (prefs: CompactDecimalFormatterPreferences, options: CompactDecimalFormatterOptions) -> error: DataError,
131        functions: [
132            try_new_short: skip,
133            try_new_short_with_buffer_provider,
134            try_new_short_unstable,
135            Self,
136        ]
137    );
138
139    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new_short)]
140    pub fn try_new_short_unstable<D>(
141        provider: &D,
142        prefs: CompactDecimalFormatterPreferences,
143        options: CompactDecimalFormatterOptions,
144    ) -> Result<Self, DataError>
145    where
146        D: DataProvider<ShortCompactDecimalFormatDataV1>
147            + DataProvider<icu_decimal::provider::DecimalSymbolsV1>
148            + DataProvider<icu_decimal::provider::DecimalDigitsV1>
149            + DataProvider<icu_plurals::provider::PluralsCardinalV1>
150            + ?Sized,
151    {
152        let locale = ShortCompactDecimalFormatDataV1::make_locale(prefs.locale_preferences);
153        Ok(Self {
154            decimal_formatter: DecimalFormatter::try_new_unstable(
155                provider,
156                (&prefs).into(),
157                options.decimal_formatter_options,
158            )?,
159            plural_rules: PluralRules::try_new_cardinal_unstable(provider, (&prefs).into())?,
160            compact_data: DataProvider::<ShortCompactDecimalFormatDataV1>::load(
161                provider,
162                DataRequest {
163                    id: DataIdentifierBorrowed::for_locale(&locale),
164                    ..Default::default()
165                },
166            )?
167            .payload
168            .cast(),
169        })
170    }
171
172    /// Constructor that takes a selected locale and a list of preferences,
173    /// then collects all compiled data necessary to format numbers in short compact
174    /// decimal notation for the given locale.
175    ///
176    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
177    ///
178    /// [📚 Help choosing a constructor](icu_provider::constructors)
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// use icu::experimental::compactdecimal::CompactDecimalFormatter;
184    /// use icu::locale::locale;
185    ///
186    /// CompactDecimalFormatter::try_new_long(
187    ///     locale!("sv").into(),
188    ///     Default::default(),
189    /// );
190    /// ```
191    #[cfg(feature = "compiled_data")]
192    pub fn try_new_long(
193        prefs: CompactDecimalFormatterPreferences,
194        options: CompactDecimalFormatterOptions,
195    ) -> Result<Self, DataError> {
196        let locale = LongCompactDecimalFormatDataV1::make_locale(prefs.locale_preferences);
197        Ok(Self {
198            decimal_formatter: DecimalFormatter::try_new(
199                (&prefs).into(),
200                options.decimal_formatter_options,
201            )?,
202            plural_rules: PluralRules::try_new_cardinal((&prefs).into())?,
203            compact_data: DataProvider::<LongCompactDecimalFormatDataV1>::load(
204                &crate::provider::Baked,
205                DataRequest {
206                    id: DataIdentifierBorrowed::for_locale(&locale),
207                    ..Default::default()
208                },
209            )?
210            .payload
211            .cast(),
212        })
213    }
214
215    icu_provider::gen_buffer_data_constructors!(
216        (prefs: CompactDecimalFormatterPreferences, options: CompactDecimalFormatterOptions) -> error: DataError,
217        functions: [
218            try_new_long: skip,
219            try_new_long_with_buffer_provider,
220            try_new_long_unstable,
221            Self,
222        ]
223    );
224
225    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::try_new_long)]
226    pub fn try_new_long_unstable<D>(
227        provider: &D,
228        prefs: CompactDecimalFormatterPreferences,
229        options: CompactDecimalFormatterOptions,
230    ) -> Result<Self, DataError>
231    where
232        D: DataProvider<LongCompactDecimalFormatDataV1>
233            + DataProvider<icu_decimal::provider::DecimalSymbolsV1>
234            + DataProvider<icu_decimal::provider::DecimalDigitsV1>
235            + DataProvider<icu_plurals::provider::PluralsCardinalV1>
236            + ?Sized,
237    {
238        let locale = LongCompactDecimalFormatDataV1::make_locale(prefs.locale_preferences);
239        Ok(Self {
240            decimal_formatter: DecimalFormatter::try_new_unstable(
241                provider,
242                (&prefs).into(),
243                options.decimal_formatter_options,
244            )?,
245            plural_rules: PluralRules::try_new_cardinal_unstable(provider, (&prefs).into())?,
246            compact_data: DataProvider::<LongCompactDecimalFormatDataV1>::load(
247                provider,
248                DataRequest {
249                    id: DataIdentifierBorrowed::for_locale(&locale),
250                    ..Default::default()
251                },
252            )?
253            .payload
254            .cast(),
255        })
256    }
257
258    /// Formats an integer in compact decimal notation using the default
259    /// precision settings.
260    ///
261    /// The result may have a fractional digit only if it is compact and its
262    /// significand is less than 10. Trailing fractional 0s are omitted, and
263    /// a sign is shown only for negative values.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use icu::experimental::compactdecimal::CompactDecimalFormatter;
269    /// use icu::locale::locale;
270    /// use writeable::assert_writeable_eq;
271    ///
272    /// let short_english = CompactDecimalFormatter::try_new_short(
273    ///     locale!("en").into(),
274    ///     Default::default(),
275    /// )
276    /// .unwrap();
277    ///
278    /// assert_writeable_eq!(short_english.format_i64(0), "0");
279    /// assert_writeable_eq!(short_english.format_i64(2), "2");
280    /// assert_writeable_eq!(short_english.format_i64(843), "843");
281    /// assert_writeable_eq!(short_english.format_i64(2207), "2.2K");
282    /// assert_writeable_eq!(short_english.format_i64(15_127), "15K");
283    /// assert_writeable_eq!(short_english.format_i64(3_010_349), "3M");
284    /// assert_writeable_eq!(short_english.format_i64(-13_132), "-13K");
285    /// ```
286    ///
287    /// The result is the nearest such compact number, with halfway cases-
288    /// rounded towards the number with an even least significant digit.
289    ///
290    /// ```
291    /// # use icu::experimental::compactdecimal::CompactDecimalFormatter;
292    /// # use icu::locale::locale;
293    /// # use writeable::assert_writeable_eq;
294    /// #
295    /// # let short_english = CompactDecimalFormatter::try_new_short(
296    /// #    locale!("en").into(),
297    /// #    Default::default(),
298    /// # ).unwrap();
299    /// assert_writeable_eq!(short_english.format_i64(999_499), "999K");
300    /// assert_writeable_eq!(short_english.format_i64(999_500), "1M");
301    /// assert_writeable_eq!(short_english.format_i64(1650), "1.6K");
302    /// assert_writeable_eq!(short_english.format_i64(1750), "1.8K");
303    /// assert_writeable_eq!(short_english.format_i64(1950), "2K");
304    /// assert_writeable_eq!(short_english.format_i64(-1_172_700), "-1.2M");
305    /// ```
306    pub fn format_i64(&self, value: i64) -> FormattedCompactDecimal<'_> {
307        let unrounded = Decimal::from(value);
308        self.format_fixed_decimal(&unrounded)
309    }
310
311    /// Formats a floating-point number in compact decimal notation using the default
312    /// precision settings.
313    ///
314    /// The result may have a fractional digit only if it is compact and its
315    /// significand is less than 10. Trailing fractional 0s are omitted, and
316    /// a sign is shown only for negative values.
317    ///
318    /// ✨ *Enabled with the `ryu` Cargo feature.*
319    ///
320    /// # Examples
321    ///
322    /// ```
323    /// use icu::experimental::compactdecimal::CompactDecimalFormatter;
324    /// use icu::locale::locale;
325    /// use writeable::assert_writeable_eq;
326    ///
327    /// let short_english = CompactDecimalFormatter::try_new_short(
328    ///     locale!("en").into(),
329    ///     Default::default(),
330    /// )
331    /// .unwrap();
332    ///
333    /// assert_writeable_eq!(short_english.format_f64(0.0).unwrap(), "0");
334    /// assert_writeable_eq!(short_english.format_f64(2.0).unwrap(), "2");
335    /// assert_writeable_eq!(short_english.format_f64(843.0).unwrap(), "843");
336    /// assert_writeable_eq!(short_english.format_f64(2207.0).unwrap(), "2.2K");
337    /// assert_writeable_eq!(short_english.format_f64(15_127.0).unwrap(), "15K");
338    /// assert_writeable_eq!(short_english.format_f64(3_010_349.0).unwrap(), "3M");
339    /// assert_writeable_eq!(short_english.format_f64(-13_132.0).unwrap(), "-13K");
340    /// ```
341    ///
342    /// The result is the nearest such compact number, with halfway cases-
343    /// rounded towards the number with an even least significant digit.
344    ///
345    /// ```
346    /// # use icu::experimental::compactdecimal::CompactDecimalFormatter;
347    /// # use icu::locale::locale;
348    /// # use writeable::assert_writeable_eq;
349    /// #
350    /// # let short_english = CompactDecimalFormatter::try_new_short(
351    /// #    locale!("en").into(),
352    /// #    Default::default(),
353    /// # ).unwrap();
354    /// assert_writeable_eq!(short_english.format_f64(999_499.99).unwrap(), "999K");
355    /// assert_writeable_eq!(short_english.format_f64(999_500.00).unwrap(), "1M");
356    /// assert_writeable_eq!(short_english.format_f64(1650.0).unwrap(), "1.6K");
357    /// assert_writeable_eq!(short_english.format_f64(1750.0).unwrap(), "1.8K");
358    /// assert_writeable_eq!(short_english.format_f64(1950.0).unwrap(), "2K");
359    /// assert_writeable_eq!(
360    ///     short_english.format_f64(-1_172_700.0).unwrap(),
361    ///     "-1.2M"
362    /// );
363    /// ```
364    #[cfg(feature = "ryu")]
365    pub fn format_f64(
366        &self,
367        value: f64,
368    ) -> Result<FormattedCompactDecimal<'_>, fixed_decimal::LimitError> {
369        use fixed_decimal::FloatPrecision::RoundTrip;
370        // NOTE: This first gets the shortest representation of the f64, which
371        // manifests as double rounding.
372        let partly_rounded = Decimal::try_from_f64(value, RoundTrip)?;
373        Ok(self.format_fixed_decimal(&partly_rounded))
374    }
375
376    /// Formats a [`Decimal`] by automatically scaling and rounding it.
377    ///
378    /// The result may have a fractional digit only if it is compact and its
379    /// significand is less than 10. Trailing fractional 0s are omitted.
380    ///
381    /// Because the Decimal is mutated before formatting, this function
382    /// takes ownership of it.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use fixed_decimal::Decimal;
388    /// use icu::experimental::compactdecimal::CompactDecimalFormatter;
389    /// use icu::locale::locale;
390    /// use writeable::assert_writeable_eq;
391    ///
392    /// let short_english = CompactDecimalFormatter::try_new_short(
393    ///     locale!("en").into(),
394    ///     Default::default(),
395    /// )
396    /// .unwrap();
397    ///
398    /// assert_writeable_eq!(
399    ///     short_english.format_fixed_decimal(&Decimal::from(0)),
400    ///     "0"
401    /// );
402    /// assert_writeable_eq!(
403    ///     short_english.format_fixed_decimal(&Decimal::from(2)),
404    ///     "2"
405    /// );
406    /// assert_writeable_eq!(
407    ///     short_english.format_fixed_decimal(&Decimal::from(843)),
408    ///     "843"
409    /// );
410    /// assert_writeable_eq!(
411    ///     short_english.format_fixed_decimal(&Decimal::from(2207)),
412    ///     "2.2K"
413    /// );
414    /// assert_writeable_eq!(
415    ///     short_english.format_fixed_decimal(&Decimal::from(15127)),
416    ///     "15K"
417    /// );
418    /// assert_writeable_eq!(
419    ///     short_english.format_fixed_decimal(&Decimal::from(3010349)),
420    ///     "3M"
421    /// );
422    /// assert_writeable_eq!(
423    ///     short_english.format_fixed_decimal(&Decimal::from(-13132)),
424    ///     "-13K"
425    /// );
426    ///
427    /// // The sign display on the Decimal is respected:
428    /// assert_writeable_eq!(
429    ///     short_english.format_fixed_decimal(
430    ///         &Decimal::from(2500)
431    ///             .with_sign_display(fixed_decimal::SignDisplay::ExceptZero)
432    ///     ),
433    ///     "+2.5K"
434    /// );
435    /// ```
436    ///
437    /// The result is the nearest such compact number, with halfway cases-
438    /// rounded towards the number with an even least significant digit.
439    ///
440    /// ```
441    /// # use icu::experimental::compactdecimal::CompactDecimalFormatter;
442    /// # use icu::locale::locale;
443    /// # use writeable::assert_writeable_eq;
444    /// #
445    /// # let short_english = CompactDecimalFormatter::try_new_short(
446    /// #    locale!("en").into(),
447    /// #    Default::default(),
448    /// # ).unwrap();
449    /// assert_writeable_eq!(
450    ///     short_english.format_fixed_decimal(&"999499.99".parse().unwrap()),
451    ///     "999K"
452    /// );
453    /// assert_writeable_eq!(
454    ///     short_english.format_fixed_decimal(&"999500.00".parse().unwrap()),
455    ///     "1M"
456    /// );
457    /// assert_writeable_eq!(
458    ///     short_english.format_fixed_decimal(&"1650".parse().unwrap()),
459    ///     "1.6K"
460    /// );
461    /// assert_writeable_eq!(
462    ///     short_english.format_fixed_decimal(&"1750".parse().unwrap()),
463    ///     "1.8K"
464    /// );
465    /// assert_writeable_eq!(
466    ///     short_english.format_fixed_decimal(&"1950".parse().unwrap()),
467    ///     "2K"
468    /// );
469    /// assert_writeable_eq!(
470    ///     short_english.format_fixed_decimal(&"-1172700".parse().unwrap()),
471    ///     "-1.2M"
472    /// );
473    /// ```
474    pub fn format_fixed_decimal(&self, value: &Decimal) -> FormattedCompactDecimal<'_> {
475        let log10_type = value.absolute.nonzero_magnitude_start();
476        let (mut plural_map, mut exponent) = self.plural_map_and_exponent_for_magnitude(log10_type);
477        let mut significand = value.clone();
478        significand.multiply_pow10(-i16::from(exponent));
479        // If we have just one digit before the decimal point…
480        if significand.absolute.nonzero_magnitude_start() == 0 {
481            // …round to one fractional digit…
482            significand.round(-1);
483        } else {
484            // …otherwise, we have at least 2 digits before the decimal point,
485            // so round to eliminate the fractional part.
486            significand.round(0);
487        }
488        let rounded_magnitude =
489            significand.absolute.nonzero_magnitude_start() + i16::from(exponent);
490        if rounded_magnitude > log10_type {
491            // We got bumped up a magnitude by rounding.
492            // This means that `significand` is a power of 10.
493            let old_exponent = exponent;
494            // NOTE(egg): We could inline `plural_map_and_exponent_for_magnitude`
495            // to avoid iterating twice (we only need to look at the next key),
496            // but this obscures the logic and the map is tiny.
497            (plural_map, exponent) = self.plural_map_and_exponent_for_magnitude(rounded_magnitude);
498            significand = significand.clone();
499            significand.multiply_pow10(i16::from(old_exponent) - i16::from(exponent));
500            // There is no need to perform any rounding: `significand`, being
501            // a power of 10, is as round as it gets, and since `exponent` can
502            // only have become larger, it is already the correct rounding of
503            // `unrounded` to the precision we want to show.
504        }
505        significand.absolute.trim_end();
506        FormattedCompactDecimal {
507            formatter: self,
508            plural_map,
509            value: Cow::Owned(CompactDecimal::from_significand_and_exponent(
510                significand,
511                exponent,
512            )),
513        }
514    }
515
516    /// Formats a [`CompactDecimal`] object according to locale data.
517    ///
518    /// This is an advanced API; prefer using [`Self::format_i64()`] in simple
519    /// cases.
520    ///
521    /// Since the caller specifies the exact digits that are displayed, this
522    /// allows for arbitrarily complex rounding rules.
523    /// However, contrary to [`DecimalFormatter::format()`], this operation
524    /// can fail, because the given [`CompactDecimal`] can be inconsistent with
525    /// the locale data; for instance, if the locale uses lakhs and crores and
526    /// millions are requested, or vice versa, this function returns an error.
527    ///
528    /// The given [`CompactDecimal`] should be constructed using
529    /// [`Self::compact_exponent_for_magnitude()`] on the same
530    /// [`CompactDecimalFormatter`] object.
531    /// Specifically, `formatter.format_compact_decimal(n)` requires that `n.exponent()`
532    /// be equal to `formatter.compact_exponent_for_magnitude(n.significand().nonzero_magnitude_start() + n.exponent())`.
533    ///
534    /// # Examples
535    ///
536    /// ```
537    /// # use icu::experimental::compactdecimal::CompactDecimalFormatter;
538    /// # use icu::locale::locale;
539    /// # use writeable::assert_writeable_eq;
540    /// # use std::str::FromStr;
541    /// use fixed_decimal::CompactDecimal;
542    ///
543    /// # let short_french = CompactDecimalFormatter::try_new_short(
544    /// #    locale!("fr").into(),
545    /// #    Default::default(),
546    /// # ).unwrap();
547    /// # let long_french = CompactDecimalFormatter::try_new_long(
548    /// #    locale!("fr").into(),
549    /// #    Default::default()
550    /// # ).unwrap();
551    /// # let long_bangla = CompactDecimalFormatter::try_new_long(
552    /// #    locale!("bn").into(),
553    /// #    Default::default()
554    /// # ).unwrap();
555    /// #
556    /// let about_a_million = CompactDecimal::from_str("1.20c6").unwrap();
557    /// let three_million = CompactDecimal::from_str("+3c6").unwrap();
558    /// let ten_lakhs = CompactDecimal::from_str("10c5").unwrap();
559    /// # // The following line contains U+00A0 NO-BREAK SPACE.
560    /// assert_writeable_eq!(
561    ///     short_french
562    ///         .format_compact_decimal(&about_a_million)
563    ///         .unwrap(),
564    ///     "1,20 M"
565    /// );
566    /// assert_writeable_eq!(
567    ///     long_french
568    ///         .format_compact_decimal(&about_a_million)
569    ///         .unwrap(),
570    ///     "1,20 million"
571    /// );
572    ///
573    /// # // The following line contains U+00A0 NO-BREAK SPACE.
574    /// assert_writeable_eq!(
575    ///     short_french.format_compact_decimal(&three_million).unwrap(),
576    ///     "+3 M"
577    /// );
578    /// assert_writeable_eq!(
579    ///     long_french.format_compact_decimal(&three_million).unwrap(),
580    ///     "+3 millions"
581    /// );
582    ///
583    /// assert_writeable_eq!(
584    ///     long_bangla.format_compact_decimal(&ten_lakhs).unwrap(),
585    ///     "১০ লাখ"
586    /// );
587    ///
588    /// assert_eq!(
589    ///     long_bangla
590    ///         .format_compact_decimal(&about_a_million)
591    ///         .err()
592    ///         .unwrap()
593    ///         .to_string(),
594    ///     "Expected compact exponent 5 for 10^6, got 6",
595    /// );
596    /// assert_eq!(
597    ///     long_french
598    ///         .format_compact_decimal(&ten_lakhs)
599    ///         .err()
600    ///         .unwrap()
601    ///         .to_string(),
602    ///     "Expected compact exponent 6 for 10^6, got 5",
603    /// );
604    ///
605    /// /// Some patterns omit the digits; in those cases, the output does not
606    /// /// contain the sequence of digits specified by the CompactDecimal.
607    /// let a_thousand = CompactDecimal::from_str("1c3").unwrap();
608    /// assert_writeable_eq!(
609    ///     long_french.format_compact_decimal(&a_thousand).unwrap(),
610    ///     "mille"
611    /// );
612    /// ```
613    pub fn format_compact_decimal<'l>(
614        &'l self,
615        value: &'l CompactDecimal,
616    ) -> Result<FormattedCompactDecimal<'l>, ExponentError> {
617        let log10_type =
618            value.significand().absolute.nonzero_magnitude_start() + i16::from(value.exponent());
619
620        let (plural_map, expected_exponent) =
621            self.plural_map_and_exponent_for_magnitude(log10_type);
622        if value.exponent() != expected_exponent {
623            return Err(ExponentError {
624                actual: value.exponent(),
625                expected: expected_exponent,
626                log10_type,
627            });
628        }
629
630        Ok(FormattedCompactDecimal {
631            formatter: self,
632            plural_map,
633            value: Cow::Borrowed(value),
634        })
635    }
636
637    /// Returns the compact decimal exponent that should be used for a number of
638    /// the given magnitude when using this formatter.
639    ///
640    /// # Examples
641    /// ```
642    /// use icu::experimental::compactdecimal::CompactDecimalFormatter;
643    /// use icu::locale::locale;
644    ///
645    /// let [long_french, long_japanese, long_bangla] = [
646    ///     locale!("fr").into(),
647    ///     locale!("ja").into(),
648    ///     locale!("bn").into(),
649    /// ]
650    /// .map(|locale| {
651    ///     CompactDecimalFormatter::try_new_long(locale, Default::default())
652    ///         .unwrap()
653    /// });
654    /// /// French uses millions.
655    /// assert_eq!(long_french.compact_exponent_for_magnitude(6), 6);
656    /// /// Bangla uses lakhs.
657    /// assert_eq!(long_bangla.compact_exponent_for_magnitude(6), 5);
658    /// /// Japanese uses myriads.
659    /// assert_eq!(long_japanese.compact_exponent_for_magnitude(6), 4);
660    /// ```
661    pub fn compact_exponent_for_magnitude(&self, magnitude: i16) -> u8 {
662        let (_, exponent) = self.plural_map_and_exponent_for_magnitude(magnitude);
663        exponent
664    }
665
666    fn plural_map_and_exponent_for_magnitude(
667        &self,
668        magnitude: i16,
669    ) -> (Option<ZeroMap2dCursor<'_, '_, i8, Count, PatternULE>>, u8) {
670        let plural_map = self
671            .compact_data
672            .get()
673            .patterns
674            .iter0()
675            .filter(|cursor| i16::from(*cursor.key0()) <= magnitude)
676            .last();
677        let exponent = plural_map
678            .as_ref()
679            .and_then(|map| {
680                map.get1(&Count::Other)
681                    .and_then(|pattern| u8::try_from(pattern.exponent).ok())
682            })
683            .unwrap_or(0);
684        (plural_map, exponent)
685    }
686}
687
688#[cfg(feature = "serde")]
689#[cfg(test)]
690mod tests {
691    use super::*;
692    use icu_decimal::options::GroupingStrategy;
693    use icu_locale_core::locale;
694    use writeable::assert_writeable_eq;
695
696    #[allow(non_snake_case)]
697    #[test]
698    fn test_grouping() {
699        // https://unicode-org.atlassian.net/browse/ICU-22254
700        #[derive(Debug)]
701        struct TestCase<'a> {
702            short: bool,
703            options: CompactDecimalFormatterOptions,
704            expected1T: &'a str,
705            expected10T: &'a str,
706        }
707        let cases = [
708            TestCase {
709                short: true,
710                options: Default::default(),
711                expected1T: "1000T",
712                expected10T: "10,000T",
713            },
714            TestCase {
715                short: true,
716                options: GroupingStrategy::Always.into(),
717                expected1T: "1,000T",
718                expected10T: "10,000T",
719            },
720            TestCase {
721                short: true,
722                options: GroupingStrategy::Never.into(),
723                expected1T: "1000T",
724                expected10T: "10000T",
725            },
726            TestCase {
727                short: false,
728                options: Default::default(),
729                expected1T: "1000 trillion",
730                expected10T: "10,000 trillion",
731            },
732            TestCase {
733                short: false,
734                options: GroupingStrategy::Always.into(),
735                expected1T: "1,000 trillion",
736                expected10T: "10,000 trillion",
737            },
738            TestCase {
739                short: false,
740                options: GroupingStrategy::Never.into(),
741                expected1T: "1000 trillion",
742                expected10T: "10000 trillion",
743            },
744        ];
745        for case in cases {
746            let formatter = if case.short {
747                CompactDecimalFormatter::try_new_short(locale!("en").into(), case.options.clone())
748            } else {
749                CompactDecimalFormatter::try_new_long(locale!("en").into(), case.options.clone())
750            }
751            .unwrap();
752            let result1T = formatter.format_i64(1_000_000_000_000_000);
753            assert_writeable_eq!(result1T, case.expected1T, "{:?}", case);
754            let result10T = formatter.format_i64(10_000_000_000_000_000);
755            assert_writeable_eq!(result10T, case.expected10T, "{:?}", case);
756        }
757    }
758}