1mod time;
6
7use std::{cmp::PartialOrd, fmt::Display};
8
9pub use time::{format_timestamp_secs, next_grid_tick_magnitude_nanos, parse_timestamp_secs};
10
11const MINUS: char = '−';
17
18pub trait UnsignedAbs {
20 type Unsigned;
22
23 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
81pub 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#[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
109fn 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 let thin_space = '\u{2009}'; 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
143pub struct FloatFormatOptions {
144 pub always_sign: bool,
146
147 pub precision: usize,
151
152 pub num_decimals: Option<usize>,
156
157 pub strip_trailing_zeros: bool,
158
159 pub min_decimals_for_thousands_separators: usize,
161}
162
163impl FloatFormatOptions {
164 #[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 #[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 #[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 #[inline]
196 pub fn with_always_sign(mut self, always_sign: bool) -> Self {
197 self.always_sign = always_sign;
198 self
199 }
200
201 #[inline]
204 pub fn with_precision(mut self, precision: usize) -> Self {
205 self.precision = precision;
206 self
207 }
208
209 #[inline]
213 pub fn with_decimals(mut self, num_decimals: usize) -> Self {
214 self.num_decimals = Some(num_decimals);
215 self
216 }
217
218 #[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 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 "−" } 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 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 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 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) }
306 }
307 };
308
309 format!("{sign}{abs_string}")
310 }
311}
312
313pub fn format_f64(value: f64) -> String {
318 FloatFormatOptions::DEFAULT_f64.format(value)
319}
320
321pub fn format_f32(value: f32) -> String {
326 FloatFormatOptions::DEFAULT_f32.format(value)
327}
328
329pub fn format_f16(value: half::f16) -> String {
334 FloatFormatOptions::DEFAULT_f16.format(value)
335}
336
337pub 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"), (-std::f32::consts::PI, "−3.141 593"),
371 (-std::f32::consts::PI * 1e6, "−3 141 593"),
372 (-std::f32::consts::PI * 1e20, "−3.141593e20"), ];
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"), (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"), (12345.6789, "12 345.6789"), (78.4321, "78.4321"), (-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"), ];
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::from_f32(12_345.0), "12 344"), (f16::PI, "3.1406"), ];
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
456pub fn parse_f64(text: &str) -> Option<f64> {
459 let text: String = text
460 .chars()
461 .filter(|c| !c.is_whitespace())
463 .map(|c| if c == '−' { '-' } else { c })
465 .collect();
466
467 text.parse().ok()
468}
469
470pub fn parse_i64(text: &str) -> Option<i64> {
473 let text: String = text
474 .chars()
475 .filter(|c| !c.is_whitespace())
477 .map(|c| if c == '−' { '-' } else { c })
479 .collect();
480
481 text.parse().ok()
482}
483
484pub 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
536pub 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 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), ("−10B", -10), ];
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 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), ("−10B", -10), ];
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 ("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 ("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
744pub 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
774pub 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}