Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions components/decimal/src/abstract_formatter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use fixed_decimal::UnsignedDecimal;
use writeable::Writeable;

use crate::{
CompactDecimalFormatter, DecimalFormatter, FormattedSign, FormattedUnsignedCompactDecimal,
FormattedUnsignedDecimal,
};

pub trait Sealed {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add documentation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub trait Sealed {}
/// An un-exportable supertrait used to seal [`AbstractFormatter`].
///
/// Because this trait is not re-exported from the crate root, downstream crates cannot
/// implement it. This prevents third-party implementations of [`AbstractFormatter`],
/// preserving SemVer stability and allowing internal formatting methods to evolve
/// without breaking external code.
pub trait Sealed {}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will follow-up


/// A trait representing an abstract number formatter.
///
/// This is a building block for more complicated formatters, like currency or units.
pub trait AbstractFormatter: core::fmt::Debug + Sealed {
#[doc(hidden)]
type FormattedUnsigned<'a>: Writeable
where
Self: 'a;

#[doc(hidden)]
fn format_unsigned<'a>(&'a self, value: &'a UnsignedDecimal) -> Self::FormattedUnsigned<'a>;

#[doc(hidden)]
fn format_sign<'a, W: Writeable>(
&'a self,
value: W,
sign: fixed_decimal::Sign,
) -> FormattedSign<'a, W>;
}

impl Sealed for DecimalFormatter {}
impl AbstractFormatter for DecimalFormatter {
type FormattedUnsigned<'a> = FormattedUnsignedDecimal<'a>;

fn format_unsigned<'a>(&'a self, value: &'a UnsignedDecimal) -> Self::FormattedUnsigned<'a> {
self.format_unsigned(crate::Cow::Borrowed(value))
}

fn format_sign<'a, W: Writeable>(
&'a self,
value: W,
sign: fixed_decimal::Sign,
) -> FormattedSign<'a, W> {
self.format_sign(sign, value)
}
}

impl Sealed for CompactDecimalFormatter {}
impl AbstractFormatter for CompactDecimalFormatter {
type FormattedUnsigned<'a> = FormattedUnsignedCompactDecimal<'a>;

fn format_unsigned<'a>(&'a self, value: &'a UnsignedDecimal) -> Self::FormattedUnsigned<'a> {
self.format_unsigned(value)
}

fn format_sign<'a, W: Writeable>(
&'a self,
value: W,
sign: fixed_decimal::Sign,
) -> FormattedSign<'a, W> {
self.decimal_formatter.format_sign(sign, value)
}
}
158 changes: 88 additions & 70 deletions components/decimal/src/compact_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{
#[cfg(feature = "alloc")]
use alloc::string::String;
use fixed_decimal::UnsignedDecimal;
use icu_pattern::{Pattern, PatternBackend, SinglePlaceholder};
use icu_pattern::{Pattern, SinglePlaceholder};
use icu_plurals::PluralRules;
use icu_provider::DataError;
use icu_provider::{marker::ErasedMarker, prelude::*};
Expand Down Expand Up @@ -76,7 +76,7 @@ use writeable::Writeable;
#[derive(Debug)]
pub struct CompactDecimalFormatter {
plural_rules: PluralRules,
decimal_formatter: DecimalFormatter,
pub(crate) decimal_formatter: DecimalFormatter,
compact_data:
DataPayload<ErasedMarker<<DecimalCompactLongV1 as DynamicDataMarker>::DataStruct>>,
}
Expand Down Expand Up @@ -347,20 +347,80 @@ impl CompactDecimalFormatter {
/// "999K"
/// );
/// ```
pub fn format(&self, value: &Decimal) -> impl Writeable + Display + '_ + use<'_> {
let (compact_pattern, significand) = self
pub fn format<'a>(&'a self, value: &Decimal) -> impl Writeable + Display + 'a {
self.decimal_formatter
.format_sign(value.sign, self.format_unsigned(&value.absolute))
}

pub(crate) fn format_unsigned<'a>(
&'a self,
value: &UnsignedDecimal,
) -> FormattedUnsignedCompactDecimal<'a> {
let log10_type = value.nonzero_magnitude_start();

let entry = self
.compact_data
.get()
.get_pattern_and_significand(&value.absolute, &self.plural_rules);
.0
.iter()
.enumerate()
.filter(|&(_, t)| i16::from(t.sized) <= log10_type)
.last();

self.decimal_formatter.format_sign(
value.sign,
compact_pattern
.unwrap_or(Pattern::<SinglePlaceholder>::PASS_THROUGH)
.interpolate([self
.decimal_formatter
.format_unsigned(Cow::Owned(significand))]),
)
let exponent = entry
.map(|(_, t)| t.sized - t.variable.get_default().0.get())
.unwrap_or_default();
let rounding_magnitude = if log10_type > i16::from(exponent) {
// If we have at least 2 digits before the decimal point,
// round to eliminate the fractional part.
i16::from(exponent)
} else {
// …otherwise, round to two significant digits
log10_type - 1
};
if let Some(t) = self
.compact_data
.get()
.0
.get(entry.map(|(idx, _)| idx + 1).unwrap_or_default())
{
let next_exponent = t.sized - t.variable.get_default().0.get();

let rounds_to_next_exponent = log10_type + 1 == i16::from(next_exponent)
&& value.digit_at(rounding_magnitude - 1) >= 5
&& (rounding_magnitude..=log10_type).all(|m| value.digit_at(m) == 9);

// We got bumped up a magnitude by rounding.
if rounds_to_next_exponent {
return FormattedUnsignedCompactDecimal {
pattern: Some(t.variable.get(1.into(), &self.plural_rules).1),
significand: UnsignedDecimal::ONE,
decimal_formatter: &self.decimal_formatter,
};
}
}

