1use lazy_static::lazy_static;
2use rust_decimal::prelude::FromPrimitive;
3use rust_decimal::Decimal;
4
5pub use rust_decimal::RoundingStrategy;
6
7lazy_static! {
8 pub static ref ABBREVIATIONS: [&'static str; 7] = ["", "k", "M", "B", "T", "P", "E"];
10}
11
12#[derive(Debug, Default, Copy, Clone)]
14pub struct Options<'a> {
15 pub precision: Option<u32>,
17 pub abbreviations: Option<[&'a str; 7]>,
19 pub rounding_strategy: Option<RoundingStrategy>,
22}
23
24pub fn abbrev_num(number: isize, options: Option<Options>) -> Option<String> {
51 if number == 0 {
52 return Some("0".to_string());
53 }
54
55 let options = options.unwrap_or_default();
56 let absolute = number.unsigned_abs();
57 let level = absolute.ilog10() / 3;
58 let sign = if number.is_negative() { "-" } else { "" };
59 let precision = options.precision.unwrap_or(1);
60
61 let abbreviation = if let Some(abbreviations) = options.abbreviations {
62 abbreviations.get(level as usize).copied()
63 } else {
64 ABBREVIATIONS.get(level as usize).copied()
65 }?;
66
67 if level == 0 {
68 return Some(format!("{sign}{absolute}{abbreviation}"));
69 }
70
71 let result = absolute as f64 / 10_f64.powi(level as i32 * 3);
72 let result = Decimal::from_f64(result)?.round_dp_with_strategy(
73 precision,
74 options
75 .rounding_strategy
76 .unwrap_or(RoundingStrategy::MidpointNearestEven),
77 );
78
79 Some(format!("{sign}{}{abbreviation}", result.normalize()))
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn can_abbreviate_numbers() {
88 let fixtures: Vec<(isize, &str)> = vec![
89 (0, "0"),
90 (-0, "0"),
91 (1, "1"),
92 (10, "10"),
93 (150, "150"),
94 (999, "999"),
95 (1_000, "1k"),
96 (1_200, "1.2k"),
97 (10_000, "10k"),
98 (150_000, "150k"),
99 (1_000_000, "1M"),
100 (4_500_000, "4.5M"),
101 (-10, "-10"),
102 (-1_500, "-1.5k"),
103 ];
104
105 fixtures.iter().for_each(|(case, expected)| {
106 let result = abbrev_num(*case, None);
107 assert_eq!(result, Some(expected.to_string()));
108 });
109 }
110
111 #[test]
112 fn can_abbreviate_upto_a_certain_precision() {
113 let result = abbrev_num(
114 1_566_450,
115 Some(Options {
116 precision: Some(3),
117 ..Default::default()
118 }),
119 );
120 assert_eq!(result, Some("1.566M".to_string()));
121
122 let result = abbrev_num(
124 1_566_450,
125 Some(Options {
126 precision: Some(0),
127 ..Default::default()
128 }),
129 );
130 assert_eq!(result, Some("2M".to_string()));
131 }
132
133 #[test]
134 fn can_abbreviate_using_custom_units() {
135 let units: [&str; 7] = ["_c0", "_c1", "_c2", "_c3", "_c4", "_c5", "_c6"];
136 let fixtures: Vec<(isize, &str)> = vec![
137 (0, "0"),
138 (10, "10_c0"),
139 (1_000, "1_c1"),
140 (1_000_000, "1_c2"),
141 (1_000_000_000, "1_c3"),
142 (1_000_000_000_000, "1_c4"),
143 (1_000_000_000_000_000, "1_c5"),
144 (1_000_000_000_000_000_000, "1_c6"),
145 ];
146
147 fixtures.iter().for_each(|(case, expected)| {
148 let result = abbrev_num(
149 *case,
150 Some(Options {
151 abbreviations: Some(units),
152 ..Default::default()
153 }),
154 );
155 assert_eq!(result, Some(expected.to_string()));
156 });
157 }
158}