Skip to content

Commit b9b4dbe

Browse files
use icu4x for number formatting
1 parent c34b16d commit b9b4dbe

File tree

5 files changed

+113
-17
lines changed

5 files changed

+113
-17
lines changed

fluent-bundle/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ include = [
2626
]
2727

2828
[dependencies]
29+
fixed_decimal = { version = "0.5.1", features = ["ryu"] }
2930
fluent-langneg.workspace = true
3031
fluent-syntax.workspace = true
32+
icu = "1"
33+
icu_decimal = "1"
34+
icu_provider = { version = "1", features = ["sync"] }
3135
intl_pluralrules.workspace = true
3236
rustc-hash.workspace = true
3337
unic-langid.workspace = true

fluent-bundle/src/bundle.rs

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use crate::message::FluentMessage;
2424
use crate::resolver::{ResolveValue, Scope, WriteValue};
2525
use crate::resource::FluentResource;
2626
use crate::types::FluentValue;
27+
use crate::types::NumberFormatProvider;
2728

2829
/// A collection of localization messages for a single locale, which are meant
2930
/// to be used together in a single view, widget or any other UI abstraction.
@@ -141,6 +142,7 @@ pub struct FluentBundle<R, M> {
141142
pub(crate) use_isolating: bool,
142143
pub(crate) transform: Option<fn(&str) -> Cow<str>>,
143144
pub(crate) formatter: Option<fn(&FluentValue, &M) -> Option<String>>,
145+
pub(crate) number_format_provider: Option<NumberFormatProvider>,
144146
}
145147

146148
impl<R, M> FluentBundle<R, M> {
@@ -586,6 +588,7 @@ impl<R> FluentBundle<R, IntlLangMemoizer> {
586588
use_isolating: true,
587589
transform: None,
588590
formatter: None,
591+
number_format_provider: None,
589592
}
590593
}
591594
}

fluent-bundle/src/concurrent.rs

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ impl<R> FluentBundle<R, IntlLangMemoizer> {
3131
use_isolating: true,
3232
transform: None,
3333
formatter: None,
34+
number_format_provider: None,
3435
}
3536
}
3637
}

fluent-bundle/src/types/number.rs

+102-14
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@ use std::convert::TryInto;
33
use std::default::Default;
44
use std::str::FromStr;
55

6+
use fixed_decimal::{DoublePrecision, FixedDecimal};
7+
use icu::locid::{LanguageIdentifier as IcuLanguageIdentifier, ParserError};
8+
use icu_decimal::options::{FixedDecimalFormatterOptions, GroupingStrategy};
9+
use icu_decimal::provider::DecimalSymbolsV1Marker;
10+
use icu_decimal::{DecimalError, FixedDecimalFormatter};
11+
use icu_provider::prelude::*;
12+
use intl_memoizer::Memoizable;
613
use intl_pluralrules::operands::PluralOperands;
14+
use unic_langid::LanguageIdentifier;
715

816
use crate::args::FluentArgs;
17+
use crate::bundle::FluentBundle;
18+
use crate::memoizer::MemoizerKind;
919
use crate::types::FluentValue;
1020

1121
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
@@ -133,22 +143,38 @@ impl FluentNumber {
133143
Self { value, options }
134144
}
135145

136-
pub fn as_string(&self) -> Cow<'static, str> {
137-
let mut val = self.value.to_string();
146+
pub fn as_string<R, M: MemoizerKind>(&self, bundle: &FluentBundle<R, M>) -> Cow<'static, str> {
147+
let fixed_decimal = self.as_fixed_decimal();
148+
let options = FormatterOptions {
149+
use_grouping: self.options.use_grouping,
150+
};
151+
if let Some(data_provider) = &bundle.number_format_provider {
152+
let formatted = bundle
153+
.intls
154+
.with_try_get_threadsafe::<NumberFormatter, _, _>(
155+
(options,),
156+
data_provider,
157+
|formatter| formatter.0.format_to_string(&fixed_decimal),
158+
)
159+
.unwrap();
160+
return formatted.into();
161+
}
162+
163+
fixed_decimal.to_string().into()
164+
}
165+
166+
fn as_fixed_decimal(&self) -> FixedDecimal {
167+
let mut fixed_decimal =
168+
FixedDecimal::try_from_f64(self.value, DoublePrecision::Floating).unwrap();
169+
138170
if let Some(minfd) = self.options.minimum_fraction_digits {
139-
if let Some(pos) = val.find('.') {
140-
let frac_num = val.len() - pos - 1;
141-
let missing = if frac_num > minfd {
142-
0
143-
} else {
144-
minfd - frac_num
145-
};
146-
val = format!("{}{}", val, "0".repeat(missing));
147-
} else {
148-
val = format!("{}.{}", val, "0".repeat(minfd));
149-
}
171+
fixed_decimal.pad_end(-(minfd as i16));
150172
}
151-
val.into()
173+
fixed_decimal
174+
}
175+
176+
pub fn as_string_basic(&self) -> String {
177+
self.as_fixed_decimal().to_string()
152178
}
153179
}
154180

