Skip to main content

canic_core/format/
mod.rs

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