1use std::fmt::Write;
2
3use fluent_static_value::Value;
4
5pub fn format(locale: &str, value: &Value, out: &mut impl Write) -> std::fmt::Result {
6 match value {
7 Value::String(s) => out.write_str(s),
8 Value::Number { value, format } => number::format_number(locale, value, format, out),
9 Value::Empty => Ok(()),
10 Value::Error => write!(out, "#error#"),
11 }
12}
13
14mod number {
15
16 use std::{cell::RefCell, collections::HashMap, fmt::Write};
17
18 use fluent_static_value::{
19 number::format::{
20 CurrencyDisplayStyle, CurrencySignMode, GroupingStyle, NumberStyle, UnitDisplayStyle,
21 },
22 Number, NumberFormat,
23 };
24 use rust_icu_unumberformatter::{UFormattedNumber, UNumberFormatter};
25
26 thread_local! {
27 static FORMATTER_CACHE: RefCell<HashMap<(String, NumberFormat), Result<UNumberFormatter, std::fmt::Error>>> = RefCell::new(HashMap::new());
28 }
29
30 pub(super) fn format_number(
31 locale: &str,
32 value: &Number,
33 format: &Option<NumberFormat>,
34 out: &mut impl Write,
35 ) -> std::fmt::Result {
36 if let Some(format) = format {
37 let key = (locale.to_string(), format.clone());
38 FORMATTER_CACHE.with(|cache| {
39 let mut cache = cache.borrow_mut();
40 if let Ok(formatter) = cache.entry(key).or_insert_with(|| {
41 let skeleton = make_icu_skeleton(format)?;
42 UNumberFormatter::try_new(&skeleton, locale).map_err(|_| std::fmt::Error)
43 }) {
44 let formatted_number: UFormattedNumber = match value {
45 Number::I64(n) => formatter.format_int(*n),
46 n => formatter.format_double(n.as_f64()),
47 }
48 .map_err(|_| std::fmt::Error)?;
49 let s: String = formatted_number.try_into().map_err(|_| std::fmt::Error)?;
50 out.write_str(&s)?;
51 Ok(())
52 } else {
53 Err(std::fmt::Error)
54 }
55 })
56 } else {
57 match value {
58 Number::I64(n) => write!(out, "{}", n),
59 Number::U64(n) => write!(out, "{}", n),
60 Number::I128(n) => write!(out, "{}", n),
61 Number::U128(n) => write!(out, "{}", n),
62 Number::F64(n) => write!(out, "{}", n),
63 }
64 }
65 }
66
67 fn make_icu_skeleton(format: &NumberFormat) -> Result<String, std::fmt::Error> {
68 let mut out = String::new();
69
70 match &format.use_grouping {
71 GroupingStyle::Always => {
72 }
75 GroupingStyle::Auto => {
76 write!(out, "group-auto ")?;
77 }
78 GroupingStyle::Min2 => {
79 write!(out, "group-min2 ")?;
80 }
81 GroupingStyle::Off => {
82 write!(out, "group-off ")?;
83 }
84 }
85
86 match &format.style {
87 NumberStyle::Decimal => {}
88 NumberStyle::Currency { code, style, sign } => {
89 write!(out, "currency/{} ", code)?;
90 match style {
91 CurrencyDisplayStyle::Code => {
92 write!(out, "unit-width-iso-code ")?;
93 }
94 CurrencyDisplayStyle::Symbol => {
95 write!(out, "unit-width-short")?;
96 }
97 CurrencyDisplayStyle::NarrowSymbol => {
98 write!(out, "unit-width-narrow")?;
99 }
100 CurrencyDisplayStyle::Name => {
101 write!(out, "unit-width-full-name ")?;
102 }
103 }
104 match sign {
105 CurrencySignMode::Standard => {}
106 CurrencySignMode::Accounting => {
107 write!(out, "sign-accounting ")?;
108 }
109 }
110 }
111 NumberStyle::Percent => write!(out, "percent")?,
112 NumberStyle::Unit { identifier, style } => {
113 write!(out, "unit/{} ", identifier)?;
114 match style {
115 UnitDisplayStyle::Short => {
116 write!(out, "unit-width-short ")?;
117 }
118 UnitDisplayStyle::Narrow => {
119 write!(out, "unit-width-narrow ")?;
120 }
121 UnitDisplayStyle::Long => {
122 write!(out, "unit-width-full-name ")?;
123 }
124 }
125 }
126 }
127
128 if let Some(min_int_digits) = format.minimum_integer_digits.as_ref() {
129 if *min_int_digits > 0 {
130 write!(
131 out,
132 "integer-width/*{:0>width$} ",
133 "",
134 width = *min_int_digits
135 )?;
136 }
137 }
138
139 let min_frac = format.minimum_fraction_digits.clone();
140 let max_frac = format.maximum_fraction_digits.clone();
141
142 if let (Some(min_frac), Some(max_frac)) = (min_frac, max_frac) {
143 if min_frac == max_frac {
144 write!(out, ".{}", "0".repeat(min_frac))?;
145 } else {
146 write!(
147 out,
148 ".{}{}",
149 "0".repeat(min_frac),
150 "#".repeat(max_frac - min_frac)
151 )?;
152 }
153 } else if let Some(min_frac) = min_frac {
154 write!(out, ".{}*", "0".repeat(min_frac))?;
155 } else if let Some(max_frac) = max_frac {
156 write!(out, ".{}", "#".repeat(max_frac))?;
157 }
158
159 let min_sig = format.minimum_significant_digits.clone();
160 let max_sig = format.maximum_significant_digits.clone();
161
162 if let (Some(min_sig), Some(max_sig)) = (min_sig, max_sig) {
164 if min_sig == max_sig {
165 write!(out, "{}", "@".repeat(min_sig))?;
166 } else {
167 write!(
168 out,
169 "@{}{}",
170 "@".repeat(min_sig - 1),
171 "#".repeat(max_sig - min_sig)
172 )?;
173 }
174 } else if let Some(min_sig) = min_sig {
175 write!(out, "@{}*", "@".repeat(min_sig - 1))?;
176 } else if let Some(max_sig) = max_sig {
177 write!(out, "@{}", "#".repeat(max_sig - 1))?;
178 }
179
180 Ok(out)
181 }
182
183 #[cfg(test)]
184 mod test {
185 use fluent_static_value::{
186 number::format::{
187 CurrencyCode, CurrencyDisplayStyle, CurrencySignMode, GroupingStyle, NumberStyle,
188 UnitDisplayStyle, UnitIdentifier,
189 },
190 Number, NumberFormat,
191 };
192
193 use crate::number::make_icu_skeleton;
194
195 use super::format_number;
196
197 #[test]
198 fn default_format() {
199 let mut s = String::new();
200 format_number(
201 "en-US",
202 &Number::from(42),
203 &Some(NumberFormat::default()),
204 &mut s,
205 )
206 .expect("Number to be formatted");
207
208 assert_eq!("42", s);
209 }
210
211 #[test]
212 fn test_currency_format() {
213 let mut s = String::new();
214 format_number(
215 "en-US",
216 &Number::from(20),
217 &Some(NumberFormat {
218 style: NumberStyle::Currency {
219 code: CurrencyCode::USD,
220 style: CurrencyDisplayStyle::Name,
221 sign: CurrencySignMode::default(),
222 },
223 ..Default::default()
224 }),
225 &mut s,
226 )
227 .expect("Number to be formatted");
228
229 assert_eq!("20.00 US dollars", s);
230 }
231
232 #[test]
233 fn test_currency_symbol() {
234 let mut s = String::new();
235 format_number(
236 "en-US",
237 &Number::from(20),
238 &Some(NumberFormat {
239 style: NumberStyle::Currency {
240 code: CurrencyCode::USD,
241 style: CurrencyDisplayStyle::Symbol,
242 sign: CurrencySignMode::default(),
243 },
244 ..Default::default()
245 }),
246 &mut s,
247 )
248 .expect("Number to be formatted");
249
250 assert_eq!("$20.00", s);
251 }
252
253 #[test]
254 fn test_currency_narrow_symbol() {
255 let mut s = String::new();
256 format_number(
257 "en-US",
258 &Number::from(20),
259 &Some(NumberFormat {
260 style: NumberStyle::Currency {
261 code: CurrencyCode::USD,
262 style: CurrencyDisplayStyle::NarrowSymbol,
263 sign: CurrencySignMode::default(),
264 },
265 ..Default::default()
266 }),
267 &mut s,
268 )
269 .expect("Number to be formatted");
270
271 assert_eq!("$20.00", s);
272 }
273
274 #[test]
275 fn test_unit_short() {
276 let mut s = String::new();
277 format_number(
278 "en-US",
279 &Number::from(20),
280 &Some(NumberFormat {
281 style: NumberStyle::Unit {
282 identifier: UnitIdentifier::Meter,
283 style: UnitDisplayStyle::Short,
284 },
285 ..Default::default()
286 }),
287 &mut s,
288 )
289 .expect("Number to be formatted");
290
291 assert_eq!("20 m", s);
292 }
293
294 #[test]
295 fn test_unit_long() {
296 let mut s = String::new();
297 format_number(
298 "en-US",
299 &Number::from(20),
300 &Some(NumberFormat {
301 style: NumberStyle::Unit {
302 identifier: UnitIdentifier::Meter,
303 style: UnitDisplayStyle::Long,
304 },
305 ..Default::default()
306 }),
307 &mut s,
308 )
309 .expect("Number to be formatted");
310
311 assert_eq!("20 meters", s);
312 }
313
314 #[test]
315 fn test_unit_narrow() {
316 let mut s = String::new();
317 format_number(
318 "en-US",
319 &Number::from(20),
320 &Some(NumberFormat {
321 style: NumberStyle::Unit {
322 identifier: UnitIdentifier::Meter,
323 style: UnitDisplayStyle::Narrow,
324 },
325 ..Default::default()
326 }),
327 &mut s,
328 )
329 .expect("Number to be formatted");
330
331 assert_eq!("20m", s);
332 }
333
334 #[test]
335 fn test_unit_compound_short() {
336 let mut s = String::new();
337 format_number(
338 "en-US",
339 &Number::from(20),
340 &Some(NumberFormat {
341 style: NumberStyle::Unit {
342 identifier: UnitIdentifier::Mile.per(UnitIdentifier::Hour),
343 style: UnitDisplayStyle::Short,
344 },
345 ..Default::default()
346 }),
347 &mut s,
348 )
349 .expect("Number to be formatted");
350
351 assert_eq!("20 mph", s);
352 }
353
354 #[test]
355 fn test_min_integer() {
356 let mut s = String::new();
357 format_number(
358 "en-US",
359 &Number::from(20),
360 &Some(NumberFormat {
361 minimum_integer_digits: Some(5),
362 use_grouping: GroupingStyle::Off,
363 ..Default::default()
364 }),
365 &mut s,
366 )
367 .expect("Number to be formatted");
368
369 assert_eq!("00020", s);
370 }
371
372 #[test]
373 fn test_fraction_digits() {
374 let test_data: Vec<(f64, Option<usize>, Option<usize>, &str)> = vec![
375 (0.12345, Some(2), None, "0.12345"),
376 (0.12345, None, Some(2), "0.12"),
377 (0.1, Some(3), None, "0.100"),
378 (0.12345, Some(3), Some(4), "0.1234"),
379 ];
380
381 for (n, min, max, expected) in test_data {
382 let mut s = String::new();
383 format_number(
384 "en-US",
385 &Number::from(n),
386 &Some(NumberFormat {
387 use_grouping: GroupingStyle::Off,
388 minimum_fraction_digits: min.clone(),
389 maximum_fraction_digits: max.clone(),
390 ..Default::default()
391 }),
392 &mut s,
393 )
394 .expect("Number to be formatted");
395
396 assert_eq!(
397 expected,
398 s,
399 "n={}, min={}, max={}",
400 n,
401 min.map(|min| min.to_string()).unwrap_or_default(),
402 max.map(|max| max.to_string()).unwrap_or_default()
403 );
404 }
405 }
406
407 #[test]
408 fn test_significant_digits() {
409 let test_data: Vec<(f64, Option<usize>, Option<usize>, &str)> = vec![
410 (123.45, None, None, "123.45"),
411 (0.12345, None, None, "0.12345"),
412 (123.45, Some(2), None, "123.45"),
413 (12.345, Some(3), None, "12.345"),
414 (1.2345, Some(4), None, "1.2345"),
415 (0.12345, Some(5), None, "0.12345"),
416 (0.001234, Some(3), None, "0.001234"),
417 (123.45, None, Some(2), "120"),
418 (12.345, None, Some(3), "12.3"),
419 (0.12345, None, Some(5), "0.12345"),
421 (0.001234, None, Some(3), "0.00123"),
422 (12.345, Some(3), Some(5), "12.345"),
424 (1.2345, Some(4), Some(6), "1.2345"),
425 (0.001234, Some(2), Some(3), "0.00123"),
427 (0.0, Some(2), None, "0.0"),
428 (0.0, None, Some(2), "0"),
429 (0.0, Some(2), Some(3), "0.0"),
430 (123.45, Some(3), Some(3), "123"),
431 (0.12345, Some(3), Some(3), "0.123"),
432 ];
433
434 let mut i = 0;
435
436 for (n, min, max, expected) in test_data {
437 let mut s = String::new();
438 let format = NumberFormat {
439 use_grouping: GroupingStyle::Off,
440 minimum_significant_digits: min.clone(),
441 maximum_significant_digits: max.clone(),
442 ..Default::default()
443 };
444
445 format_number("en-US", &Number::from(n), &Some(format.clone()), &mut s)
446 .expect("Number to be formatted");
447
448 assert_eq!(
449 expected,
450 s,
451 "{}: n={}, min={}, max={}, skel={}",
452 i,
453 n,
454 min.map(|min| min.to_string()).unwrap_or_default(),
455 max.map(|max| max.to_string()).unwrap_or_default(),
456 make_icu_skeleton(&format).unwrap()
457 );
458
459 i = i + 1;
460 }
461 }
462 }
463}