@@ -238,8 +264,58 @@ from_num!(i8 i16 i32 i64 i128 isize);
238264
from_num!(u8 u16 u32 u64 u128 usize);
239265
from_num!(f32 f64);
240266

267+
pub type NumberFormatProvider = Box<dyn DataProvider<DecimalSymbolsV1Marker>>;
268+
269+
#[derive(Debug, Eq, PartialEq, Clone, Default, Hash)]
270+
struct FormatterOptions {
271+
use_grouping: bool,
272+
}
273+
274+
struct NumberFormatter(FixedDecimalFormatter);
275+
276+
#[derive(Debug)]
277+
enum NumberFormatterError {
278+
ParserError(ParserError),
279+
DecimalError(DecimalError),
280+
}
281+
282+
impl Memoizable for NumberFormatter {
283+
type Args = (FormatterOptions,);
284+
type Error = NumberFormatterError;
285+
type DataProvider = NumberFormatProvider;
286+
287+
fn construct(
288+
lang: LanguageIdentifier,
289+
args: Self::Args,
290+
data_provider: &Self::DataProvider,
291+
) -> Result<Self, Self::Error> {
292+
let locale = to_icu_lang_id(lang).map_err(NumberFormatterError::ParserError)?;
293+
294+
let mut options: FixedDecimalFormatterOptions = Default::default();
295+
options.grouping_strategy = match args.0.use_grouping {
296+
true => GroupingStrategy::Always,
297+
false => GroupingStrategy::Auto,
298+
};
299+
300+
let inner = FixedDecimalFormatter::try_new_unstable(
301+
data_provider.as_ref(),
302+
&locale.into(),
303+
options,
304+
)
305+
.map_err(NumberFormatterError::DecimalError)?;
306+
Ok(NumberFormatter(inner))
307+
}
308+
}
309+
310+
fn to_icu_lang_id(lang: LanguageIdentifier) -> Result<IcuLanguageIdentifier, ParserError> {
311+
return IcuLanguageIdentifier::try_from_locale_bytes(lang.to_string().as_bytes());
312+
}
313+
241314
#[cfg(test)]
242315
mod tests {
316+
use super::to_icu_lang_id;
317+
use unic_langid::langid;
318+
243319
use crate::types::FluentValue;
244320

245321
#[test]
@@ -249,4 +325,16 @@ mod tests {
249325
let z: FluentValue = y.into();
250326
assert_eq!(z, FluentValue::try_number(1));
251327
}
328+
329+
#[test]
330+
fn lang_to_icu() {
331+
assert_eq!(
332+
to_icu_lang_id(langid!("en-US")).unwrap(),
333+
icu::locid::langid!("en-US")
334+
);
335+
assert_eq!(
336+
to_icu_lang_id(langid!("pl")).unwrap(),
337+
icu::locid::langid!("pl")
338+
);
339+
}
252340
}

fluent-bundle/tests/types_test.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,18 @@ fn fluent_number_style() {
123123
assert_eq!(fno.use_grouping, false);
124124

125125
let num = FluentNumber::new(0.2, FluentNumberOptions::default());
126-
assert_eq!(num.as_string(), "0.2");
126+
assert_eq!(num.as_string_basic(), "0.2");
127127

128128
let opts = FluentNumberOptions {
129129
minimum_fraction_digits: Some(3),
130130
..Default::default()
131131
};
132132

133133
let num = FluentNumber::new(0.2, opts.clone());
134-
assert_eq!(num.as_string(), "0.200");
134+
assert_eq!(num.as_string_basic(), "0.200");
135135

136136
let num = FluentNumber::new(2.0, opts);
137-
assert_eq!(num.as_string(), "2.000");
137+
assert_eq!(num.as_string_basic(), "2.000");
138138
}
139139

140140
#[test]

0 commit comments

Comments
 (0)