FormattedUnsignedCompactDecimal {
pattern: entry.map(|(_, t)| {
t.variable
.get(
(&value
.clone()
.rounded(rounding_magnitude)
.multiplied_pow10(-i16::from(exponent))
.trimmed_end())
.into(),
&self.plural_rules,
)
.1
}),
significand: value
.clone()
.rounded(rounding_magnitude)
.multiplied_pow10(-i16::from(exponent))
.trimmed_end(),
decimal_formatter: &self.decimal_formatter,
}
}

/// Formats a [`Decimal`], returning a [`String`].
Expand Down Expand Up @@ -542,64 +602,22 @@ impl CompactDecimalFormatter {
}
}

impl<'a, P: PatternBackend> CompactPatterns<'a, P> {
/// Gets the compact pattern and significand for the given decimal
pub fn get_pattern_and_significand(
&'a self,
value: &UnsignedDecimal,
rules: &PluralRules,
) -> (Option<&'a Pattern<P>>, UnsignedDecimal) {
let log10_type = value.nonzero_magnitude_start();

let entry = self
.0
.iter()
.enumerate()
.filter(|&(_, t)| i16::from(t.sized) <= log10_type)
.last();

let exponent = entry
.map(|(_, t)| t.sized - t.variable.get_default().0.get())
.unwrap_or_default();

let rounding_magnitude = if log10_type > i16::from(exponent) {
// If we have at least 2 digits before the decimal point,
// round to eliminate the fractional part.
i16::from(exponent)
} else {
// …otherwise, round to two significant digits
log10_type - 1
};

if let Some(t) = self
.0
.get(entry.map(|(idx, _)| idx + 1).unwrap_or_default())
{
let next_exponent = t.sized - t.variable.get_default().0.get();

let rounds_to_next_exponent = log10_type + 1 == i16::from(next_exponent)
&& value.digit_at(rounding_magnitude - 1) >= 5
&& (rounding_magnitude..=log10_type).all(|m| value.digit_at(m) == 9);

// We got bumped up a magnitude by rounding.
if rounds_to_next_exponent {
return (
Some(t.variable.get(1.into(), rules).1),
UnsignedDecimal::ONE,
);
}
}

let significand = value
.clone()
.rounded(rounding_magnitude)
.multiplied_pow10(-i16::from(exponent))
.trimmed_end();
#[doc(hidden)] // TODO(#3647): should be private
#[derive(Debug)]
pub struct FormattedUnsignedCompactDecimal<'l> {
pattern: Option<&'l Pattern<SinglePlaceholder>>,
significand: UnsignedDecimal,
decimal_formatter: &'l DecimalFormatter,
}

(
entry.map(|(_, t)| t.variable.get((&significand).into(), rules).1),
significand,
)
impl Writeable for FormattedUnsignedCompactDecimal<'_> {
fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
self.pattern
.unwrap_or(Pattern::<SinglePlaceholder>::PASS_THROUGH)
.interpolate([self
.decimal_formatter
.format_unsigned(Cow::Borrowed(&self.significand))])
.write_to(sink)
}
}

Expand Down
6 changes: 2 additions & 4 deletions components/decimal/src/decimal_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ impl DecimalFormatter {
))
}

#[doc(hidden)] // TODO(#3647): should be private
pub fn format_unsigned<'l>(
pub(crate) fn format_unsigned<'l>(
&'l self,
value: Cow<'l, UnsignedDecimal>,
) -> FormattedUnsignedDecimal<'l> {
Expand All @@ -131,8 +130,7 @@ impl DecimalFormatter {
}
}

#[doc(hidden)] // TODO(#3647): should be private
pub fn format_sign<'l, T>(&'l self, sign: Sign, value: T) -> FormattedSign<'l, T> {
pub(crate) fn format_sign<'l, T>(&'l self, sign: Sign, value: T) -> FormattedSign<'l, T> {
FormattedSign {
sign: match sign {
Sign::None => None,
Expand Down
7 changes: 6 additions & 1 deletion components/decimal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ impl<'a, T> core::ops::Deref for Cow<'a, T> {
}
}

#[cfg(feature = "unstable")]
mod abstract_formatter;
#[cfg(feature = "unstable")]
mod compact_formatter;
mod decimal_formatter;
Expand All @@ -130,7 +132,10 @@ pub use decimal_formatter::{
};

#[cfg(feature = "unstable")]
pub use compact_formatter::CompactDecimalFormatter;
pub use compact_formatter::{CompactDecimalFormatter, FormattedUnsignedCompactDecimal};

#[cfg(feature = "unstable")]
pub use abstract_formatter::AbstractFormatter;

pub use preferences::DecimalFormatterPreferences;

Expand Down
Loading
Loading