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(feature = "defmt")]
336impl defmt::Format for FormattedBytes {
337 fn format(&self, fmt: defmt::Formatter) {
338 if let Ok(text) = self.as_str() {
341 defmt::write!(fmt, "{=str}", text);
342 } else {
343 defmt::write!(fmt, "<prettier-bytes: invalid utf-8>");
346 }
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 extern crate alloc;
353
354 use alloc::format;
355
356 use super::*;
357
358 macro_rules! assert_fmt {
359 ($val:expr, $unit:path, $std:path, $space:expr, $expected:expr) => {
360 let fmt = ByteFormatter::new()
361 .unit($unit)
362 .standard($std)
363 .space($space)
364 .format($val);
365 assert_eq!(fmt.as_str().unwrap(), $expected);
366 };
367 }
368
369 #[test]
370 fn test_zero() {
371 assert_fmt!(0, Unit::Bytes, Standard::SI, true, "0 B");
372 assert_fmt!(0, Unit::Bits, Standard::SI, true, "0 b");
373 assert_fmt!(0, Unit::Bytes, Standard::Binary, false, "0B");
374 assert_fmt!(0, Unit::Bits, Standard::Binary, false, "0b");
375 }
376
377 #[test]
378 fn test_base_units_under_1000() {
379 assert_fmt!(1, Unit::Bytes, Standard::SI, true, "1 B");
381 assert_fmt!(12, Unit::Bytes, Standard::Binary, true, "12 B");
382 assert_fmt!(345, Unit::Bytes, Standard::SI, false, "345B");
383 assert_fmt!(999, Unit::Bytes, Standard::SI, true, "999 B");
384 assert_fmt!(999, Unit::Bytes, Standard::Binary, true, "999 B");
385 }
386
387 #[test]
388 fn test_si_exact_magnitudes() {
389 assert_fmt!(1_000, Unit::Bytes, Standard::SI, true, "1.00 kB");
391 assert_fmt!(1_000_000, Unit::Bytes, Standard::SI, true, "1.00 MB");
392 assert_fmt!(1_000_000_000, Unit::Bytes, Standard::SI, true, "1.00 GB");
393 assert_fmt!(
394 1_000_000_000_000,
395 Unit::Bytes,
396 Standard::SI,
397 true,
398 "1.00 TB"
399 );
400 assert_fmt!(
401 1_000_000_000_000_000,
402 Unit::Bytes,
403 Standard::SI,
404 true,
405 "1.00 PB"
406 );
407 assert_fmt!(
408 1_000_000_000_000_000_000,
409 Unit::Bytes,
410 Standard::SI,
411 true,
412 "1.00 EB"
413 );
414 }
415
416 #[test]
417 fn test_binary_exact_magnitudes() {
418 assert_fmt!(1_024, Unit::Bytes, Standard::Binary, true, "1.00 KiB");
420 assert_fmt!(1_048_576, Unit::Bytes, Standard::Binary, true, "1.00 MiB");
421 assert_fmt!(
422 1_073_741_824,
423 Unit::Bytes,
424 Standard::Binary,
425 true,
426 "1.00 GiB"
427 );
428 assert_fmt!(
429 1_099_511_627_776,
430 Unit::Bytes,
431 Standard::Binary,
432 true,
433 "1.00 TiB"
434 );
435 assert_fmt!(
436 1_125_899_906_842_624,
437 Unit::Bytes,
438 Standard::Binary,
439 true,
440 "1.00 PiB"
441 );
442 assert_fmt!(
443 1_152_921_504_606_846_976,
444 Unit::Bytes,
445 Standard::Binary,
446 true,
447 "1.00 EiB"
448 );
449 }
450
451 #[test]
452 fn test_si_vs_binary_difference() {
453 assert_fmt!(1_000, Unit::Bytes, Standard::SI, true, "1.00 kB");
455 assert_fmt!(1_000, Unit::Bytes, Standard::Binary, true, "1000 B");
456
457 assert_fmt!(1_023, Unit::Bytes, Standard::SI, true, "1.02 kB");
459 assert_fmt!(1_023, Unit::Bytes, Standard::Binary, true, "1023 B");
460 }
461
462 #[test]
463 fn test_rounding_and_decimals() {
464 assert_fmt!(1_500, Unit::Bytes, Standard::SI, true, "1.50 kB");
466
467 assert_fmt!(1_536, Unit::Bytes, Standard::Binary, true, "1.50 KiB");
469
470 assert_fmt!(1_004, Unit::Bytes, Standard::SI, true, "1.00 kB");
472
473 assert_fmt!(1_005, Unit::Bytes, Standard::SI, true, "1.01 kB");
475
476 assert_fmt!(1_230_000, Unit::Bytes, Standard::SI, true, "1.23 MB");
478 }
479
480 #[test]
481 fn test_carry_over_rounding() {
482 assert_fmt!(999_999, Unit::Bytes, Standard::SI, true, "1000.00 kB");
485
486 assert_fmt!(
489 1_048_575,
490 Unit::Bytes,
491 Standard::Binary,
492 true,
493 "1024.00 KiB"
494 );
495 }
496
497 #[test]
498 fn test_formatting_variations() {
499 let val = 2_500_000;
500
501 assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
503 assert_fmt!(val, Unit::Bits, Standard::SI, true, "2.50 Mb");
504
505 assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
507 assert_fmt!(val, Unit::Bytes, Standard::Binary, true, "2.38 MiB");
508
509 assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
511 assert_fmt!(val, Unit::Bytes, Standard::SI, false, "2.50MB");
512 }
513
514 #[test]
515 fn test_extreme_values() {
516 assert_fmt!(u64::MAX, Unit::Bytes, Standard::SI, true, "18.45 EB");
520
521 assert_fmt!(u64::MAX, Unit::Bytes, Standard::Binary, true, "16.00 EiB");
524 }
525
526 #[test]
527 fn test_as_bytes() {
528 let fmt = ByteFormatter::new()
530 .unit(Unit::Bytes)
531 .standard(Standard::SI)
532 .space(false)
533 .format(1500);
534 assert_eq!(fmt.as_bytes(), b"1.50kB");
535 }
536
537 #[test]
538 fn test_number_boundaries() {
539 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"); }
546
547 #[test]
548 fn test_display_trait() {
549 let fmt = ByteFormatter::new()
550 .unit(Unit::Bytes)
551 .standard(Standard::Binary)
552 .space(true)
553 .format(1_048_576);
554
555 let output = format!("{fmt}");
557
558 assert_eq!(output, "1.00 MiB");
559 }
560}