Skip to main content

canic_core/format/
mod.rs

1//!
2//! Small formatting helpers shared across logs and status responses.
3//!
4use std::fmt::{self, Display, Formatter};
5
6///
7/// OptionalDisplay
8///
9
10pub struct OptionalDisplay<T>(pub Option<T>);
11
12impl<T> Display for OptionalDisplay<T>
13where
14    T: Display,
15{
16    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
17        match &self.0 {
18            Some(value) => value.fmt(f),
19            None => f.write_str("None"),
20        }
21    }
22}
23
24///
25/// Truncate a string to at most `max_chars` Unicode scalar values.
26///
27/// Returns the original string when it already fits.
28///
29#[must_use]
30pub fn truncate(s: &str, max_chars: usize) -> String {
31    let mut chars = s.chars();
32    let truncated: String = chars.by_ref().take(max_chars).collect();
33
34    if chars.next().is_some() {
35        truncated
36    } else {
37        s.to_string()
38    }
39}
40
41///
42/// Format a byte size using IEC units with two decimal places.
43///
44/// Examples: `512.00 B`, `720.79 KiB`, `13.61 MiB`.
45///
46#[must_use]
47#[expect(clippy::cast_precision_loss)]
48pub fn byte_size(bytes: u64) -> String {
49    const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
50
51    let mut value = bytes as f64;
52    let mut unit_index = 0usize;
53
54    while value >= 1024.0 && unit_index < UNITS.len() - 1 {
55        value /= 1024.0;
56        unit_index += 1;
57    }
58
59    format!("{value:.2} {}", UNITS[unit_index])
60}
61
62///
63/// Format a cycle balance in trillions with two decimal places.
64///
65/// Examples: `4.49 TC`, `12.35 TC`.
66///
67#[must_use]
68pub fn cycles_tc(cycles: u128) -> String {
69    const HUNDREDTH_TC: u128 = 10_000_000_000;
70
71    let hundredths = cycles.saturating_add(HUNDREDTH_TC / 2) / HUNDREDTH_TC;
72    format!("{}.{:02} TC", hundredths / 100, hundredths % 100)
73}
74
75///
76/// Format one optional display value for logs and status output.
77///
78#[must_use]
79pub const fn display_optional<T>(value: Option<T>) -> OptionalDisplay<T>
80where
81    T: Display,
82{
83    OptionalDisplay(value)
84}
85
86///
87/// TESTS
88///
89
90#[cfg(test)]
91mod tests {
92    use super::{byte_size, cycles_tc, display_optional, truncate};
93    use crate::cdk::types::Principal;
94
95    #[test]
96    fn keeps_short_strings() {
97        assert_eq!(truncate("root", 9), "root");
98        assert_eq!(truncate("abcdefgh", 9), "abcdefgh");
99        assert_eq!(truncate("abcdefghi", 9), "abcdefghi");
100    }
101
102    #[test]
103    fn truncates_long_strings() {
104        assert_eq!(truncate("abcdefghijkl", 9), "abcdefghi");
105        assert_eq!(truncate("abcdefghijklmnopqrstuvwxyz", 9), "abcdefghi");
106    }
107
108    #[test]
109    fn formats_small_byte_sizes() {
110        assert_eq!(byte_size(0), "0.00 B");
111        assert_eq!(byte_size(512), "512.00 B");
112        assert_eq!(byte_size(1024), "1.00 KiB");
113    }
114
115    #[test]
116    fn formats_larger_byte_sizes() {
117        assert_eq!(byte_size(720_795), "703.90 KiB");
118        assert_eq!(byte_size(13_936_529), "13.29 MiB");
119        assert_eq!(byte_size(9_102_643), "8.68 MiB");
120    }
121
122    #[test]
123    fn formats_cycles_in_tc() {
124        assert_eq!(cycles_tc(4_487_280_757_485), "4.49 TC");
125        assert_eq!(cycles_tc(12_345_678_900_000), "12.35 TC");
126    }
127
128    #[test]
129    fn formats_optional_display_values() {
130        let pid = Principal::from_slice(&[7; 29]);
131        assert_eq!(display_optional(Some(pid)).to_string(), pid.to_string());
132        assert_eq!(display_optional::<Principal>(None).to_string(), "None");
133    }
134}