Skip to main content

egui_charts/scales/
price_formatter.rs

1//! Price formatting for Y-axis labels.
2//!
3//! The [`PriceFormatter`] trait lets users control how numeric price values are
4//! rendered as text on the price axis.  The library ships with several built-in
5//! formatters ([`DefaultPriceFormatter`], [`CurrencyFormatter`],
6//! [`VolumeFormatter`], [`PercentageFormatter`], [`ScientificFormatter`]) and
7//! supports closures via [`CustomPriceFormatter`].
8//!
9//! Built-in formatters cap precision at 324 decimal places, enough to display the
10//! smallest positive `f64`. This also applies when public fields are set directly.
11//! Use [`PriceFormatterBuilder`] for a fluent construction API.
12
13use crate::model::{Instrument, SeriesDisplayFormat, SeriesFormat, percent_change};
14use std::sync::Arc;
15
16// Enough decimal places to keep even the smallest positive f64 from rounding to zero.
17const MAX_PRECISION: usize = 324;
18
19// Inspect the rounded digits so rounding ties follow Rust's formatter.
20pub(super) fn format_decimal(value: f64, precision: usize) -> String {
21    let precision = precision.min(MAX_PRECISION);
22    let mut formatted = format!("{value:.precision$}");
23    if formatted
24        .strip_prefix('-')
25        .is_some_and(|digits| digits.bytes().all(|b| b == b'0' || b == b'.'))
26    {
27        formatted.remove(0);
28    }
29    formatted
30}
31
32/// `value` as the whole multiple of `step` that `round` picks from `value / step`, written at the
33/// step's decimals.
34#[cfg(feature = "widget")]
35pub(crate) fn to_step(value: f64, step: f64, round: impl Fn(f64) -> f64) -> f64 {
36    let scale = 10f64.powi(decimals(step) as i32);
37    (round(value / step) * step * scale).round() / scale
38}
39
40/// Decimals needed to write `step`, such as 2 for 0.25.
41#[cfg(feature = "widget")]
42pub(crate) fn decimals(step: f64) -> usize {
43    let text = format!("{step}");
44    text.split_once('.')
45        .map_or(0, |(_, fraction)| fraction.len())
46}
47
48/// Writes `separator` between the groups of three digits of the integer part of `text`, its
49/// first run of ASCII digits: `-42541.77` becomes `-42,541.77` with `','`. Text whose integer
50/// part has three digits or fewer, text without digits (`NaN`, `inf`) and a `None` separator
51/// come back unchanged, without allocating.
52pub(crate) fn group_thousands(text: String, separator: Option<char>) -> String {
53    let Some(separator) = separator else {
54        return text;
55    };
56    let Some(start) = text.find(|c: char| c.is_ascii_digit()) else {
57        return text;
58    };
59    let digits = text[start..]
60        .find(|c: char| !c.is_ascii_digit())
61        .unwrap_or(text.len() - start);
62    if digits <= 3 {
63        return text;
64    }
65    let mut grouped = String::with_capacity(text.len() + (digits - 1) / 3 * separator.len_utf8());
66    grouped.push_str(&text[..start]);
67    for (index, digit) in text[start..start + digits].chars().enumerate() {
68        if index > 0 && (digits - index) % 3 == 0 {
69            grouped.push(separator);
70        }
71        grouped.push(digit);
72    }
73    grouped.push_str(&text[start + digits..]);
74    grouped
75}
76
77/// Instrument decimals, falling back to the decimal places needed to write its minimum tick.
78/// Invalid tick sizes are ignored; explicit decimals take precedence.
79pub fn instrument_price_precision(instrument: &Instrument) -> Option<u8> {
80    instrument.price_decimals.or_else(|| {
81        let tick = instrument
82            .tick_size
83            .filter(|tick| tick.is_finite() && *tick > 0.0)?;
84        let text = tick.to_string();
85        Some(text.split_once('.').map_or(0, |(_, decimals)| {
86            decimals.len().min(usize::from(u8::MAX)) as u8
87        }))
88    })
89}
90
91/// Trait for custom price formatting on the Y axis.
92///
93/// Implement this trait to control how numeric prices are displayed as labels.
94/// All formatters must be `Send + Sync` so they can be shared across the
95/// render pipeline.
96pub trait PriceFormatter: Send + Sync {
97    /// Format a price value into a display string
98    ///
99    /// # Arguments
100    /// * `price` - The price value to format
101    ///
102    /// # Returns
103    /// Formatted string to display on the price axis
104    fn format(&self, price: f64) -> String;
105
106    /// Format a price axis tick. Defaults to [`format`](Self::format); override it
107    /// to write ticks differently from crosshair and price tag labels.
108    fn format_tick(&self, price: f64) -> String {
109        self.format(price)
110    }
111
112    /// Clone this formatter into a Box
113    fn clone_box(&self) -> Box<dyn PriceFormatter>;
114}
115
116impl PriceFormatter for SeriesDisplayFormat {
117    fn format(&self, value: f64) -> String {
118        let step = self.min_move;
119        let rounded = (value / step).round() * step;
120        let value = if step.is_finite() && step > 0.0 && rounded.is_finite() {
121            rounded
122        } else {
123            value
124        };
125        let precision = usize::from(self.precision);
126        match self.kind {
127            SeriesFormat::Price => DefaultPriceFormatter::with_precision(precision).format(value),
128            SeriesFormat::Volume => VolumeFormatter { precision }.format(value),
129            SeriesFormat::Percentage => format!("{}%", format_decimal(value, precision)),
130        }
131    }
132
133    fn clone_box(&self) -> Box<dyn PriceFormatter> {
134        Box::new(*self)
135    }
136}
137
138/// Default price formatter with configurable precision
139#[derive(Debug, Clone)]
140pub struct DefaultPriceFormatter {
141    /// Number of decimal places, capped at 324 when formatting.
142    pub precision: usize,
143
144    /// Min number of decimal places (0 = auto)
145    pub min_precision: usize,
146}
147
148impl Default for DefaultPriceFormatter {
149    fn default() -> Self {
150        Self {
151            precision: 2,
152            min_precision: 0,
153        }
154    }
155}
156
157impl DefaultPriceFormatter {
158    /// Create formatter with fixed precision
159    pub fn with_precision(precision: usize) -> Self {
160        Self {
161            precision,
162            min_precision: precision,
163        }
164    }
165
166    /// Create formatter with auto precision (shows significant digits)
167    pub fn auto() -> Self {
168        Self {
169            precision: 8,
170            min_precision: 0,
171        }
172    }
173}
174
175impl PriceFormatter for DefaultPriceFormatter {
176    fn format(&self, price: f64) -> String {
177        if self.min_precision > 0 {
178            // Fixed precision
179            format_decimal(price, self.precision)
180        } else {
181            // Auto precision - remove trailing zeros
182            let formatted = format_decimal(price, self.precision);
183            if self.precision == 0 {
184                return formatted;
185            }
186            formatted
187                .trim_end_matches('0')
188                .trim_end_matches('.')
189                .to_string()
190        }
191    }
192
193    fn clone_box(&self) -> Box<dyn PriceFormatter> {
194        Box::new(self.clone())
195    }
196}
197
198/// Percentage price formatter
199#[derive(Debug, Clone)]
200pub struct PercentageFormatter {
201    /// Number of decimal places for percentage, capped at 324 when formatting.
202    pub precision: usize,
203
204    /// Base value for percentage calculation (100.0 = 100%)
205    pub base_val: f64,
206}
207
208impl Default for PercentageFormatter {
209    fn default() -> Self {
210        Self {
211            precision: 2,
212            base_val: 100.0,
213        }
214    }
215}
216
217impl PriceFormatter for PercentageFormatter {
218    fn format(&self, price: f64) -> String {
219        // A zero base has no defined percentage: percent_change gives a neutral 0%
220        // rather than "inf%"/"NaN%" on the axis.
221        let percentage = percent_change(self.base_val, price);
222        let formatted = format_decimal(percentage, self.precision);
223        let sign = if formatted.starts_with('-') { "" } else { "+" };
224        format!("{sign}{formatted}%")
225    }
226
227    fn clone_box(&self) -> Box<dyn PriceFormatter> {
228        Box::new(self.clone())
229    }
230}
231
232/// Currency formatter with symbol.
233///
234/// If the thousands and decimal separators match, grouping is omitted so an amount
235/// never uses the same character for both separators, including after direct field edits.
236#[derive(Debug, Clone)]
237pub struct CurrencyFormatter {
238    /// Currency symbol (e.g., "$", "€", "£", "¥")
239    pub symbol: String,
240
241    /// Number of decimal places, capped at 324 when formatting.
242    pub precision: usize,
243
244    /// Whether to place symbol before or after price
245    pub symbol_before: bool,
246
247    /// Whether to add space between symbol and value
248    pub add_space: bool,
249
250    /// Thousands separator (e.g., "," for US, "." for EU)
251    pub thousands_sep: Option<char>,
252
253    /// Decimal separator (e.g., '.' for US, ',' for EU).
254    pub decimal_sep: char,
255}
256
257impl CurrencyFormatter {
258    /// Create USD formatter
259    pub fn usd() -> Self {
260        Self {
261            symbol: "$".to_string(),
262            precision: 2,
263            symbol_before: true,
264            add_space: false,
265            thousands_sep: Some(','),
266            decimal_sep: '.',
267        }
268    }
269
270    /// Create EUR formatter
271    pub fn eur() -> Self {
272        Self {
273            symbol: "€".to_string(),
274            precision: 2,
275            symbol_before: false,
276            add_space: true,
277            thousands_sep: Some('.'),
278            decimal_sep: ',',
279        }
280    }
281
282    /// Create BTC formatter
283    pub fn btc() -> Self {
284        Self {
285            symbol: "₿".to_string(),
286            precision: 8,
287            symbol_before: false,
288            add_space: true,
289            thousands_sep: Some(','),
290            decimal_sep: '.',
291        }
292    }
293
294    /// Create custom currency formatter
295    pub fn new(symbol: impl Into<String>, precision: usize) -> Self {
296        Self {
297            symbol: symbol.into(),
298            precision,
299            symbol_before: true,
300            add_space: false,
301            thousands_sep: Some(','),
302            decimal_sep: '.',
303        }
304    }
305}
306
307impl PriceFormatter for CurrencyFormatter {
308    fn format(&self, price: f64) -> String {
309        // The decimal point is replaced first, so a '.' thousands separator is never confused with it.
310        let val_str = group_thousands(
311            format_decimal(price, self.precision).replace('.', &self.decimal_sep.to_string()),
312            self.thousands_sep.filter(|sep| *sep != self.decimal_sep),
313        );
314
315        let space = if self.add_space { " " } else { "" };
316
317        if self.symbol_before {
318            // A negative amount reads "-$1,234.50": the sign moves in front of
319            // the symbol instead of between it and the digits.
320            match val_str.strip_prefix('-') {
321                Some(digits) => format!("-{}{}{}", self.symbol, space, digits),
322                None => format!("{}{}{}", self.symbol, space, val_str),
323            }
324        } else {
325            format!("{}{}{}", val_str, space, self.symbol)
326        }
327    }
328
329    fn clone_box(&self) -> Box<dyn PriceFormatter> {
330        Box::new(self.clone())
331    }
332}
333
334/// Volume formatter (K, M, B suffixes)
335#[derive(Debug, Clone)]
336pub struct VolumeFormatter {
337    /// Number of decimal places, capped at 324 when formatting.
338    pub precision: usize,
339}
340
341impl Default for VolumeFormatter {
342    fn default() -> Self {
343        Self { precision: 2 }
344    }
345}
346
347impl PriceFormatter for VolumeFormatter {
348    fn format(&self, value: f64) -> String {
349        let abs_val = value.abs();
350        let (mut unit, mut suffix) = if abs_val >= 1_000_000_000.0 {
351            (1_000_000_000.0, "B")
352        } else if abs_val >= 1_000_000.0 {
353            (1_000_000.0, "M")
354        } else if abs_val >= 1_000.0 {
355            (1_000.0, "K")
356        } else {
357            (1.0, "")
358        };
359        let mut formatted = format_decimal(value / unit, self.precision);
360        if unit < 1_000_000_000.0
361            && formatted.trim_start_matches('-').split('.').next() == Some("1000")
362        {
363            unit *= 1_000.0;
364            suffix = match suffix {
365                "" => "K",
366                "K" => "M",
367                _ => "B",
368            };
369            formatted = format_decimal(value / unit, self.precision);
370        }
371        formatted.push_str(suffix);
372        formatted
373    }
374
375    fn clone_box(&self) -> Box<dyn PriceFormatter> {
376        Box::new(self.clone())
377    }
378}
379
380/// Scientific notation formatter
381#[derive(Debug, Clone)]
382pub struct ScientificFormatter {
383    /// Number of digits after the decimal point, capped at 324 when formatting.
384    pub precision: usize,
385}
386
387impl Default for ScientificFormatter {
388    fn default() -> Self {
389        Self { precision: 3 }
390    }
391}
392
393impl PriceFormatter for ScientificFormatter {
394    fn format(&self, price: f64) -> String {
395        let price = if price == 0.0 { 0.0 } else { price };
396        format!(
397            "{:.prec$e}",
398            price,
399            prec = self.precision.min(MAX_PRECISION)
400        )
401    }
402
403    fn clone_box(&self) -> Box<dyn PriceFormatter> {
404        Box::new(self.clone())
405    }
406}
407
408/// Custom price formatter using a closure
409pub struct CustomPriceFormatter {
410    formatter: Arc<dyn Fn(f64) -> String + Send + Sync>,
411}
412
413impl CustomPriceFormatter {
414    /// Create a new custom formatter from a closure
415    pub fn new<F>(formatter: F) -> Self
416    where
417        F: Fn(f64) -> String + Send + Sync + 'static,
418    {
419        Self {
420            formatter: Arc::new(formatter),
421        }
422    }
423}
424
425impl PriceFormatter for CustomPriceFormatter {
426    fn format(&self, price: f64) -> String {
427        (self.formatter)(price)
428    }
429
430    fn clone_box(&self) -> Box<dyn PriceFormatter> {
431        Box::new(Self {
432            formatter: Arc::clone(&self.formatter),
433        })
434    }
435}
436
437/// Builder for creating price formatters
438pub struct PriceFormatterBuilder {
439    precision: usize,
440    currency: Option<String>,
441    percentage: bool,
442    volume: bool,
443}
444
445impl PriceFormatterBuilder {
446    /// Create a new formatter builder
447    pub fn new() -> Self {
448        Self {
449            precision: 2,
450            currency: None,
451            percentage: false,
452            volume: false,
453        }
454    }
455
456    /// Set precision
457    pub fn with_precision(mut self, precision: usize) -> Self {
458        self.precision = precision;
459        self
460    }
461
462    /// Set currency symbol
463    pub fn with_currency(mut self, symbol: impl Into<String>) -> Self {
464        self.currency = Some(symbol.into());
465        self
466    }
467
468    /// Enable percentage formatting
469    pub fn as_percentage(mut self) -> Self {
470        self.percentage = true;
471        self
472    }
473
474    /// Enable volume formatting
475    pub fn as_volume(mut self) -> Self {
476        self.volume = true;
477        self
478    }
479
480    /// Build the formatter
481    pub fn build(self) -> Box<dyn PriceFormatter> {
482        if self.percentage {
483            Box::new(PercentageFormatter {
484                precision: self.precision,
485                base_val: 100.0,
486            })
487        } else if self.volume {
488            Box::new(VolumeFormatter {
489                precision: self.precision,
490            })
491        } else if let Some(symbol) = self.currency {
492            Box::new(CurrencyFormatter::new(symbol, self.precision))
493        } else {
494            Box::new(DefaultPriceFormatter::with_precision(self.precision))
495        }
496    }
497}
498
499impl Default for PriceFormatterBuilder {
500    fn default() -> Self {
501        Self::new()
502    }
503}
504
505/// Decimals for a price by its magnitude: 2 from 100 up, 4 from 1 up, 6 below 1.
506#[cfg(feature = "widget")]
507pub(crate) fn price_decimals(price: f64) -> usize {
508    if price.abs() >= 100.0 {
509        2
510    } else if price.abs() >= 1.0 {
511        4
512    } else {
513        6
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    #[test]
522    fn test_default_formatter() {
523        let formatter = DefaultPriceFormatter::default();
524        assert_eq!(formatter.format(123.456), "123.46");
525        assert_eq!(formatter.format(123.0), "123");
526        for value in [-0.0, -0.001] {
527            assert_eq!(formatter.format(value), "0");
528            assert_eq!(
529                DefaultPriceFormatter::with_precision(2).format(value),
530                "0.00"
531            );
532            for unit in [
533                super::super::StudyFormat::Price,
534                super::super::StudyFormat::Fixed(2),
535            ] {
536                assert_eq!(unit.format(value, None), "0.00");
537            }
538            assert_eq!(
539                super::super::StudyFormat::Percent.format(value, None),
540                "0.00%"
541            );
542        }
543        assert_eq!(formatter.format(-0.006), "-0.01");
544    }
545
546    #[test]
547    fn extreme_precision_is_bounded() {
548        let decimals = "0".repeat(324);
549        for precision in [324, 325, usize::MAX] {
550            assert_eq!(
551                DefaultPriceFormatter::with_precision(precision).format(100.0),
552                format!("100.{decimals}")
553            );
554            assert_eq!(
555                DefaultPriceFormatter {
556                    precision,
557                    min_precision: 0,
558                }
559                .format(100.0),
560                "100"
561            );
562            assert_eq!(
563                CurrencyFormatter::new("$", precision).format(100.0),
564                format!("$100.{decimals}")
565            );
566            assert_eq!(
567                PercentageFormatter {
568                    precision,
569                    base_val: 100.0
570                }
571                .format(200.0),
572                format!("+100.{decimals}%")
573            );
574            for (value, suffix) in [
575                (100.0, ""),
576                (100_000.0, "K"),
577                (100_000_000.0, "M"),
578                (100_000_000_000.0, "B"),
579            ] {
580                assert_eq!(
581                    VolumeFormatter { precision }.format(value),
582                    format!("100.{decimals}{suffix}")
583                );
584            }
585            assert_eq!(
586                ScientificFormatter { precision }.format(100.0),
587                format!("1.{decimals}e2")
588            );
589        }
590    }
591
592    #[test]
593    fn test_fixed_precision() {
594        let formatter = DefaultPriceFormatter::with_precision(4);
595        assert_eq!(formatter.format(123.456), "123.4560");
596        for (value, expected) in [(0.0, "0"), (10.0, "10"), (100.0, "100"), (-100.0, "-100")] {
597            assert_eq!(
598                DefaultPriceFormatter::with_precision(0).format(value),
599                expected
600            );
601        }
602    }
603
604    #[test]
605    fn test_percentage_formatter() {
606        assert_eq!(PercentageFormatter::default().format(105.0), "+5.00%");
607        // A spread keeps the sign of the change; a zero base has no defined percentage.
608        for (base_val, value, expected) in [
609            (100.0, 95.0, "-5.00%"),
610            (100.0, 99.999, "+0.00%"),
611            (100.0, 99.994, "-0.01%"),
612            (-10.0, -5.0, "+50.00%"),
613            (-10.0, -15.0, "-50.00%"),
614            (0.0, 123.0, "+0.00%"),
615        ] {
616            let formatter = PercentageFormatter {
617                precision: 2,
618                base_val,
619            };
620            assert_eq!(
621                formatter.format(value),
622                expected,
623                "base {base_val}, value {value}"
624            );
625        }
626    }
627
628    #[test]
629    fn test_currency_formatter() {
630        let usd = CurrencyFormatter::usd();
631        assert_eq!(usd.format(1234.56), "$1,234.56");
632
633        for (formatter, value, expected) in [
634            (CurrencyFormatter::usd(), 1234567.891, "$1,234,567.89"),
635            (CurrencyFormatter::eur(), 1234.56, "1.234,56 €"),
636            (CurrencyFormatter::eur(), -1234.5, "-1.234,50 €"),
637            (CurrencyFormatter::eur(), 0.5, "0,50 €"),
638            (CurrencyFormatter::eur(), 123.45, "123,45 €"),
639            (CurrencyFormatter::btc(), 0.12345678, "0.12345678 ₿"),
640            (
641                CurrencyFormatter {
642                    thousands_sep: None,
643                    ..CurrencyFormatter::eur()
644                },
645                1234.56,
646                "1234,56 €",
647            ),
648            (
649                CurrencyFormatter {
650                    decimal_sep: '.',
651                    ..CurrencyFormatter::eur()
652                },
653                1234.56,
654                "1234.56 €",
655            ),
656            (
657                CurrencyFormatter {
658                    decimal_sep: ',',
659                    ..CurrencyFormatter::usd()
660                },
661                1234.56,
662                "$1234,56",
663            ),
664            (
665                CurrencyFormatter {
666                    precision: 0,
667                    ..CurrencyFormatter::eur()
668                },
669                1234.56,
670                "1.235 €",
671            ),
672        ] {
673            assert_eq!(formatter.format(value), expected);
674        }
675    }
676
677    #[test]
678    fn test_currency_negative_amounts_sign_before_symbol_and_never_grouped() {
679        // Expected strings written by hand: the sign sits before the symbol when
680        // the symbol comes first, and no separator ever lands next to the sign.
681        let cases = [
682            (CurrencyFormatter::usd(), -123.45, "-$123.45"),
683            (CurrencyFormatter::usd(), -123456.0, "-$123,456.00"),
684            (CurrencyFormatter::new("CHF", 1), -1234.5, "-CHF1,234.5"),
685            (CurrencyFormatter::usd(), -0.5, "-$0.50"),
686            (CurrencyFormatter::usd(), 0.0, "$0.00"),
687            (CurrencyFormatter::usd(), -0.0, "$0.00"),
688            (CurrencyFormatter::usd(), -0.001, "$0.00"),
689            (CurrencyFormatter::usd(), -0.006, "-$0.01"),
690            (CurrencyFormatter::new("JPY", 0), -0.1, "JPY0"),
691            (CurrencyFormatter::new("JPY", 0), -0.6, "-JPY1"),
692            (CurrencyFormatter::btc(), -0.000000001, "0.00000000 ₿"),
693            (CurrencyFormatter::eur(), -0.001, "0,00 €"),
694            (
695                CurrencyFormatter {
696                    thousands_sep: None,
697                    ..CurrencyFormatter::usd()
698                },
699                -0.001,
700                "$0.00",
701            ),
702            (CurrencyFormatter::usd(), 123.45, "$123.45"),
703            // Symbol after the value keeps the sign on the number itself.
704            (CurrencyFormatter::btc(), -1234.5, "-1,234.50000000 ₿"),
705        ];
706        for (formatter, value, expected) in cases {
707            assert_eq!(formatter.format(value), expected);
708        }
709    }
710
711    #[test]
712    fn test_volume_formatter() {
713        let formatter = VolumeFormatter::default();
714        assert_eq!(formatter.format(1_500_000_000.0), "1.50B");
715        assert_eq!(formatter.format(2_500_000.0), "2.50M");
716        assert_eq!(formatter.format(3_500.0), "3.50K");
717        assert_eq!(formatter.format(500.0), "500.00");
718        for value in [-0.0, -0.001] {
719            assert_eq!(formatter.format(value), "0.00");
720        }
721        assert_eq!(formatter.format(-0.006), "-0.01");
722        assert_eq!(formatter.format(-3_500.0), "-3.50K");
723        for (precision, value, expected) in [
724            (2, 999.996, "1.00K"),
725            (2, 999_999.0, "1.00M"),
726            (2, 999_996_000.0, "1.00B"),
727            (2, 999.994, "999.99"),
728            (2, 999_994.0, "999.99K"),
729            (2, 999_994_000.0, "999.99M"),
730            (0, 999.6, "1K"),
731            (0, 999_600.0, "1M"),
732            (3, 999_999.4, "999.999K"),
733            (3, 999_999.6, "1.000M"),
734            (2, 999_999_999_999.0, "1000.00B"),
735        ] {
736            let formatter = VolumeFormatter { precision };
737            assert_eq!(formatter.format(value), expected);
738            assert_eq!(formatter.format(-value), format!("-{expected}"));
739        }
740    }
741
742    #[test]
743    fn test_scientific_formatter() {
744        let formatter = ScientificFormatter::default();
745        let result = formatter.format(0.000123);
746        assert_eq!(result, "1.230e-4");
747        assert_eq!(formatter.format(-0.0), "0.000e0");
748        assert_eq!(formatter.format(-0.001), "-1.000e-3");
749    }
750
751    #[test]
752    fn test_formatter_builder() {
753        let formatter = PriceFormatterBuilder::new().with_precision(4).build();
754        assert_eq!(formatter.format(123.456789), "123.4568");
755
756        let currency_formatter = PriceFormatterBuilder::new()
757            .with_currency("$")
758            .with_precision(2)
759            .build();
760        assert_eq!(currency_formatter.format(100.0), "$100.00");
761    }
762}