1#![doc = include_str!("../README.md")]
2#![no_std]
3#![deny(clippy::pedantic)]
4#![deny(clippy::nursery)]
5#![forbid(clippy::indexing_slicing)]
6#![forbid(clippy::panic)]
7#![forbid(clippy::unwrap_used)]
8#![forbid(clippy::expect_used)]
9#![forbid(clippy::unreachable)]
10#![forbid(clippy::todo)]
11#![forbid(clippy::unimplemented)]
12#![forbid(clippy::alloc_instead_of_core)]
13#![forbid(clippy::float_arithmetic)]
14#![forbid(clippy::cast_possible_wrap)]
15#![forbid(clippy::cast_possible_truncation)]
16#![forbid(unsafe_code)]
17
18use core::fmt;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct ByteFormatter {
32 unit: Unit,
33 standard: Standard,
34 space: bool,
35}
36
37impl Default for ByteFormatter {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl ByteFormatter {
44 #[must_use]
47 pub const fn new() -> Self {
48 Self {
49 unit: Unit::Bytes,
50 standard: Standard::Binary,
51 space: true,
52 }
53 }
54
55 #[must_use]
57 pub const fn standard(mut self, standard: Standard) -> Self {
58 self.standard = standard;
59 self
60 }
61
62 #[must_use]
64 pub const fn unit(mut self, unit: Unit) -> Self {
65 self.unit = unit;
66 self
67 }
68
69 #[must_use]
86 pub const fn space(mut self, space: bool) -> Self {
87 self.space = space;
88 self
89 }
90
91 #[must_use]
93 pub fn format(self, val: u64) -> FormattedBytes {
94 FormattedBytes::from_formatter(val, self.unit, self.standard, self.space)
95 }
96}
97
98#[derive(Debug, Copy, Clone, PartialEq, Eq)]
100pub enum Unit {
101 Bytes,
103 Bits,
105}
106
107#[derive(Debug, Copy, Clone, PartialEq, Eq)]
109pub enum Standard {
110 SI,
112 Binary,
114}
115
116#[derive(Clone, Copy)]
121pub struct FormattedBytes {
122 buf: [u8; 16],
123 len: usize,
124}
125
126impl FormattedBytes {
127 pub(crate) fn from_formatter(val: u64, unit: Unit, standard: Standard, space: bool) -> Self {
128 let mag = if val == 0 {
130 0
131 } else {
132 match standard {
133 Standard::SI => (val.ilog10() / 3) as usize,
134 Standard::Binary => (val.ilog2() / 10) as usize,
135 }
136 };
137
138 let mag = mag.min(6);
140
141 let (mut whole, mut frac) = if mag == 0 {
144 (val, 0)
145 } else {
146 match standard {
147 Standard::Binary => {
148 let shift = mag * 10;
150 let divisor = 1_u64 << shift;
151 let whole = val >> shift;
152 let rem = val & (divisor - 1);
153
154 let (scaled_rem, final_shift) = if mag == 6 {
156 (rem >> 7, shift - 7)
157 } else {
158 (rem, shift)
159 };
160
161 let rounder = 1_u64 << (final_shift - 1);
163 let f = ((scaled_rem * 100) + rounder) >> final_shift;
164
165 (whole, f)
166 }
167 Standard::SI => {
168 macro_rules! calc_si {
172 ($div:expr) => {{
173 let w = val / $div;
174 let r = val % $div;
175 let (sr, sd) = if mag == 6 {
176 (r >> 7, $div >> 7)
177 } else {
178 (r, $div)
179 };
180 let f = (sr * 100 + (sd / 2)) / sd;
181 (w, f)
182 }};
183 }
184
185 match mag {
186 1 => calc_si!(1_000_u64),
187 2 => calc_si!(1_000_000_u64),
188 3 => calc_si!(1_000_000_000_u64),
189 4 => calc_si!(1_000_000_000_000_u64),
190 5 => calc_si!(1_000_000_000_000_000_u64),
191 _ => calc_si!(1_000_000_000_000_000_000_u64),
192 }
193 }
194 }
195 };
196
197 if frac >= 100 {
199 frac = 0;
200 whole += 1;
201 }
202
203 let mut buf = [0u8; 16];
205 let mut iter = buf.iter_mut();
206
207 let mut push = |b: u8| {
209 if let Some(slot) = iter.next() {
210 *slot = b;
211 }
212 };
213
214 let (num_buf, num_len) = format_small_num(whole);
215
216 for b in num_buf.into_iter().take(num_len) {
218 push(b);
219 }
220
221 if mag != 0 {
223 push(b'.');
224 push(b'0' + u8::try_from(frac / 10).unwrap_or(0));
225 push(b'0' + u8::try_from(frac % 10).unwrap_or(0));
226 }
227
228 if space {
230 push(b' ');
231 }
232
233 if mag != 0 {
235 let prefix = match (mag, standard) {
236 (1, Standard::SI) => b'k',
237 (1, Standard::Binary) => b'K',
238 (2, _) => b'M',
239 (3, _) => b'G',
240 (4, _) => b'T',
241 (5, _) => b'P',
242 _ => b'E',
243 };
244 push(prefix);
245
246 if standard == Standard::Binary {
247 push(b'i');
248 }
249 }
250
251 push(match unit {
253 Unit::Bytes => b'B',
254 Unit::Bits => b'b',
255 });
256
257 let len = 16 - iter.len();
259
260 Self { buf, len }
261 }
262
263 #[inline]
266 #[must_use]
267 pub fn as_bytes(&self) -> &[u8] {
268 self.buf.get(..self.len).unwrap_or(&self.buf)
269 }
270
271 #[inline]
279 pub fn as_str(&self) -> Result<&str, core::str::Utf8Error> {
280 let bytes = self.buf.get(..self.len).unwrap_or(&self.buf);
281 core::str::from_utf8(bytes)
282 }
283}
284
285impl fmt::Display for FormattedBytes {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 f.write_str(self.as_str().map_err(|_| fmt::Error)?)
290 }
291}
292
293#[inline]
297fn format_small_num(n: u64) -> ([u8; 4], usize) {
298 if n < 10 {
299 ([b'0' + u8::try_from(n).unwrap_or(0), 0, 0, 0], 1)
300 } else if n < 100 {
301 (
302 [
303 b'0' + u8::try_from(n / 10).unwrap_or(0),
304 b'0' + u8::try_from(n % 10).unwrap_or(0),
305 0,
306 0,
307 ],
308 2,
309 )
310 } else if n < 1000 {
311 (
312 [
313 b'0' + u8::try_from(n / 100).unwrap_or(0),
314 b'0' + u8::try_from((n / 10) % 10).unwrap_or(0),
315 b'0' + u8::try_from(n % 10).unwrap_or(0),
316 0,
317 ],
318 3,
319 )
320 } else {
321 (
322 [
323 b'0' + u8::try_from(n / 1000).unwrap_or(0),
324 b'0' + u8::try_from((n / 100) % 10).unwrap_or(0),
325 b'0' + u8::try_from((n / 10) % 10).unwrap_or(0),
326 b'0' + u8::try_from(n % 10).unwrap_or(0),
327 ],
328 4,
329 )
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 extern crate alloc;
336
337 use alloc::format;
338
339 use super::*;
340
341 macro_rules! assert_fmt {
342 ($val:expr, $unit:path, $std:path, $space:expr, $expected:expr) => {
343 let fmt = ByteFormatter::new()
344 .unit($unit)
345 .standard($std)
346 .space($space)
347 .format($val);
348 assert_eq!(fmt.as_str().unwrap(), $expected);
349 };
350 }
351
352 #[test]
353 fn test_zero() {
354 assert_fmt!(0, Unit::Bytes, Standard::SI, true, "0 B");
355 assert_fmt!(0, Unit::Bits, Standard::SI, true, "0 b");
356 assert_fmt!(0, Unit::Bytes, Standard::Binary, false, "0B");
357 assert_fmt!(0, Unit::Bits, Standard::Binary, false, "0b");
358 }
359
360 #[test]
361 fn test_base_units_under_1000() {
362 assert_fmt!(1, Unit::Bytes, Standard::SI, true, "1 B");
364 assert_fmt!(12, Unit::Bytes, Standard::Binary, true, "12 B");
365 assert_fmt!(345, Unit::Bytes, Standard::SI, false, "345B");
366 assert_fmt!(999, Unit::Bytes, Standard::SI, true, "999 B");
367 assert_fmt!(999, Unit::Bytes, Standard::Binary, true, "999 B");
368 }
369
370 #[test]
371 fn test_si_exact_magnitudes() {
372 assert_fmt!(1_000, Unit::Bytes, Standard::SI, true, "1.00 kB");
374 assert_fmt!(1_000_000, Unit::Bytes, Standard::SI, true, "1.00 MB");
375 assert_fmt!(1_000_000_000, Unit::Bytes, Standard::SI, true, "1.00 GB");
376 assert_fmt!(
377 1_000_000_000_000,
378 Unit::Bytes,
379 Standard::SI,
380 true,
381 "1.00 TB"
382 );
383 assert_fmt!(
384 1_000_000_000_000_000,
385 Unit::Bytes,
386 Standard::SI,
387 true,
388 "1.00 PB"
389 );
390 assert_fmt!(
391 1_000_000_000_000_000_000,
392 Unit::Bytes,
393 Standard::SI,
394 true,
395 "1.00 EB"
396 );
397 }
398
399 #[test]
400 fn test_binary_exact_magnitudes() {
401 assert_fmt!(1_024, Unit::Bytes, Standard::Binary, true, "1.00 KiB");
403 assert_fmt!(1_048_576, Unit::Bytes, Standard::Binary, true, "1.00 MiB");
404 assert_fmt!(
405 1_073_741_824,
406 Unit::Bytes,
407 Standard::Binary,
408 true,
409 "1.00 GiB"
410 );
411 assert_fmt!(
412 1_099_511_627_776,
413 Unit::Bytes,
414 Standard::Binary,
415 true,
416 "1.00 TiB"
417 );
418 assert_fmt!(
419 1_125_899_906_842_624,
420 Unit::Bytes,
421 Standard::Binary,
422 true,
423 "1.00 PiB"
424 );
425 assert_fmt!(
426 1_152_921_504_606_846_976,
427 Unit::Bytes,
428 Standard::Binary,
429 true,
430 "1.00 EiB"
431 );
432 }
433
434 #[test]
435 fn test_si_vs_binary_difference() {
436 assert_fmt!(1_000, Unit::Bytes, Standard::SI, true, "1.00 kB");
438 assert_fmt!(1_000, Unit::Bytes, Standard::Binary, true, "1000 B");
439
440 assert_fmt!(1_023, Unit::Bytes, Standard::SI, true, "1.02 kB");
442 assert_fmt!(1_023, Unit::Bytes, Standard::Binary, true, "1023 B");
443 }
444
445 #[test]
446 fn test_rounding_and_decimals() {
447 assert_fmt!(1_500, Unit::Bytes, Standard::SI, true, "1.50 kB");
449
450 assert_fmt!(1_536, Unit::Bytes, Standard::Binary, true, "1.50 KiB");
452
453 assert_fmt!(1_004, Unit::Bytes, Standard::SI, true, "1.00 kB");
455
456 assert_fmt!(1_005, Unit::Bytes, Standard::SI, true, "1.01 kB");
458
459 assert_fmt!(1_230_000, Unit::Bytes, Standard::SI, true, "1.23 MB");
461 }
462
463 #[test]
464 fn test_carry_over_rounding() {
465 assert_fmt!(999_999, Unit::Bytes, Standard::SI, true, "1000.00 kB");
468
469 assert_fmt!(
472 1_048_575,
473 Unit::Bytes,
474 Standard::Binary,
475 true,
476 "1024.00 KiB"
477 );
478 }
479
480 #[test]
481 fn test_formatting_variations() {
482 let val = 2_500_000;
483
484 assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
486 assert_fmt!(val, Unit::Bits, Standard::SI, true, "2.50 Mb");
487
488 assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
490 assert_fmt!(val, Unit::Bytes, Standard::Binary, true, "2.38 MiB");
491
492 assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
494 assert_fmt!(val, Unit::Bytes, Standard::SI, false, "2.50MB");
495 }
496
497 #[test]
498 fn test_extreme_values() {
499 assert_fmt!(u64::MAX, Unit::Bytes, Standard::SI, true, "18.45 EB");
503
504 assert_fmt!(u64::MAX, Unit::Bytes, Standard::Binary, true, "16.00 EiB");
507 }
508
509 #[test]
510 fn test_as_bytes() {
511 let fmt = ByteFormatter::new()
513 .unit(Unit::Bytes)
514 .standard(Standard::SI)
515 .space(false)
516 .format(1500);
517 assert_eq!(fmt.as_bytes(), b"1.50kB");
518 }
519
520 #[test]
521 fn test_number_boundaries() {
522 assert_fmt!(9, Unit::Bytes, Standard::SI, true, "9 B"); assert_fmt!(10, Unit::Bytes, Standard::SI, true, "10 B"); assert_fmt!(99, Unit::Bytes, Standard::SI, true, "99 B"); assert_fmt!(100, Unit::Bytes, Standard::SI, true, "100 B"); assert_fmt!(999, Unit::Bytes, Standard::SI, true, "999 B"); }
529
530 #[test]
531 fn test_display_trait() {
532 let fmt = ByteFormatter::new()
533 .unit(Unit::Bytes)
534 .standard(Standard::Binary)
535 .space(true)
536 .format(1_048_576);
537
538 let output = format!("{fmt}");
540
541 assert_eq!(output, "1.00 MiB");
542 }
543}