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}