1mod duration;
6mod plural;
7pub mod time;
8
9use std::cmp::PartialOrd;
10use std::fmt::Display;
11
12pub use self::duration::DurationFormatOptions;
13pub use self::plural::{format_plural_s, format_plural_signed_s};
14
15pub const MINUS: char = '−';
21
22pub const THIN_SPACE: char = '\u{2009}';
26
27pub fn strip_whitespace_and_normalize(text: &str) -> String {
29 text.chars()
30 .filter(|c| !c.is_whitespace())
32 .map(|c| if c == MINUS { '-' } else { c })
34 .collect()
35}
36
37pub trait UnsignedAbs {
39 type Unsigned;
41
42 fn unsigned_abs(self) -> Self::Unsigned;
44}
45
46impl UnsignedAbs for i8 {
47 type Unsigned = u8;
48
49 #[inline]
50 fn unsigned_abs(self) -> Self::Unsigned {
51 self.unsigned_abs()
52 }
53}
54
55impl UnsignedAbs for i16 {
56 type Unsigned = u16;
57
58 #[inline]
59 fn unsigned_abs(self) -> Self::Unsigned {
60 self.unsigned_abs()
61 }
62}
63
64impl UnsignedAbs for i32 {
65 type Unsigned = u32;
66
67 #[inline]
68 fn unsigned_abs(self) -> Self::Unsigned {
69 self.unsigned_abs()
70 }
71}
72
73impl UnsignedAbs for i64 {
74 type Unsigned = u64;
75
76 #[inline]
77 fn unsigned_abs(self) -> Self::Unsigned {
78 self.unsigned_abs()
79 }
80}
81
82impl UnsignedAbs for i128 {
83 type Unsigned = u128;
84
85 #[inline]
86 fn unsigned_abs(self) -> Self::Unsigned {
87 self.unsigned_abs()
88 }
89}
90
91impl UnsignedAbs for isize {
92 type Unsigned = usize;
93
94 #[inline]
95 fn unsigned_abs(self) -> Self::Unsigned {
96 self.unsigned_abs()
97 }
98}
99
100pub fn format_int<Int>(number: Int) -> String
105where
106 Int: Display + PartialOrd + num_traits::Zero + UnsignedAbs,
107 Int::Unsigned: Display + num_traits::Unsigned,
108{
109 if number < Int::zero() {
110 format!("{MINUS}{}", format_uint(number.unsigned_abs()))
111 } else {
112 add_thousands_separators(&number.to_string())
113 }
114}
115
116#[expect(clippy::needless_pass_by_value)]
121pub fn format_uint<Uint>(number: Uint) -> String
122where
123 Uint: Display + num_traits::Unsigned,
124{
125 add_thousands_separators(&number.to_string())
126}
127
128fn add_thousands_separators(number: &str) -> String {
131 let mut chars = number.chars().rev().peekable();
132
133 let mut result = vec![];
134 while chars.peek().is_some() {
135 if !result.is_empty() {
136 result.push(THIN_SPACE);
138 }
139 for _ in 0..3 {
140 if let Some(c) = chars.next() {
141 result.push(c);
142 }
143 }
144 }
145
146 result.reverse();
147 result.into_iter().collect()
148}
149
150#[test]
151fn test_format_uint() {
152 assert_eq!(format_uint(42_u32), "42");
153 assert_eq!(format_uint(999_u32), "999");
154 assert_eq!(format_uint(1_000_u32), "1 000");
155 assert_eq!(format_uint(123_456_u32), "123 456");
156 assert_eq!(format_uint(1_234_567_u32), "1 234 567");
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
161pub struct FloatFormatOptions {
162 pub always_sign: bool,
164
165 pub precision: usize,
169
170 pub num_decimals: Option<usize>,
174
175 pub strip_trailing_zeros: bool,
176
177 pub min_decimals_for_thousands_separators: usize,
179}
180
181impl FloatFormatOptions {
182 #[expect(non_upper_case_globals)]
184 pub const DEFAULT_f16: Self = Self {
185 always_sign: false,
186 precision: 5,
187 num_decimals: None,
188 strip_trailing_zeros: true,
189 min_decimals_for_thousands_separators: 6,
190 };
191
192 #[expect(non_upper_case_globals)]
194 pub const DEFAULT_f32: Self = Self {
195 always_sign: false,
196 precision: 7,
197 num_decimals: None,
198 strip_trailing_zeros: true,
199 min_decimals_for_thousands_separators: 6,
200 };
201
202 #[expect(non_upper_case_globals)]
204 pub const DEFAULT_f64: Self = Self {
205 always_sign: false,
206 precision: 15,
207 num_decimals: None,
208 strip_trailing_zeros: true,
209 min_decimals_for_thousands_separators: 6,
210 };
211
212 #[inline]
214 pub fn with_always_sign(mut self, always_sign: bool) -> Self {
215 self.always_sign = always_sign;
216 self
217 }
218
219 #[inline]
222 pub fn with_precision(mut self, precision: usize) -> Self {
223 self.precision = precision;
224 self
225 }
226
227 #[inline]
231 pub fn with_decimals(mut self, num_decimals: usize) -> Self {
232 self.num_decimals = Some(num_decimals);
233 self
234 }
235
236 #[inline]
238 pub fn with_strip_trailing_zeros(mut self, strip_trailing_zeros: bool) -> Self {
239 self.strip_trailing_zeros = strip_trailing_zeros;
240 self
241 }
242
243 pub fn format(&self, value: impl Into<f64>) -> String {
246 self.format_f64(value.into())
247 }
248
249 fn format_f64(&self, mut value: f64) -> String {
250 fn reverse(s: &str) -> String {
251 s.chars().rev().collect()
252 }
253
254 let Self {
255 always_sign,
256 precision,
257 num_decimals,
258 strip_trailing_zeros,
259 min_decimals_for_thousands_separators,
260 } = *self;
261
262 if value.is_nan() {
263 return "NaN".to_owned();
264 }
265
266 let sign = if value < 0.0 {
267 value = -value;
268 "−" } else if always_sign {
270 "+"
271 } else {
272 ""
273 };
274
275 let abs_string = if value == f64::INFINITY {
276 "∞".to_owned()
277 } else {
278 let magnitude = value.log10();
279 let max_decimals = precision as f64 - magnitude.max(0.0);
280
281 if max_decimals < 0.0 {
282 format!("{:.*e}", precision.saturating_sub(1), value)
286 } else {
287 let max_decimals = max_decimals as usize;
288
289 let num_decimals = if let Some(num_decimals) = num_decimals {
290 num_decimals.min(max_decimals)
291 } else {
292 max_decimals
293 };
294
295 let mut formatted = format!("{value:.num_decimals$}");
296
297 if strip_trailing_zeros && formatted.contains('.') {
298 while formatted.ends_with('0') {
299 formatted.pop();
300 }
301 if formatted.ends_with('.') {
302 formatted.pop();
303 }
304 }
305
306 if let Some(dot) = formatted.find('.') {
307 let integer_part = &formatted[..dot];
308 let fractional_part = &formatted[dot + 1..];
309 let integer_part = add_thousands_separators(integer_part);
312
313 if fractional_part.len() < min_decimals_for_thousands_separators {
314 format!("{integer_part}.{fractional_part}")
315 } else {
316 let fractional_part =
318 reverse(&add_thousands_separators(&reverse(fractional_part)));
319 format!("{integer_part}.{fractional_part}")
320 }
321 } else {
322 add_thousands_separators(&formatted) }
324 }
325 };
326
327 format!("{sign}{abs_string}")
328 }
329}
330
331pub fn format_f64(value: f64) -> String {
336 FloatFormatOptions::DEFAULT_f64.format(value)
337}
338
339pub fn format_f32(value: f32) -> String {
344 FloatFormatOptions::DEFAULT_f32.format(value)
345}
346
347pub fn format_f16(value: half::f16) -> String {
352 FloatFormatOptions::DEFAULT_f16.format(value)
353}
354
355pub fn format_lat_lon(value: f64) -> String {
359 format!(
360 "{}°",
361 FloatFormatOptions {
362 always_sign: true,
363 precision: 10,
364 num_decimals: Some(6),
365 strip_trailing_zeros: false,
366 min_decimals_for_thousands_separators: 10,
367 }
368 .format_f64(value)
369 )
370}
371
372#[test]
373fn test_format_f32() {
374 let cases = [
375 (f32::NAN, "NaN"),
376 (f32::INFINITY, "∞"),
377 (f32::NEG_INFINITY, "−∞"),
378 (0.0, "0"),
379 (42.0, "42"),
380 (10_000.0, "10 000"),
381 (1_000_000.0, "1 000 000"),
382 (10_000_000.0, "10 000 000"),
383 (11_000_000.0, "1.100000e7"),
384 (-42.0, "−42"),
385 (-4.20, "−4.2"),
386 (123_456.78, "123 456.8"),
387 (78.4321, "78.4321"), (-std::f32::consts::PI, "−3.141 593"),
389 (-std::f32::consts::PI * 1e6, "−3 141 593"),
390 (-std::f32::consts::PI * 1e20, "−3.141593e20"), ];
392 for (value, expected) in cases {
393 let got = format_f32(value);
394 assert!(
395 got == expected,
396 "Expected to format {value} as '{expected}', but got '{got}'"
397 );
398 }
399}
400
401#[test]
402fn test_format_f64() {
403 let cases = [
404 (f64::NAN, "NaN"),
405 (f64::INFINITY, "∞"),
406 (f64::NEG_INFINITY, "−∞"),
407 (0.0, "0"),
408 (42.0, "42"),
409 (-42.0, "−42"),
410 (-4.20, "−4.2"),
411 (123_456_789.0, "123 456 789"),
412 (123_456_789.123_45, "123 456 789.12345"), (0.0000123456789, "0.000 012 345 678 9"),
414 (0.123456789, "0.123 456 789"),
415 (1.23456789, "1.234 567 89"),
416 (12.3456789, "12.345 678 9"),
417 (123.456789, "123.456 789"),
418 (1234.56789, "1 234.56789"), (12345.6789, "12 345.6789"), (78.4321, "78.4321"), (-std::f64::consts::PI, "−3.141 592 653 589 79"),
422 (-std::f64::consts::PI * 1e6, "−3 141 592.653 589 79"),
423 (-std::f64::consts::PI * 1e20, "−3.14159265358979e20"), ];
425 for (value, expected) in cases {
426 let got = format_f64(value);
427 assert!(
428 got == expected,
429 "Expected to format {value} as '{expected}', but got '{got}'"
430 );
431 }
432}
433
434#[test]
435fn test_format_f16() {
436 use half::f16;
437
438 let cases = [
439 (f16::from_f32(f32::NAN), "NaN"),
440 (f16::INFINITY, "∞"),
441 (f16::NEG_INFINITY, "−∞"),
442 (f16::ZERO, "0"),
443 (f16::from_f32(42.0), "42"),
444 (f16::from_f32(-42.0), "−42"),
445 (f16::from_f32(-4.20), "−4.1992"), (f16::from_f32(12_345.0), "12 344"), (f16::PI, "3.1406"), ];
449 for (value, expected) in cases {
450 let got = format_f16(value);
451 assert_eq!(
452 got, expected,
453 "Expected to format {value} as '{expected}', but got '{got}'"
454 );
455 }
456}
457
458#[test]
459fn test_format_f64_custom() {
460 let cases = [(
461 FloatFormatOptions::DEFAULT_f64.with_decimals(2),
462 123.456789,
463 "123.46",
464 )];
465 for (options, value, expected) in cases {
466 let got = options.format(value);
467 assert!(
468 got == expected,
469 "Expected to format {value} as '{expected}', but got '{got}'. Options: {options:#?}"
470 );
471 }
472}
473
474pub fn parse_f64(text: &str) -> Option<f64> {
477 let text = strip_whitespace_and_normalize(text);
478 text.parse().ok()
479}
480
481pub fn parse_i64(text: &str) -> Option<i64> {
484 let text = strip_whitespace_and_normalize(text);
485 text.parse().ok()
486}
487
488pub fn approximate_large_number(number: f64) -> String {
501 if number < 0.0 {
502 format!("{MINUS}{}", approximate_large_number(-number))
503 } else if number < 1000.0 {
504 format!("{number:.0}")
505 } else if number < 1_000_000.0 {
506 let decimals = (number < 10_000.0) as usize;
507 format!("{:.*}k", decimals, number / 1_000.0)
508 } else if number < 1_000_000_000.0 {
509 let decimals = (number < 10_000_000.0) as usize;
510 format!("{:.*}M", decimals, number / 1_000_000.0)
511 } else {
512 let decimals = (number < 10_000_000_000.0) as usize;
513 format!("{:.*}G", decimals, number / 1_000_000_000.0)
514 }
515}
516
517#[test]
518fn test_format_large_number() {
519 let test_cases = [
520 (999.0, "999"),
521 (1000.0, "1.0k"),
522 (1001.0, "1.0k"),
523 (999_999.0, "1000k"),
524 (1_000_000.0, "1.0M"),
525 (999_999_999.0, "1000M"),
526 (1_000_000_000.0, "1.0G"),
527 (999_999_999_999.0, "1000G"),
528 (1_000_000_000_000.0, "1000G"),
529 (123.0, "123"),
530 (12_345.0, "12k"),
531 (1_234_567.0, "1.2M"),
532 (123_456_789.0, "123M"),
533 ];
534
535 for (value, expected) in test_cases {
536 assert_eq!(expected, approximate_large_number(value));
537 }
538}
539
540pub fn format_bytes(number_of_bytes: f64) -> String {
552 if number_of_bytes < 0.0 {
553 format!("{MINUS}{}", format_bytes(-number_of_bytes))
554 } else if number_of_bytes == 0.0 {
555 "0 B".to_owned()
556 } else if number_of_bytes < 1.0 {
557 format!("{number_of_bytes} B")
558 } else if number_of_bytes < 20.0 {
559 let is_integer = number_of_bytes.round() == number_of_bytes;
560 if is_integer {
561 format!("{number_of_bytes:.0} B")
562 } else {
563 format!("{number_of_bytes:.1} B")
564 }
565 } else if number_of_bytes < 10.0_f64.exp2() {
566 format!("{number_of_bytes:.0} B")
567 } else if number_of_bytes < 20.0_f64.exp2() {
568 let decimals = (10.0 * number_of_bytes < 20.0_f64.exp2()) as usize;
569 format!("{:.*} KiB", decimals, number_of_bytes / 10.0_f64.exp2())
570 } else if number_of_bytes < 30.0_f64.exp2() {
571 let decimals = (10.0 * number_of_bytes < 30.0_f64.exp2()) as usize;
572 format!("{:.*} MiB", decimals, number_of_bytes / 20.0_f64.exp2())
573 } else {
574 let decimals = (10.0 * number_of_bytes < 40.0_f64.exp2()) as usize;
575 format!("{:.*} GiB", decimals, number_of_bytes / 30.0_f64.exp2())
576 }
577}
578
579#[test]
580fn test_format_bytes() {
581 let test_cases = [
582 (0.0, "0 B"),
583 (0.25, "0.25 B"),
584 (1.51, "1.5 B"),
585 (11.0, "11 B"),
586 (12.5, "12.5 B"),
587 (999.0, "999 B"),
588 (1000.0, "1000 B"),
589 (1001.0, "1001 B"),
590 (1023.0, "1023 B"),
591 (1024.0, "1.0 KiB"),
592 (1025.0, "1.0 KiB"),
593 (1024.0 * 1.2345, "1.2 KiB"),
594 (1024.0 * 12.345, "12.3 KiB"),
595 (1024.0 * 123.45, "123 KiB"),
596 (1024f64.powi(2) - 1.0, "1024 KiB"),
597 (1024f64.powi(2) + 0.0, "1.0 MiB"),
598 (1024f64.powi(2) + 1.0, "1.0 MiB"),
599 (1024f64.powi(3) - 1.0, "1024 MiB"),
600 (1024f64.powi(3) + 0.0, "1.0 GiB"),
601 (1024f64.powi(3) + 1.0, "1.0 GiB"),
602 (1.2345 * 30.0_f64.exp2(), "1.2 GiB"),
603 (12.345 * 30.0_f64.exp2(), "12.3 GiB"),
604 (123.45 * 30.0_f64.exp2(), "123 GiB"),
605 (1024f64.powi(4) - 1.0, "1024 GiB"),
606 (1024f64.powi(4) + 0.0, "1024 GiB"),
607 (1024f64.powi(4) + 1.0, "1024 GiB"),
608 (123.0, "123 B"),
609 (12_345.0, "12.1 KiB"),
610 (1_234_567.0, "1.2 MiB"),
611 (123_456_789.0, "118 MiB"),
612 ];
613
614 for (value, expected) in test_cases {
615 assert_eq!(format_bytes(value), expected);
616 }
617}
618
619pub fn parse_bytes_base10(bytes: &str) -> Option<i64> {
620 let bytes = strip_whitespace_and_normalize(bytes);
621
622 if bytes == "0" {
623 return Some(0);
624 }
625
626 if let Some(rest) = bytes.strip_prefix(MINUS) {
628 Some(-parse_bytes_base10(rest)?)
629 } else if let Some(kb) = bytes.strip_suffix("kB") {
630 Some((kb.parse::<f64>().ok()? * 1e3) as _)
631 } else if let Some(mb) = bytes.strip_suffix("MB") {
632 Some((mb.parse::<f64>().ok()? * 1e6) as _)
633 } else if let Some(gb) = bytes.strip_suffix("GB") {
634 Some((gb.parse::<f64>().ok()? * 1e9) as _)
635 } else if let Some(tb) = bytes.strip_suffix("TB") {
636 Some((tb.parse::<f64>().ok()? * 1e12) as _)
637 } else if let Some(b) = bytes.strip_suffix('B') {
638 Some(b.parse::<i64>().ok()?)
639 } else {
640 None
641 }
642}
643
644#[test]
645fn test_parse_bytes_base10() {
646 let test_cases = [
647 ("0", 0), ("-1B", -1),
649 ("999B", 999),
650 ("1000B", 1_000),
651 ("1kB", 1_000),
652 ("1000kB", 1_000_000),
653 ("1MB", 1_000_000),
654 ("1000MB", 1_000_000_000),
655 ("1GB", 1_000_000_000),
656 ("1000GB", 1_000_000_000_000),
657 ("1TB", 1_000_000_000_000),
658 ("1000TB", 1_000_000_000_000_000),
659 ("123B", 123),
660 ("12kB", 12_000),
661 ("123MB", 123_000_000),
662 ("-10B", -10), ("−10B", -10), ];
665 for (value, expected) in test_cases {
666 assert_eq!(Some(expected), parse_bytes_base10(value));
667 }
668}
669
670pub fn parse_bytes_base2(bytes: &str) -> Option<i64> {
671 let bytes = strip_whitespace_and_normalize(bytes);
672
673 if bytes == "0" {
674 return Some(0);
675 }
676
677 if let Some(rest) = bytes.strip_prefix(MINUS) {
679 Some(-parse_bytes_base2(rest)?)
680 } else if let Some(kb) = bytes.strip_suffix("KiB") {
681 Some((kb.parse::<f64>().ok()? * 1024.0) as _)
682 } else if let Some(mb) = bytes.strip_suffix("MiB") {
683 Some((mb.parse::<f64>().ok()? * 1024.0 * 1024.0) as _)
684 } else if let Some(gb) = bytes.strip_suffix("GiB") {
685 Some((gb.parse::<f64>().ok()? * 1024.0 * 1024.0 * 1024.0) as _)
686 } else if let Some(tb) = bytes.strip_suffix("TiB") {
687 Some((tb.parse::<f64>().ok()? * 1024.0 * 1024.0 * 1024.0 * 1024.0) as _)
688 } else if let Some(b) = bytes.strip_suffix('B') {
689 Some(b.parse::<i64>().ok()?)
690 } else {
691 None
692 }
693}
694
695#[test]
696fn test_parse_bytes_base2() {
697 let test_cases = [
698 ("0", 0), ("-1B", -1),
700 ("999B", 999),
701 ("1023B", 1_023),
702 ("1024B", 1_024),
703 ("1KiB", 1_024),
704 ("1000KiB", 1_000 * 1024),
705 ("1MiB", 1024 * 1024),
706 ("1000MiB", 1_000 * 1024 * 1024),
707 ("1GiB", 1024 * 1024 * 1024),
708 ("1000GiB", 1_000 * 1024 * 1024 * 1024),
709 ("1TiB", 1024 * 1024 * 1024 * 1024),
710 ("1000TiB", 1_000 * 1024 * 1024 * 1024 * 1024),
711 ("123B", 123),
712 ("12KiB", 12 * 1024),
713 ("123MiB", 123 * 1024 * 1024),
714 ("-10B", -10), ("−10B", -10), ];
717 for (value, expected) in test_cases {
718 assert_eq!(Some(expected), parse_bytes_base2(value));
719 }
720}
721
722pub fn parse_bytes(bytes: &str) -> Option<i64> {
723 parse_bytes_base10(bytes).or_else(|| parse_bytes_base2(bytes))
724}
725
726#[test]
727fn test_parse_bytes() {
728 let test_cases = [
729 ("0", 0), ("-1B", -1),
732 ("999B", 999),
733 ("1000B", 1_000),
734 ("1kB", 1_000),
735 ("1000kB", 1_000_000),
736 ("1MB", 1_000_000),
737 ("1000MB", 1_000_000_000),
738 ("1GB", 1_000_000_000),
739 ("1000GB", 1_000_000_000_000),
740 ("1TB", 1_000_000_000_000),
741 ("1000TB", 1_000_000_000_000_000),
742 ("123B", 123),
743 ("12kB", 12_000),
744 ("123MB", 123_000_000),
745 ("999B", 999),
747 ("1023B", 1_023),
748 ("1024B", 1_024),
749 ("1KiB", 1_024),
750 ("1000KiB", 1_000 * 1024),
751 ("1MiB", 1024 * 1024),
752 ("1000MiB", 1_000 * 1024 * 1024),
753 ("1GiB", 1024 * 1024 * 1024),
754 ("1000GiB", 1_000 * 1024 * 1024 * 1024),
755 ("1TiB", 1024 * 1024 * 1024 * 1024),
756 ("1000TiB", 1_000 * 1024 * 1024 * 1024 * 1024),
757 ("123B", 123),
758 ("12KiB", 12 * 1024),
759 ("123MiB", 123 * 1024 * 1024),
760 ];
761 for (value, expected) in test_cases {
762 assert_eq!(Some(expected), parse_bytes(value));
763 }
764}
765
766pub fn parse_duration(duration: &str) -> Result<f32, String> {
769 fn parse_num(s: &str) -> Result<f32, String> {
770 s.parse()
771 .map_err(|_ignored| format!("Expected a number, got {s:?}"))
772 }
773
774 if let Some(ms) = duration.strip_suffix("ms") {
775 Ok(parse_num(ms)? * 1e-3)
776 } else if let Some(s) = duration.strip_suffix('s') {
777 Ok(parse_num(s)?)
778 } else if let Some(s) = duration.strip_suffix('m') {
779 Ok(parse_num(s)? * 60.0)
780 } else if let Some(s) = duration.strip_suffix('h') {
781 Ok(parse_num(s)? * 60.0 * 60.0)
782 } else {
783 Err(format!(
784 "Expected a suffix of 'ms', 's', 'm' or 'h' in string {duration:?}"
785 ))
786 }
787}
788
789#[test]
790fn test_parse_duration() {
791 assert_eq!(parse_duration("3.2s"), Ok(3.2));
792 assert_eq!(parse_duration("250ms"), Ok(0.250));
793 assert_eq!(parse_duration("3m"), Ok(3.0 * 60.0));
794}
795
796pub fn remove_number_formatting(s: &str) -> String {
800 s.chars()
801 .filter_map(|c| {
802 if c == MINUS {
803 Some('-')
804 } else if c == THIN_SPACE {
805 None
806 } else {
807 Some(c)
808 }
809 })
810 .collect()
811}
812
813#[test]
814fn test_remove_number_formatting() {
815 assert_eq!(
816 remove_number_formatting(&format_f32(-123_456.78)),
817 "-123456.8"
818 );
819 assert_eq!(
820 remove_number_formatting(&format_f64(-123_456.78)),
821 "-123456.78"
822 );
823 assert_eq!(
824 remove_number_formatting(&format_int(-123_456_789_i32)),
825 "-123456789"
826 );
827 assert_eq!(
828 remove_number_formatting(&format_uint(123_456_789_u32)),
829 "123456789"
830 );
831}