abbrev_num/
lib.rs

1use lazy_static::lazy_static;
2use rust_decimal::prelude::FromPrimitive;
3use rust_decimal::Decimal;
4
5pub use rust_decimal::RoundingStrategy;
6
7lazy_static! {
8    /// The default list of abbreviation units.
9    pub static ref ABBREVIATIONS: [&'static str; 7] = ["", "k", "M", "B", "T", "P", "E"];
10}
11
12/// The options for abbreviating a number.
13#[derive(Debug, Default, Copy, Clone)]
14pub struct Options<'a> {
15    /// The precision of the result. `1` by default.
16    pub precision: Option<u32>,
17    /// A list of custom abbreviation units. [ABBREVIATIONS] is used by default.
18    pub abbreviations: Option<[&'a str; 7]>,
19    /// The [RoundingStrategy] to use on the result.
20    /// [RoundingStrategy::MidpointNearestEven] is used by default.
21    pub rounding_strategy: Option<RoundingStrategy>,
22}
23
24/// Abbreviates the given number into a human-friendly format according to specified
25/// options.
26///
27/// # Arguments
28///
29/// * `number` - The integer to be abbreviated.
30/// * `options` - An optional parameter specifying the [Options] for abbreviation.
31///
32/// # Returns
33///
34/// `Some(value)`, a string representation of the abbreviated form of the number.
35/// Returns `None` if the number is out of bounds or cannot be abbreviated using the
36/// provided abbreviations.
37///
38/// # Examples
39///
40/// ```
41/// use abbrev_num::{abbrev_num, Options};
42///
43/// let options = Options {
44///     precision: Some(3),
45///     ..Default::default()
46/// };
47///
48/// assert_eq!(abbrev_num(10_500, Some(options)), Some("10.5k".to_string()));
49/// ```
50pub 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        // Zero precision
123        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}