pretty_num/
lib.rs

1#![warn(missing_docs)]
2//! This crate formats numbers in a compact form similar to that used on social media sites:
3//! ```
4//! use pretty_num::PrettyNumber;
5//! 
6//! assert_eq!(23_520_123.pretty_format(), String::from("23.5M"));
7//! ```
8
9const SUFFIXES: [char; 4] = ['k', 'M', 'B', 'T'];
10
11/// A number that can be formatted prettily.
12pub trait PrettyNumber {
13    /// Formats an integer to be more compact. The resulting string will have a maximum of 3 significant digits with no more than one decimal point.
14    /// # Examples
15    /// ```
16    /// # use pretty_num::PrettyNumber;
17    /// // Integers with a magnitude less than 1,000 do not get compacted.
18    /// assert_eq!(534.pretty_format(), String::from("534"));
19    /// 
20    /// // Integers with a magnitude greater than or equal to 1,000 get compacted.
21    /// assert_eq!(15_000.pretty_format(), String::from("15k"));
22    /// 
23    /// // Integers will have a single decimal point when rounded.
24    /// assert_eq!(4_230_542.pretty_format(), String::from("4.2M"));
25    /// 
26    /// // Formatted numbers get rounded to a number without a decimal place when appropriate.
27    /// assert_eq!(5_031.pretty_format(), String::from("5k"));
28    /// 
29    /// // Also works with negative numbers.
30    /// assert_eq!((-25_621_783).pretty_format(), String::from("-25.6M"));
31    /// 
32    /// // Can go as high as trillions!
33    /// assert_eq!(36_777_121_590_100i64.pretty_format(), String::from("36.8T"));
34    /// ```
35    /// # Panics
36    /// This function panics if it is passed a number greater than 1 quadrillion or less than negative 1 quadrillion.
37    fn pretty_format(self) -> String;
38}
39
40impl<N: Into<i64>> PrettyNumber for N {
41    fn pretty_format(self) -> String {
42        let number: i64 = self.into();
43
44        if number.abs() < 1000 {
45            number.to_string()
46        } else {
47            let sign: i8 = if number < 0 { -1 } else { 1 };
48            let mut number_as_float = number.abs() as f32;
49            for suffix in SUFFIXES {
50                number_as_float /= 1000f32;
51
52                if number_as_float < 1000f32 {
53                    return format!(
54                        "{:.*}{suffix}",
55                        if (number_as_float - number_as_float.floor()) < 0.1
56                            || number_as_float >= 100f32
57                        {
58                            0
59                        } else {
60                            1
61                        },
62                        sign as f32 * number_as_float
63                    );
64                }
65            }
66
67            panic!("Number {number} is larger than 1 quadrillion!");
68        }
69    }
70}
71
72#[cfg(test)]
73mod test {
74    use crate::PrettyNumber;
75    use rstest::rstest;
76
77    #[rstest]
78    #[case(7, "7")]
79    #[case(42, "42")]
80    #[case(717, "717")]
81    #[case(-5, "-5")]
82    #[case(-76, "-76")]
83    #[case(-224, "-224")]
84    #[case(1_001, "1k")]
85    #[case(1_624, "1.6k")]
86    #[case(-5_020, "-5k")]
87    #[case(-9_505, "-9.5k")]
88    #[case(19_007, "19k")]
89    #[case(73_444, "73.4k")]
90    #[case(-55_033, "-55k")]
91    #[case(-42_780, "-42.8k")]
92    #[case(469_070, "469k")]
93    #[case(945_661, "946k")]
94    #[case(-223_090, "-223k")]
95    #[case(-671_522, "-672k")]
96    #[case(3_001_500, "3M")]
97    #[case(7_926_400, "7.9M")]
98    #[case(-4_030_115, "-4M")]
99    #[case(-3_333_221, "-3.3M")]
100    #[case(75_032_115, "75M")]
101    #[case(23_333_452, "23.3M")]
102    #[case(-54_012_560, "-54M")]
103    #[case(-11_740_662, "-11.7M")]
104    #[case(555_067_885, "555M")]
105    #[case(352_344_120, "352M")]
106    #[case(-222_000_554, "-222M")]
107    #[case(-434_875_500, "-435M")]
108    #[case(2_004_254_578, "2B")]
109    #[case(7_667_973_223, "7.7B")]
110    #[case(-4_002_154_900, "-4B")]
111    #[case(-6_534_664_725, "-6.5B")]
112    #[case(87_050_671_768, "87B")]
113    #[case(44_444_333_222, "44.4B")]
114    #[case(-32_010_345_093, "-32B")]
115    #[case(-65_420_132_543, "-65.4B")]
116    #[case(899_055_111_032, "899B")]
117    #[case(723_999_324_999, "724B")]
118    #[case(-666_000_142_543, "-666B")]
119    #[case(-400_601_897_231, "-401B")]
120    #[case(5_000_023_667_158, "5T")]
121    #[case(1_222_333_444_555, "1.2T")]
122    #[case(-4_000_354_984_333, "-4T")]
123    #[case(-6_923_000_178_126, "-6.9T")]
124    #[case(66_001_789_809_223, "66T")]
125    #[case(93_723_000_151_300, "93.7T")]
126    #[case(-50_032_745_113_006, "-50T")]
127    #[case(-11_444_653_221_094, "-11.4T")]
128    #[case(343_003_766_091_322, "343T")]
129    #[case(357_455_634_091_722, "357T")]
130    #[case(-567_023_400_999_234, "-567T")]
131    #[case(-871_567_223_222_546, "-872T")]
132    fn pretty_format_test(#[case] input: i64, #[case] expected: &str) {
133        assert_eq!(input.pretty_format().as_str(), expected);
134    }
135
136    #[rstest]
137    #[case(1_000_000_000_000_000)]
138    #[case(-1_000_000_000_000_000)]
139    #[should_panic]
140    fn format_quadrillion_should_panic(#[case] num: i64) {
141        let _ = num.pretty_format();
142    }
143}