Skip to main content

askama/filters/
humansize.rs

1use core::convert::Infallible;
2use core::fmt;
3
4use crate::{FastWritable, NO_VALUES, Values};
5
6/// Returns adequate string representation (in KB, ..) of number of bytes
7///
8/// ## Example
9/// ```
10/// # use askama::Template;
11/// #[derive(Template)]
12/// #[template(
13///     source = "Filesize: {{ size_in_bytes | filesizeformat }}.",
14///     ext = "html"
15/// )]
16/// struct Example {
17///     size_in_bytes: u64,
18/// }
19///
20/// let tmpl = Example { size_in_bytes: 1_234_567 };
21/// assert_eq!(tmpl.to_string(),  "Filesize: 1.23 MB.");
22/// ```
23#[inline]
24pub fn filesizeformat(bytes: u128, precision: u8) -> Result<FilesizeFormatFilter, Infallible> {
25    Ok(FilesizeFormatFilter { bytes, precision })
26}
27
28#[derive(Debug, Clone, Copy)]
29pub struct FilesizeFormatFilter {
30    /// The number of bytes to format nicely
31    bytes: u128,
32    /// The precision with which to display the generated string.
33    /// This determines the number of digits after the decimal separator.
34    precision: u8,
35}
36
37impl fmt::Display for FilesizeFormatFilter {
38    #[inline]
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        Ok(self.write_into(f, NO_VALUES)?)
41    }
42}
43
44impl FastWritable for FilesizeFormatFilter {
45    fn write_into(&self, dest: &mut dyn fmt::Write, values: &dyn Values) -> crate::Result<()> {
46        if self.bytes < 1000 {
47            (self.bytes as u32).write_into(dest, values)?;
48            return Ok(dest.write_str(" B")?);
49        }
50
51        // find appropriate unit to format in
52        let unit = SI_PREFIXES
53            .iter()
54            .take_while(|siprefix| siprefix.lower_bound <= self.bytes)
55            .last()
56            .unwrap_or(&SI_PREFIXES[SI_PREFIXES.len() - 1]);
57        // divide number up into "full" units, and remainder
58        let integer = self.bytes / unit.lower_bound;
59        let remainder = self.bytes % unit.lower_bound;
60
61        // Compute fractional part with desired precision
62        // limit precision to the chosen unit's exponent - a greater precision will just
63        // generated tailing 0s that will be dropped.
64        let precision = self.precision.min(unit.exponent);
65
66        // integer portion (before decimal point)
67        integer.write_into(dest, values)?;
68        if precision > 0 {
69            let mut scale = 10u128.pow(precision as u32);
70            let mut fractional = remainder.saturating_mul(scale) / unit.lower_bound;
71            if fractional > 0 {
72                '.'.write_into(dest, values)?;
73                for _ in 0..precision {
74                    scale /= 10;
75                    let digit = (b'0' + (fractional / scale) as u8) as char;
76                    digit.write_into(dest, values)?;
77                    fractional %= scale;
78                    if fractional == 0 {
79                        break;
80                    }
81                }
82            }
83        }
84        ' '.write_into(dest, values)?;
85        unit.prefix_char.write_into(dest, values)?;
86        'B'.write_into(dest, values)?;
87
88        Ok(())
89    }
90}
91
92struct SiPrefix {
93    /// SI-prefix character that is prepended to the unit (B) as scaler.
94    prefix_char: char,
95    /// The smallest number of bytes that will be represented using this si-prefix.
96    /// This is 10 ^ self.exponent
97    lower_bound: u128,
98    /// The exponent (10 ^ exponent) that calculates this si-prefix's lower bound.
99    exponent: u8,
100}
101impl SiPrefix {
102    const fn new(prefix_char: char, exponent: u8) -> Self {
103        Self {
104            prefix_char,
105            lower_bound: 10u128.pow(exponent as u32),
106            exponent,
107        }
108    }
109}
110
111/// The set of supported SI-Prefixes.
112/// The fitting prefix for a given number of bytes is selected by choosing the prefix with
113/// the highest lower_bound, that is lower than the number of bytes.
114const SI_PREFIXES: &[SiPrefix] = &[
115    SiPrefix::new('k', 3),
116    SiPrefix::new('M', 6),
117    SiPrefix::new('G', 9),
118    SiPrefix::new('T', 12),
119    SiPrefix::new('P', 15),
120    SiPrefix::new('E', 18),
121    SiPrefix::new('Z', 21),
122    SiPrefix::new('Y', 24),
123    SiPrefix::new('R', 27),
124    SiPrefix::new('Q', 30),
125];
126
127#[test]
128#[cfg(feature = "alloc")]
129fn test_filesizeformat_edgecases() {
130    use alloc::string::ToString;
131
132    assert_eq!(filesizeformat(1000, 0).unwrap().to_string(), "1 kB");
133
134    assert_eq!(
135        filesizeformat(954_548_589_125_249_215_468, 0)
136            .unwrap()
137            .to_string(),
138        "954 EB"
139    );
140    assert_eq!(
141        filesizeformat(954_548_589_125_249_215_468, 10)
142            .unwrap()
143            .to_string(),
144        "954.5485891252 EB"
145    );
146    assert_eq!(
147        filesizeformat(954_548_589_125_249_215_468, 255)
148            .unwrap()
149            .to_string(),
150        "954.548589125249215468 EB"
151    );
152}
153
154#[test]
155#[cfg(feature = "alloc")]
156fn test_filesizeformat_prec2() {
157    use alloc::string::ToString;
158
159    assert_eq!(filesizeformat(0, 2).unwrap().to_string(), "0 B");
160    assert_eq!(filesizeformat(999, 2).unwrap().to_string(), "999 B");
161    assert_eq!(filesizeformat(1000, 2).unwrap().to_string(), "1 kB");
162    assert_eq!(filesizeformat(1023, 2).unwrap().to_string(), "1.02 kB");
163    assert_eq!(filesizeformat(1024, 2).unwrap().to_string(), "1.02 kB");
164    assert_eq!(filesizeformat(1025, 2).unwrap().to_string(), "1.02 kB");
165    assert_eq!(filesizeformat(1100, 2).unwrap().to_string(), "1.1 kB");
166    assert_eq!(filesizeformat(9_499_014, 2).unwrap().to_string(), "9.49 MB");
167    assert_eq!(
168        filesizeformat(954_548_589, 2).unwrap().to_string(),
169        "954.54 MB"
170    );
171    assert_eq!(
172        filesizeformat(300_000_000_000, 2).unwrap().to_string(),
173        "300 GB"
174    );
175    assert_eq!(
176        filesizeformat(600_000_000_000, 2).unwrap().to_string(),
177        "600 GB"
178    );
179    assert_eq!(
180        filesizeformat(7_000_000_000_000, 2).unwrap().to_string(),
181        "7 TB"
182    );
183    assert_eq!(
184        filesizeformat(2_300_000_000_000_000, 2)
185            .unwrap()
186            .to_string(),
187        "2.3 PB"
188    );
189    assert_eq!(
190        filesizeformat(9_900_000_000_000_000_000, 2)
191            .unwrap()
192            .to_string(),
193        "9.9 EB"
194    );
195    assert_eq!(
196        filesizeformat(4_500_000_000_000_000_000_000, 2)
197            .unwrap()
198            .to_string(),
199        "4.5 ZB"
200    );
201}
202
203#[test]
204#[cfg(feature = "alloc")]
205fn test_filesizeformat_prec3() {
206    use alloc::string::ToString;
207
208    assert_eq!(filesizeformat(0, 3).unwrap().to_string(), "0 B");
209    assert_eq!(filesizeformat(999, 3).unwrap().to_string(), "999 B");
210    assert_eq!(filesizeformat(1000, 3).unwrap().to_string(), "1 kB");
211    assert_eq!(filesizeformat(1023, 3).unwrap().to_string(), "1.023 kB");
212    assert_eq!(filesizeformat(1024, 3).unwrap().to_string(), "1.024 kB");
213    assert_eq!(filesizeformat(1025, 3).unwrap().to_string(), "1.025 kB");
214    assert_eq!(filesizeformat(1100, 3).unwrap().to_string(), "1.1 kB");
215    assert_eq!(
216        filesizeformat(9_499_014, 3).unwrap().to_string(),
217        "9.499 MB"
218    );
219    assert_eq!(
220        filesizeformat(954_548_589, 3).unwrap().to_string(),
221        "954.548 MB"
222    );
223    assert_eq!(
224        filesizeformat(300_000_000_000, 3).unwrap().to_string(),
225        "300 GB"
226    );
227    assert_eq!(
228        filesizeformat(600_000_000_000, 3).unwrap().to_string(),
229        "600 GB"
230    );
231    assert_eq!(
232        filesizeformat(7_000_000_000_000, 3).unwrap().to_string(),
233        "7 TB"
234    );
235    assert_eq!(
236        filesizeformat(2_300_000_000_000_000, 3)
237            .unwrap()
238            .to_string(),
239        "2.3 PB"
240    );
241    assert_eq!(
242        filesizeformat(9_900_000_000_000_000_000, 3)
243            .unwrap()
244            .to_string(),
245        "9.9 EB"
246    );
247    assert_eq!(
248        filesizeformat(4_500_000_000_000_000_000_000, 3)
249            .unwrap()
250            .to_string(),
251        "4.5 ZB"
252    );
253}