Skip to main content

smart_format/combinator/
pad.rs

1use core::fmt;
2
3use smart_string::DisplayExt;
4
5/// Right-pads formatted output to `width` characters using `fill`.
6///
7/// Single-pass: streams inner value through `format_with` counting chars, then
8/// appends fill characters if the output was shorter than `width`.
9pub struct PadRight<T> {
10    inner: T,
11    width: usize,
12    fill: char,
13}
14
15impl<T> PadRight<T> {
16    pub(crate) fn new(inner: T, width: usize, fill: char) -> Self {
17        PadRight { inner, width, fill }
18    }
19}
20
21impl<T: fmt::Display> fmt::Display for PadRight<T> {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        use fmt::Write;
24
25        let mut char_count: usize = 0;
26
27        self.inner.format_with(|s: Option<&str>| {
28            let Some(s) = s else { return Ok(()) };
29            char_count += s.chars().count();
30            f.write_str(s)
31        })?;
32
33        for _ in char_count..self.width {
34            f.write_char(self.fill)?;
35        }
36
37        Ok(())
38    }
39}
40
41/// Left-pads formatted output to `width` characters using `fill`.
42///
43/// # Implementation
44///
45/// Two-pass formatting:
46/// 1. First pass: format inner into a counting adapter (discards output) to measure char width.
47/// 2. Second pass: write `width - measured` fill chars, then format inner for real.
48///
49/// This is zero-alloc but formats the inner value twice. For the short values where
50/// padding typically matters, this cost is negligible.
51///
52/// Assumes `Display::fmt` is deterministic. Non-deterministic implementations (e.g.,
53/// values including timestamps) may produce incorrect padding width.
54pub struct PadLeft<T> {
55    inner: T,
56    width: usize,
57    fill: char,
58}
59
60impl<T> PadLeft<T> {
61    pub(crate) fn new(inner: T, width: usize, fill: char) -> Self {
62        PadLeft { inner, width, fill }
63    }
64}
65
66impl<T: fmt::Display> fmt::Display for PadLeft<T> {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        use fmt::Write;
69
70        // Pass 1: count chars without writing.
71        let mut char_count: usize = 0;
72        self.inner.format_with(|s: Option<&str>| {
73            if let Some(s) = s {
74                char_count += s.chars().count();
75            }
76            Ok(())
77        })?;
78
79        // Write padding.
80        for _ in char_count..self.width {
81            f.write_char(self.fill)?;
82        }
83
84        // Pass 2: write the actual content.
85        self.inner.fmt(f)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use crate::combinator::SmartFormat;
92
93    // --- PadRight ---
94
95    #[test]
96    fn pad_right_shorter() {
97        assert_eq!("hi...", "hi".display_pad_right(5, '.').to_string());
98    }
99
100    #[test]
101    fn pad_right_exact() {
102        assert_eq!("hello", "hello".display_pad_right(5, '.').to_string());
103    }
104
105    #[test]
106    fn pad_right_longer() {
107        assert_eq!("hello!", "hello!".display_pad_right(5, '.').to_string());
108    }
109
110    #[test]
111    fn pad_right_empty() {
112        assert_eq!(".....", "".display_pad_right(5, '.').to_string());
113    }
114
115    #[test]
116    fn pad_right_zero_width() {
117        assert_eq!("hello", "hello".display_pad_right(0, '.').to_string());
118    }
119
120    #[test]
121    fn pad_right_unicode() {
122        // "АБ" = 2 chars; pad to 5 with '.'
123        assert_eq!("АБ...", "АБ".display_pad_right(5, '.').to_string());
124    }
125
126    #[test]
127    fn pad_right_unicode_fill() {
128        assert_eq!("hi═══", "hi".display_pad_right(5, '═').to_string());
129    }
130
131    // --- PadLeft ---
132
133    #[test]
134    fn pad_left_shorter() {
135        assert_eq!("...hi", "hi".display_pad_left(5, '.').to_string());
136    }
137
138    #[test]
139    fn pad_left_exact() {
140        assert_eq!("hello", "hello".display_pad_left(5, '.').to_string());
141    }
142
143    #[test]
144    fn pad_left_longer() {
145        assert_eq!("hello!", "hello!".display_pad_left(5, '.').to_string());
146    }
147
148    #[test]
149    fn pad_left_empty() {
150        assert_eq!(".....", "".display_pad_left(5, '.').to_string());
151    }
152
153    #[test]
154    fn pad_left_zero_width() {
155        assert_eq!("hello", "hello".display_pad_left(0, '.').to_string());
156    }
157
158    #[test]
159    fn pad_left_unicode() {
160        assert_eq!("...АБ", "АБ".display_pad_left(5, '.').to_string());
161    }
162
163    #[test]
164    fn pad_left_number() {
165        assert_eq!("007", 7.display_pad_left(3, '0').to_string());
166    }
167
168    #[test]
169    fn pad_left_unicode_fill() {
170        assert_eq!("═══hi", "hi".display_pad_left(5, '═').to_string());
171    }
172}