re_format/
lib.rs

1//! Miscellaneous tools to format and parse numbers, durations, etc.
2//!
3//! TODO(emilk): move some of this numeric formatting into `emath` so we can use it in `egui_plot`.
4
5mod time;
6
7use std::{cmp::PartialOrd, fmt::Display};
8
9pub use time::{format_timestamp_secs, next_grid_tick_magnitude_nanos, parse_timestamp_secs};
10
11// --- Numbers ---
12
13/// The minus character: <https://www.compart.com/en/unicode/U+2212>
14///
15/// Looks slightly different from the normal hyphen `-`.
16const MINUS: char = '−';
17
18// TODO(rust-num/num-traits#315): waiting for https://github.com/rust-num/num-traits/issues/315 to land
19pub trait UnsignedAbs {
20    /// An unsigned type which is large enough to hold the absolute value of `Self`.
21    type Unsigned;
22
23    /// Computes the absolute value of `self` without any wrapping or panicking.
24    fn unsigned_abs(self) -> Self::Unsigned;
25}
26
27impl UnsignedAbs for i8 {
28    type Unsigned = u8;
29
30    #[inline]
31    fn unsigned_abs(self) -> Self::Unsigned {
32        self.unsigned_abs()
33    }
34}
35
36impl UnsignedAbs for i16 {
37    type Unsigned = u16;
38
39    #[inline]
40    fn unsigned_abs(self) -> Self::Unsigned {
41        self.unsigned_abs()
42    }
43}
44
45impl UnsignedAbs for i32 {
46    type Unsigned = u32;
47
48    #[inline]
49    fn unsigned_abs(self) -> Self::Unsigned {
50        self.unsigned_abs()
51    }
52}
53
54impl UnsignedAbs for i64 {
55    type Unsigned = u64;
56
57    #[inline]
58    fn unsigned_abs(self) -> Self::Unsigned {
59        self.unsigned_abs()
60    }
61}
62
63impl UnsignedAbs for i128 {
64    type Unsigned = u128;
65
66    #[inline]
67    fn unsigned_abs(self) -> Self::Unsigned {
68        self.unsigned_abs()
69    }
70}
71
72impl UnsignedAbs for isize {
73    type Unsigned = usize;
74
75    #[inline]
76    fn unsigned_abs(self) -> Self::Unsigned {
77        self.unsigned_abs()
78    }
79}
80
81/// Pretty format a signed number by using thousands separators for readability.
82///
83/// The returned value is for human eyes only, and can not be parsed
84/// by the normal `usize::from_str` function.
85pub fn format_int<Int>(number: Int) -> String
86where
87    Int: Display + PartialOrd + num_traits::Zero + UnsignedAbs,
88    Int::Unsigned: Display + num_traits::Unsigned,
89{
90    if number < Int::zero() {
91        format!("{MINUS}{}", format_uint(number.unsigned_abs()))
92    } else {
93        add_thousands_separators(&number.to_string())
94    }
95}
96
97/// Pretty format an unsigned integer by using thousands separators for readability.
98///
99/// The returned value is for human eyes only, and can not be parsed
100/// by the normal `usize::from_str` function.
101#[allow(clippy::needless_pass_by_value)]
102pub fn format_uint<Uint>(number: Uint) -> String
103where
104    Uint: Display + num_traits::Unsigned,
105{
106    add_thousands_separators(&number.to_string())
107}
108
109/// Add thousands separators to a number, every three steps,
110/// counting from the last character.
111fn add_thousands_separators(number: &str) -> String {
112    let mut chars = number.chars().rev().peekable();
113
114    let mut result = vec![];
115    while chars.peek().is_some() {
116        if !result.is_empty() {
117            // thousands-deliminator:
118            let thin_space = '\u{2009}'; // https://en.wikipedia.org/wiki/Thin_space
119            result.push(thin_space);
120        }
121        for _ in 0..3 {
122            if let Some(c) = chars.next() {
123                result.push(c);
124            }
125        }
126    }
127
128    result.reverse();
129    result.into_iter().collect()
130}
131
132#[test]
133fn test_format_uint() {
134    assert_eq!(format_uint(42_u32), "42");
135    assert_eq!(format_uint(999_u32), "999");
136    assert_eq!(format_uint(1_000_u32), "1 000");
137    assert_eq!(format_uint(123_456_u32), "123 456");
138    assert_eq!(format_uint(1_234_567_u32), "1 234 567");
139}
140
141/// Options for how to format a floating point number, e.g. an [`f64`].
142#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
143pub struct FloatFormatOptions {
144    /// Always show the sign, even if it is positive (`+`).
145    pub always_sign: bool,
146
147    /// Maximum digits of precision to use.
148    ///
149    /// This includes both the integer part and the fractional part.
150    pub precision: usize,
151
152    /// Max number of decimals to show after the decimal point.
153    ///
154    /// If not specified, [`Self::precision`] is used instead.
155    pub num_decimals: Option<usize>,
156
157    pub strip_trailing_zeros: bool,
158
159    /// Only add thousands separators to decimals if there are at least this many decimals.
160    pub min_decimals_for_thousands_separators: usize,
161}
162
163impl FloatFormatOptions {
164    /// Default options for formatting an [`half::f16`].
165    #[allow(non_upper_case_globals)]
166    pub const DEFAULT_f16: Self = Self {
167        always_sign: false,
168        precision: 5,
169        num_decimals: None,
170        strip_trailing_zeros: true,
171        min_decimals_for_thousands_separators: 6,
172    };
173
174    /// Default options for formatting an [`f32`].
175    #[allow(non_upper_case_globals)]
176    pub const DEFAULT_f32: Self = Self {
177        always_sign: false,
178        precision: 7,
179        num_decimals: None,
180        strip_trailing_zeros: true,
181        min_decimals_for_thousands_separators: 6,
182    };
183
184    /// Default options for formatting an [`f64`].
185    #[allow(non_upper_case_globals)]
186    pub const DEFAULT_f64: Self = Self {
187        always_sign: false,
188        precision: 15,
189        num_decimals: None,
190        strip_trailing_zeros: true,
191        min_decimals_for_thousands_separators: 6,
192    };
193
194    /// Always show the sign, even if it is positive (`+`).
195    #[inline]
196    pub fn with_always_sign(mut self, always_sign: bool) -> Self {
197        self.always_sign = always_sign;
198        self
199    }
200
201    /// Show at most this many digits of precision,
202    /// including both the integer part and the fractional part.
203    #[inline]
204    pub fn with_precision(mut self, precision: usize) -> Self {
205        self.precision = precision;
206        self
207    }
208
209    /// Max number of decimals to show after the decimal point.
210    ///
211    /// If not specified, [`Self::precision`] is used instead.
212    #[inline]
213    pub fn with_decimals(mut self, num_decimals: usize) -> Self {
214        self.num_decimals = Some(num_decimals);
215        self
216    }
217
218    /// Strip trailing zeros from decimal expansion?
219    #[inline]
220    pub fn with_strip_trailing_zeros(mut self, strip_trailing_zeros: bool) -> Self {
221        self.strip_trailing_zeros = strip_trailing_zeros;
222        self
223    }
224
225    /// The returned value is for human eyes only, and can not be parsed
226    /// by the normal `f64::from_str` function.
227    pub fn format(&self, value: impl Into<f64>) -> String {
228        self.format_f64(value.into())
229    }
230
231    fn format_f64(&self, mut value: f64) -> String {
232        fn reverse(s: &str) -> String {
233            s.chars().rev().collect()
234        }
235
236        let Self {
237            always_sign,
238            precision,
239            num_decimals,
240            strip_trailing_zeros,
241            min_decimals_for_thousands_separators,
242        } = *self;
243
244        if value.is_nan() {
245            return "NaN".to_owned();
246        }
247
248        let sign = if value < 0.0 {
249            value = -value;
250            "−" // NOTE: the minus character: <https://www.compart.com/en/unicode/U+2212>
251        } else if always_sign {
252            "+"
253        } else {
254            ""
255        };
256
257        let abs_string = if value == f64::INFINITY {
258            "∞".to_owned()
259        } else {
260            let magnitude = value.log10();
261            let max_decimals = precision as f64 - magnitude.max(0.0);
262
263            if max_decimals < 0.0 {
264                // A very large number (more digits than we have precision),
265                // so use scientific notation.
266                // TODO(emilk): nice formatting of scientific notation with thousands separators
267                format!("{:.*e}", precision.saturating_sub(1), value)
268            } else {
269                let max_decimals = max_decimals as usize;
270
271                let num_decimals = if let Some(num_decimals) = num_decimals {
272                    num_decimals.min(max_decimals)
273                } else {
274                    max_decimals
275                };
276
277                let mut formatted = format!("{value:.num_decimals$}");
278
279                if strip_trailing_zeros && formatted.contains('.') {
280                    while formatted.ends_with('0') {
281                        formatted.pop();
282                    }
283                    if formatted.ends_with('.') {
284                        formatted.pop();
285                    }
286                }
287
288                if let Some(dot) = formatted.find('.') {
289                    let integer_part = &formatted[..dot];
290                    let fractional_part = &formatted[dot + 1..];
291                    // let fractional_part = &fractional_part[..num_decimals.min(fractional_part.len())];
292
293                    let integer_part = add_thousands_separators(integer_part);
294
295                    if fractional_part.len() < min_decimals_for_thousands_separators {
296                        format!("{integer_part}.{fractional_part}")
297                    } else {
298                        // For the fractional part we should start counting thousand separators from the _front_, so we reverse:
299                        let fractional_part =
300                            reverse(&add_thousands_separators(&reverse(fractional_part)));
301                        format!("{integer_part}.{fractional_part}")
302                    }
303                } else {
304                    add_thousands_separators(&formatted) // it's an integer
305                }
306            }
307        };
308
309        format!("{sign}{abs_string}")
310    }
311}
312
313/// Format a number with about 15 decimals of precision.
314///
315/// The returned value is for human eyes only, and can not be parsed
316/// by the normal `f64::from_str` function.
317pub fn format_f64(value: f64) -> String {
318    FloatFormatOptions::DEFAULT_f64.format(value)
319}
320
321/// Format a number with about 7 decimals of precision.
322///
323/// The returned value is for human eyes only, and can not be parsed
324/// by the normal `f64::from_str` function.
325pub fn format_f32(value: f32) -> String {
326    FloatFormatOptions::DEFAULT_f32.format(value)
327}
328
329/// Format a number with about 5 decimals of precision.
330///
331/// The returned value is for human eyes only, and can not be parsed
332/// by the normal `f64::from_str` function.
333pub fn format_f16(value: half::f16) -> String {
334    FloatFormatOptions::DEFAULT_f16.format(value)
335}
336
337/// Format a latitude or longitude value.
338///
339/// For human eyes only.
340pub fn format_lat_lon(value: f64) -> String {
341    format!(
342        "{}°",
343        FloatFormatOptions {
344            always_sign: true,
345            precision: 10,
346            num_decimals: Some(6),
347            strip_trailing_zeros: false,
348            min_decimals_for_thousands_separators: 10,
349        }
350        .format_f64(value)
351    )
352}
353
354#[test]
355fn test_format_f32() {
356    let cases = [
357        (f32::NAN, "NaN"),
358        (f32::INFINITY, "∞"),
359        (f32::NEG_INFINITY, "−∞"),
360        (0.0, "0"),
361        (42.0, "42"),
362        (10_000.0, "10 000"),
363        (1_000_000.0, "1 000 000"),
364        (10_000_000.0, "10 000 000"),
365        (11_000_000.0, "1.100000e7"),
366        (-42.0, "−42"),
367        (-4.20, "−4.2"),
368        (123_456.78, "123 456.8"),
369        (78.4321, "78.4321"), // min_decimals_for_thousands_separators
370        (-std::f32::consts::PI, "−3.141 593"),
371        (-std::f32::consts::PI * 1e6, "−3 141 593"),
372        (-std::f32::consts::PI * 1e20, "−3.141593e20"), // We switch to scientific notation to not show false precision
373    ];
374    for (value, expected) in cases {
375        let got = format_f32(value);
376        assert!(
377            got == expected,
378            "Expected to format {value} as '{expected}', but got '{got}'"
379        );
380    }
381}
382
383#[test]
384fn test_format_f64() {
385    let cases = [
386        (f64::NAN, "NaN"),
387        (f64::INFINITY, "∞"),
388        (f64::NEG_INFINITY, "−∞"),
389        (0.0, "0"),
390        (42.0, "42"),
391        (-42.0, "−42"),
392        (-4.20, "−4.2"),
393        (123_456_789.0, "123 456 789"),
394        (123_456_789.123_45, "123 456 789.12345"), // min_decimals_for_thousands_separators
395        (0.0000123456789, "0.000 012 345 678 9"),
396        (0.123456789, "0.123 456 789"),
397        (1.23456789, "1.234 567 89"),
398        (12.3456789, "12.345 678 9"),
399        (123.456789, "123.456 789"),
400        (1234.56789, "1 234.56789"), // min_decimals_for_thousands_separators
401        (12345.6789, "12 345.6789"), // min_decimals_for_thousands_separators
402        (78.4321, "78.4321"),        // min_decimals_for_thousands_separators
403        (-std::f64::consts::PI, "−3.141 592 653 589 79"),
404        (-std::f64::consts::PI * 1e6, "−3 141 592.653 589 79"),
405        (-std::f64::consts::PI * 1e20, "−3.14159265358979e20"), // We switch to scientific notation to not show false precision
406    ];
407    for (value, expected) in cases {
408        let got = format_f64(value);
409        assert!(
410            got == expected,
411            "Expected to format {value} as '{expected}', but got '{got}'"
412        );
413    }
414}
415
416#[test]
417fn test_format_f16() {
418    use half::f16;
419
420    let cases = [
421        (f16::from_f32(f32::NAN), "NaN"),
422        (f16::INFINITY, "∞"),
423        (f16::NEG_INFINITY, "−∞"),
424        (f16::ZERO, "0"),
425        (f16::from_f32(42.0), "42"),
426        (f16::from_f32(-42.0), "−42"),
427        (f16::from_f32(-4.20), "−4.1992"), // f16 precision limitation
428        (f16::from_f32(12_345.0), "12 344"), // f16 precision limitation
429        (f16::PI, "3.1406"),               // f16 precision limitation
430    ];
431    for (value, expected) in cases {
432        let got = format_f16(value);
433        assert_eq!(
434            got, expected,
435            "Expected to format {value} as '{expected}', but got '{got}'"
436        );
437    }
438}
439
440#[test]
441fn test_format_f64_custom() {
442    let cases = [(
443        FloatFormatOptions::DEFAULT_f64.with_decimals(2),
444        123.456789,
445        "123.46",
446    )];
447    for (options, value, expected) in cases {
448        let got = options.format(value);
449        assert!(
450            got == expected,
451            "Expected to format {value} as '{expected}', but got '{got}'. Options: {options:#?}"
452        );
453    }
454}
455
456/// Parses a number, ignoring whitespace (e.g. thousand separators),
457/// and treating the special minus character `MINUS` (−) as a minus sign.
458pub fn parse_f64(text: &str) -> Option<f64> {
459    let text: String = text
460        .chars()
461        // Ignore whitespace (trailing, leading, and thousands separators):
462        .filter(|c| !c.is_whitespace())
463        // Replace special minus character with normal minus (hyphen):
464        .map(|c| if c == '−' { '-' } else { c })
465        .collect();
466
467    text.parse().ok()
468}
469
470/// Parses a number, ignoring whitespace (e.g. thousand separators),
471/// and treating the special minus character `MINUS` (−) as a minus sign.
472pub fn parse_i64(text: &str) -> Option<i64> {
473    let text: String = text
474        .chars()
475        // Ignore whitespace (trailing, leading, and thousands separators):
476        .filter(|c| !c.is_whitespace())
477        // Replace special minus character with normal minus (hyphen):
478        .map(|c| if c == '−' { '-' } else { c })
479        .collect();
480
481    text.parse().ok()
482}
483
484/// Pretty format a large number by using SI notation (base 10), e.g.
485///
486/// ```
487/// # use re_format::approximate_large_number;
488/// assert_eq!(approximate_large_number(123 as _), "123");
489/// assert_eq!(approximate_large_number(12_345 as _), "12k");
490/// assert_eq!(approximate_large_number(1_234_567 as _), "1.2M");
491/// assert_eq!(approximate_large_number(123_456_789 as _), "123M");
492/// ```
493///
494/// Prefer to use [`format_uint`], which outputs an exact string,
495/// while still being readable thanks to half-width spaces used as thousands-separators.
496pub fn approximate_large_number(number: f64) -> String {
497    if number < 0.0 {
498        format!("{MINUS}{}", approximate_large_number(-number))
499    } else if number < 1000.0 {
500        format!("{number:.0}")
501    } else if number < 1_000_000.0 {
502        let decimals = (number < 10_000.0) as usize;
503        format!("{:.*}k", decimals, number / 1_000.0)
504    } else if number < 1_000_000_000.0 {
505        let decimals = (number < 10_000_000.0) as usize;
506        format!("{:.*}M", decimals, number / 1_000_000.0)
507    } else {
508        let decimals = (number < 10_000_000_000.0) as usize;
509        format!("{:.*}G", decimals, number / 1_000_000_000.0)
510    }
511}
512
513#[test]
514fn test_format_large_number() {
515    let test_cases = [
516        (999.0, "999"),
517        (1000.0, "1.0k"),
518        (1001.0, "1.0k"),
519        (999_999.0, "1000k"),
520        (1_000_000.0, "1.0M"),
521        (999_999_999.0, "1000M"),
522        (1_000_000_000.0, "1.0G"),
523        (999_999_999_999.0, "1000G"),
524        (1_000_000_000_000.0, "1000G"),
525        (123.0, "123"),
526        (12_345.0, "12k"),
527        (1_234_567.0, "1.2M"),
528        (123_456_789.0, "123M"),
529    ];
530
531    for (value, expected) in test_cases {
532        assert_eq!(expected, approximate_large_number(value));
533    }
534}
535
536// --- Bytes ---
537
538/// Pretty format a number of bytes by using SI notation (base2), e.g.
539///
540/// ```
541/// # use re_format::format_bytes;
542/// assert_eq!(format_bytes(123.0), "123 B");
543/// assert_eq!(format_bytes(12_345.0), "12.1 KiB");
544/// assert_eq!(format_bytes(1_234_567.0), "1.2 MiB");
545/// assert_eq!(format_bytes(123_456_789.0), "118 MiB");
546/// ```
547pub fn format_bytes(number_of_bytes: f64) -> String {
548    if number_of_bytes < 0.0 {
549        format!("{MINUS}{}", format_bytes(-number_of_bytes))
550    } else if number_of_bytes == 0.0 {
551        "0 B".to_owned()
552    } else if number_of_bytes < 1.0 {
553        format!("{number_of_bytes} B")
554    } else if number_of_bytes < 20.0 {
555        let is_integer = number_of_bytes.round() == number_of_bytes;
556        if is_integer {
557            format!("{number_of_bytes:.0} B")
558        } else {
559            format!("{number_of_bytes:.1} B")
560        }
561    } else if number_of_bytes < 10.0_f64.exp2() {
562        format!("{number_of_bytes:.0} B")
563    } else if number_of_bytes < 20.0_f64.exp2() {
564        let decimals = (10.0 * number_of_bytes < 20.0_f64.exp2()) as usize;
565        format!("{:.*} KiB", decimals, number_of_bytes / 10.0_f64.exp2())
566    } else if number_of_bytes < 30.0_f64.exp2() {
567        let decimals = (10.0 * number_of_bytes < 30.0_f64.exp2()) as usize;
568        format!("{:.*} MiB", decimals, number_of_bytes / 20.0_f64.exp2())
569    } else {
570        let decimals = (10.0 * number_of_bytes < 40.0_f64.exp2()) as usize;
571        format!("{:.*} GiB", decimals, number_of_bytes / 30.0_f64.exp2())
572    }
573}
574
575#[test]
576fn test_format_bytes() {
577    let test_cases = [
578        (0.0, "0 B"),
579        (0.25, "0.25 B"),
580        (1.51, "1.5 B"),
581        (11.0, "11 B"),
582        (12.5, "12.5 B"),
583        (999.0, "999 B"),
584        (1000.0, "1000 B"),
585        (1001.0, "1001 B"),
586        (1023.0, "1023 B"),
587        (1024.0, "1.0 KiB"),
588        (1025.0, "1.0 KiB"),
589        (1024.0 * 1.2345, "1.2 KiB"),
590        (1024.0 * 12.345, "12.3 KiB"),
591        (1024.0 * 123.45, "123 KiB"),
592        (1024f64.powi(2) - 1.0, "1024 KiB"),
593        (1024f64.powi(2) + 0.0, "1.0 MiB"),
594        (1024f64.powi(2) + 1.0, "1.0 MiB"),
595        (1024f64.powi(3) - 1.0, "1024 MiB"),
596        (1024f64.powi(3) + 0.0, "1.0 GiB"),
597        (1024f64.powi(3) + 1.0, "1.0 GiB"),
598        (1.2345 * 30.0_f64.exp2(), "1.2 GiB"),
599        (12.345 * 30.0_f64.exp2(), "12.3 GiB"),
600        (123.45 * 30.0_f64.exp2(), "123 GiB"),
601        (1024f64.powi(4) - 1.0, "1024 GiB"),
602        (1024f64.powi(4) + 0.0, "1024 GiB"),
603        (1024f64.powi(4) + 1.0, "1024 GiB"),
604        (123.0, "123 B"),
605        (12_345.0, "12.1 KiB"),
606        (1_234_567.0, "1.2 MiB"),
607        (123_456_789.0, "118 MiB"),
608    ];
609
610    for (value, expected) in test_cases {
611        assert_eq!(format_bytes(value), expected);
612    }
613}
614
615pub fn parse_bytes_base10(bytes: &str) -> Option<i64> {
616    // Note: intentionally case sensitive so that we don't parse `Mb` (Megabit) as `MB` (Megabyte).
617    if let Some(rest) = bytes.strip_prefix(MINUS) {
618        Some(-parse_bytes_base10(rest)?)
619    } else if let Some(kb) = bytes.strip_suffix("kB") {
620        Some(kb.parse::<i64>().ok()? * 1_000)
621    } else if let Some(mb) = bytes.strip_suffix("MB") {
622        Some(mb.parse::<i64>().ok()? * 1_000_000)
623    } else if let Some(gb) = bytes.strip_suffix("GB") {
624        Some(gb.parse::<i64>().ok()? * 1_000_000_000)
625    } else if let Some(tb) = bytes.strip_suffix("TB") {
626        Some(tb.parse::<i64>().ok()? * 1_000_000_000_000)
627    } else if let Some(b) = bytes.strip_suffix('B') {
628        Some(b.parse::<i64>().ok()?)
629    } else {
630        None
631    }
632}
633
634#[test]
635fn test_parse_bytes_base10() {
636    let test_cases = [
637        ("999B", 999),
638        ("1000B", 1_000),
639        ("1kB", 1_000),
640        ("1000kB", 1_000_000),
641        ("1MB", 1_000_000),
642        ("1000MB", 1_000_000_000),
643        ("1GB", 1_000_000_000),
644        ("1000GB", 1_000_000_000_000),
645        ("1TB", 1_000_000_000_000),
646        ("1000TB", 1_000_000_000_000_000),
647        ("123B", 123),
648        ("12kB", 12_000),
649        ("123MB", 123_000_000),
650        ("-10B", -10), // hyphen-minus
651        ("−10B", -10), // proper minus
652    ];
653    for (value, expected) in test_cases {
654        assert_eq!(Some(expected), parse_bytes_base10(value));
655    }
656}
657
658pub fn parse_bytes_base2(bytes: &str) -> Option<i64> {
659    // Note: intentionally case sensitive so that we don't parse `Mib` (Mebibit) as `MiB` (Mebibyte).
660    if let Some(rest) = bytes.strip_prefix(MINUS) {
661        Some(-parse_bytes_base2(rest)?)
662    } else if let Some(kb) = bytes.strip_suffix("KiB") {
663        Some(kb.parse::<i64>().ok()? * 1024)
664    } else if let Some(mb) = bytes.strip_suffix("MiB") {
665        Some(mb.parse::<i64>().ok()? * 1024 * 1024)
666    } else if let Some(gb) = bytes.strip_suffix("GiB") {
667        Some(gb.parse::<i64>().ok()? * 1024 * 1024 * 1024)
668    } else if let Some(tb) = bytes.strip_suffix("TiB") {
669        Some(tb.parse::<i64>().ok()? * 1024 * 1024 * 1024 * 1024)
670    } else if let Some(b) = bytes.strip_suffix('B') {
671        Some(b.parse::<i64>().ok()?)
672    } else {
673        None
674    }
675}
676
677#[test]
678fn test_parse_bytes_base2() {
679    let test_cases = [
680        ("999B", 999),
681        ("1023B", 1_023),
682        ("1024B", 1_024),
683        ("1KiB", 1_024),
684        ("1000KiB", 1_000 * 1024),
685        ("1MiB", 1024 * 1024),
686        ("1000MiB", 1_000 * 1024 * 1024),
687        ("1GiB", 1024 * 1024 * 1024),
688        ("1000GiB", 1_000 * 1024 * 1024 * 1024),
689        ("1TiB", 1024 * 1024 * 1024 * 1024),
690        ("1000TiB", 1_000 * 1024 * 1024 * 1024 * 1024),
691        ("123B", 123),
692        ("12KiB", 12 * 1024),
693        ("123MiB", 123 * 1024 * 1024),
694        ("-10B", -10), // hyphen-minus
695        ("−10B", -10), // proper minus
696    ];
697    for (value, expected) in test_cases {
698        assert_eq!(Some(expected), parse_bytes_base2(value));
699    }
700}
701
702pub fn parse_bytes(bytes: &str) -> Option<i64> {
703    parse_bytes_base10(bytes).or_else(|| parse_bytes_base2(bytes))
704}
705
706#[test]
707fn test_parse_bytes() {
708    let test_cases = [
709        // base10
710        ("999B", 999),
711        ("1000B", 1_000),
712        ("1kB", 1_000),
713        ("1000kB", 1_000_000),
714        ("1MB", 1_000_000),
715        ("1000MB", 1_000_000_000),
716        ("1GB", 1_000_000_000),
717        ("1000GB", 1_000_000_000_000),
718        ("1TB", 1_000_000_000_000),
719        ("1000TB", 1_000_000_000_000_000),
720        ("123B", 123),
721        ("12kB", 12_000),
722        ("123MB", 123_000_000),
723        // base2
724        ("999B", 999),
725        ("1023B", 1_023),
726        ("1024B", 1_024),
727        ("1KiB", 1_024),
728        ("1000KiB", 1_000 * 1024),
729        ("1MiB", 1024 * 1024),
730        ("1000MiB", 1_000 * 1024 * 1024),
731        ("1GiB", 1024 * 1024 * 1024),
732        ("1000GiB", 1_000 * 1024 * 1024 * 1024),
733        ("1TiB", 1024 * 1024 * 1024 * 1024),
734        ("1000TiB", 1_000 * 1024 * 1024 * 1024 * 1024),
735        ("123B", 123),
736        ("12KiB", 12 * 1024),
737        ("123MiB", 123 * 1024 * 1024),
738    ];
739    for (value, expected) in test_cases {
740        assert_eq!(Some(expected), parse_bytes(value));
741    }
742}
743
744// --- Durations ---
745
746pub fn parse_duration(duration: &str) -> Result<f32, String> {
747    fn parse_num(s: &str) -> Result<f32, String> {
748        s.parse()
749            .map_err(|_ignored| format!("Expected a number, got {s:?}"))
750    }
751
752    if let Some(ms) = duration.strip_suffix("ms") {
753        Ok(parse_num(ms)? * 1e-3)
754    } else if let Some(s) = duration.strip_suffix('s') {
755        Ok(parse_num(s)?)
756    } else if let Some(s) = duration.strip_suffix('m') {
757        Ok(parse_num(s)? * 60.0)
758    } else if let Some(s) = duration.strip_suffix('h') {
759        Ok(parse_num(s)? * 60.0 * 60.0)
760    } else {
761        Err(format!(
762            "Expected a suffix of 'ms', 's', 'm' or 'h' in string {duration:?}"
763        ))
764    }
765}
766
767#[test]
768fn test_parse_duration() {
769    assert_eq!(parse_duration("3.2s"), Ok(3.2));
770    assert_eq!(parse_duration("250ms"), Ok(0.250));
771    assert_eq!(parse_duration("3m"), Ok(3.0 * 60.0));
772}
773
774/// Remove the custom formatting
775///
776/// Removes the thin spaces and the special minus character. Useful when copying text.
777pub fn remove_number_formatting(copied_str: &str) -> String {
778    copied_str.replace('\u{2009}', "").replace('−', "-")
779}
780
781#[test]
782fn test_remove_number_formatting() {
783    assert_eq!(
784        remove_number_formatting(&format_f32(-123_456.78)),
785        "-123456.8"
786    );
787    assert_eq!(
788        remove_number_formatting(&format_f64(-123_456.78)),
789        "-123456.78"
790    );
791    assert_eq!(
792        remove_number_formatting(&format_int(-123_456_789_i32)),
793        "-123456789"
794    );
795    assert_eq!(
796        remove_number_formatting(&format_uint(123_456_789_u32)),
797        "123456789"
798    );
799}