fluent_static_formatter/
lib.rs

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                // TODO not supported by icu?
73                // write!(out, "")?;
74            }
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        // Significant digits
163        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                // (1.2345, None, Some(4), "1.235"),
420                (0.12345, None, Some(5), "0.12345"),
421                (0.001234, None, Some(3), "0.00123"),
422                // (123.45, Some(2), Some(4), "123.5"),
423                (12.345, Some(3), Some(5), "12.345"),
424                (1.2345, Some(4), Some(6), "1.2345"),
425                // (0.12345, Some(3), Some(4), "0.1235"),
426                (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}