1use crate::model::{Instrument, SeriesDisplayFormat, SeriesFormat, percent_change};
14use std::sync::Arc;
15
16const MAX_PRECISION: usize = 324;
18
19pub(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#[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#[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
48pub(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
77pub 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
91pub trait PriceFormatter: Send + Sync {
97 fn format(&self, price: f64) -> String;
105
106 fn format_tick(&self, price: f64) -> String {
109 self.format(price)
110 }
111
112 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#[derive(Debug, Clone)]
140pub struct DefaultPriceFormatter {
141 pub precision: usize,
143
144 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 pub fn with_precision(precision: usize) -> Self {
160 Self {
161 precision,
162 min_precision: precision,
163 }
164 }
165
166 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 format_decimal(price, self.precision)
180 } else {
181 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#[derive(Debug, Clone)]
200pub struct PercentageFormatter {
201 pub precision: usize,
203
204 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 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#[derive(Debug, Clone)]
237pub struct CurrencyFormatter {
238 pub symbol: String,
240
241 pub precision: usize,
243
244 pub symbol_before: bool,
246
247 pub add_space: bool,
249
250 pub thousands_sep: Option<char>,
252
253 pub decimal_sep: char,
255}
256
257impl CurrencyFormatter {
258 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 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 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 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 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 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#[derive(Debug, Clone)]
336pub struct VolumeFormatter {
337 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#[derive(Debug, Clone)]
382pub struct ScientificFormatter {
383 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
408pub struct CustomPriceFormatter {
410 formatter: Arc<dyn Fn(f64) -> String + Send + Sync>,
411}
412
413impl CustomPriceFormatter {
414 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
437pub struct PriceFormatterBuilder {
439 precision: usize,
440 currency: Option<String>,
441 percentage: bool,
442 volume: bool,
443}
444
445impl PriceFormatterBuilder {
446 pub fn new() -> Self {
448 Self {
449 precision: 2,
450 currency: None,
451 percentage: false,
452 volume: false,
453 }
454 }
455
456 pub fn with_precision(mut self, precision: usize) -> Self {
458 self.precision = precision;
459 self
460 }
461
462 pub fn with_currency(mut self, symbol: impl Into<String>) -> Self {
464 self.currency = Some(symbol.into());
465 self
466 }
467
468 pub fn as_percentage(mut self) -> Self {
470 self.percentage = true;
471 self
472 }
473
474 pub fn as_volume(mut self) -> Self {
476 self.volume = true;
477 self
478 }
479
480 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#[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 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 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 (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}