compile_fmt/
format.rs

1//! `Fmt` and related types.
2
3use crate::argument::Ascii;
4use core::fmt::Alignment;
5
6use crate::utils::{assert_is_ascii, count_chars};
7
8/// Length of a string measured in bytes and chars.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct StrLength {
11    /// Number of bytes the string occupies.
12    pub bytes: usize,
13    /// Number of chars in the string.
14    pub chars: usize,
15}
16
17impl StrLength {
18    pub(crate) const fn for_str(s: &str) -> Self {
19        Self {
20            bytes: s.len(),
21            chars: count_chars(s),
22        }
23    }
24
25    pub(crate) const fn for_char(c: char) -> Self {
26        Self {
27            bytes: c.len_utf8(),
28            chars: 1,
29        }
30    }
31
32    /// Creates a length in which both `bytes` and `chars` fields are set to the specified `value`.
33    pub const fn both(value: usize) -> Self {
34        Self {
35            bytes: value,
36            chars: value,
37        }
38    }
39}
40
41#[derive(Debug, Clone, Copy)]
42pub(crate) struct Pad {
43    pub align: Alignment,
44    pub width: usize,
45    pub using: char,
46}
47
48impl Pad {
49    pub const fn compute_padding(&self, char_count: usize) -> (usize, usize) {
50        if char_count >= self.width {
51            return (0, 0);
52        }
53        match self.align {
54            Alignment::Left => (0, self.width - char_count),
55            Alignment::Right => (self.width - char_count, 0),
56            Alignment::Center => {
57                let total_padding = self.width - char_count;
58                (total_padding / 2, total_padding - total_padding / 2)
59            }
60        }
61    }
62}
63
64/// Formatting specification for an [`Argument`](crate::Argument).
65///
66/// A format is necessary to specify for *dynamic* arguments of [`compile_args!`](crate::compile_args)
67/// and related macros (i.e., for arguments that are not constants). For now, the only meaningful
68/// format customization is provided for strings (`&str`). All other arguments have the only
69/// available format that can be created using [`fmt()`].
70///
71/// # Examples
72///
73/// ## Clipping string to certain width
74///
75/// ```
76/// use compile_fmt::{compile_args, clip, fmt};
77///
78/// const fn format_clipped_str(s: &str) -> impl AsRef<str> {
79///     compile_args!(
80///         "Clipped string: '", s => clip(8, "…"),
81///         "', original length: ", s.len() => fmt::<usize>()
82///     )
83/// }
84///
85/// let s = format_clipped_str("very long string indeed");
86/// assert_eq!(
87///     s.as_ref(),
88///     "Clipped string: 'very lon…', original length: 23"
89/// );
90/// ```
91///
92/// ## Padding
93///
94/// ```
95/// # use compile_fmt::{compile_args, fmt};
96/// const fn format_with_padding(value: u32) -> impl AsRef<str> {
97///     compile_args!(
98///         "Number: ", value => fmt::<u32>().pad_right(4, '0')
99///     )
100/// }
101///
102/// let s = format_with_padding(42);
103/// assert_eq!(s.as_ref(), "Number: 0042");
104/// let s = format_with_padding(19_999);
105/// assert_eq!(s.as_ref(), "Number: 19999");
106/// // ^ If the string before padding contains more chars than in the padding spec,
107/// // padding is not applied at all.
108/// ```
109///
110/// Any Unicode char can be used as padding:
111///
112/// ```
113/// # use compile_fmt::{compile_args, fmt};
114/// let s = compile_args!(
115///     "Number: ", 42 => fmt::<u32>().pad_left(4, 'πŸ’£')
116/// );
117/// assert_eq!(s.as_str(), "Number: 42πŸ’£πŸ’£");
118/// ```
119///
120/// Strings can be padded as well:
121///
122/// ```
123/// # use compile_fmt::{compile_args, clip};
124/// const fn pad_str(s: &str) -> impl AsRef<str> {
125///     compile_args!("[", s => clip(8, "").pad_center(8, ' '), "]")
126/// }
127///
128/// assert_eq!(pad_str("test").as_ref(), "[  test  ]");
129/// assert_eq!(pad_str("test!").as_ref(), "[ test!  ]");
130/// ```
131#[derive(Debug, Clone, Copy)]
132pub struct Fmt<T: FormatArgument> {
133    /// Byte capacity of the format without taking padding into account. This is a field
134    /// rather than a method in `FormatArgument` because we wouldn't be able to call this method
135    /// in `const fn`s.
136    capacity: StrLength,
137    pub(crate) details: T::Details,
138    pub(crate) pad: Option<Pad>,
139}
140
141/// Creates a default format for a type that has known bounded formatting width.
142pub const fn fmt<T>() -> Fmt<T>
143where
144    T: FormatArgument<Details = ()> + MaxLength,
145{
146    Fmt {
147        capacity: T::MAX_LENGTH,
148        details: (),
149        pad: None,
150    }
151}
152
153/// Creates a format that will clip the value to the specified max **char** width (not byte width!).
154/// If clipped, the end of the string will be replaced with the specified replacer, which can be empty.
155///
156/// # Panics
157///
158/// Panics if `clip_at` is zero.
159pub const fn clip<'a>(clip_at: usize, using: &'static str) -> Fmt<&'a str> {
160    assert!(clip_at > 0, "Clip width must be positive");
161    Fmt {
162        capacity: StrLength {
163            bytes: clip_at * char::MAX_LENGTH.bytes + using.len(),
164            chars: clip_at + count_chars(using),
165        },
166        details: StrFormat { clip_at, using },
167        pad: None,
168    }
169}
170
171/// Same as [`clip()`], but for [`Ascii`] strings.
172///
173/// # Panics
174///
175/// Panics if `clip_at` is zero or `using` contains non-ASCII chars.
176pub const fn clip_ascii<'a>(clip_at: usize, using: &'static str) -> Fmt<Ascii<'a>> {
177    assert!(clip_at > 0, "Clip width must be positive");
178    assert_is_ascii(using);
179    Fmt {
180        capacity: StrLength::both(clip_at + using.len()),
181        details: StrFormat { clip_at, using },
182        pad: None,
183    }
184}
185
186impl<T: FormatArgument> Fmt<T> {
187    const fn pad(mut self, align: Alignment, width: usize, using: char) -> Self {
188        let pad = Pad {
189            align,
190            width,
191            using,
192        };
193        self.pad = Some(pad);
194        self
195    }
196
197    /// Specifies left-aligned padding. `width` is measured in chars, rather than bytes.
198    #[must_use]
199    pub const fn pad_left(self, width: usize, using: char) -> Self {
200        self.pad(Alignment::Left, width, using)
201    }
202
203    /// Specifies right-aligned padding. `width` is measured in chars, rather than bytes.
204    #[must_use]
205    pub const fn pad_right(self, width: usize, using: char) -> Self {
206        self.pad(Alignment::Right, width, using)
207    }
208
209    /// Specifies center-aligned padding. `width` is measured in chars, rather than bytes.
210    #[must_use]
211    pub const fn pad_center(self, width: usize, using: char) -> Self {
212        self.pad(Alignment::Center, width, using)
213    }
214
215    /// Returns the byte capacity of this format in bytes.
216    #[doc(hidden)] // only used by macros
217    pub const fn capacity(&self) -> usize {
218        if let Some(pad) = &self.pad {
219            // Capacity necessary for an empty non-padded string (which we assume is always possible).
220            let full_pad_capacity = pad.using.len_utf8() * pad.width;
221
222            let max_width = if self.capacity.chars > pad.width {
223                pad.width
224            } else {
225                self.capacity.chars
226            };
227            // Capacity necessary for the maximum-length string that still has padding.
228            let min_pad_capacity =
229                pad.using.len_utf8() * (pad.width - max_width) + max_width * T::MAX_BYTES_PER_CHAR;
230
231            // Select maximum of `max_pad_capacity`, `min_pad_capacity` and the original capacity.
232            let pad_capacity = if full_pad_capacity > min_pad_capacity {
233                full_pad_capacity
234            } else {
235                min_pad_capacity
236            };
237            if pad_capacity > self.capacity.bytes {
238                return pad_capacity;
239            }
240        }
241        self.capacity.bytes
242    }
243}
244
245/// Type that can be formatted. Implemented for standard integer types, `&str` and `char`.
246pub trait FormatArgument {
247    /// Formatting specification for the type.
248    type Details: 'static + Copy;
249    /// Maximum number of bytes a single char from this format can occupy.
250    #[doc(hidden)] // implementation detail
251    const MAX_BYTES_PER_CHAR: usize;
252}
253
254impl FormatArgument for &str {
255    type Details = StrFormat;
256    const MAX_BYTES_PER_CHAR: usize = 4;
257}
258
259impl FormatArgument for Ascii<'_> {
260    type Details = StrFormat;
261    const MAX_BYTES_PER_CHAR: usize = 1;
262}
263
264/// Formatting details for strings.
265#[doc(hidden)] // implementation detail
266#[derive(Debug, Clone, Copy)]
267pub struct StrFormat {
268    pub(crate) clip_at: usize,
269    pub(crate) using: &'static str,
270}
271
272/// Type that has a known upper boundary for the formatted length.
273pub trait MaxLength {
274    /// Upper boundary for the formatted length in bytes and chars.
275    const MAX_LENGTH: StrLength;
276}
277
278macro_rules! impl_max_width_for_uint {
279    ($($uint:ty),+) => {
280        $(
281        impl MaxLength for $uint {
282            const MAX_LENGTH: StrLength = StrLength::both(
283                crate::ArgumentWrapper::new(Self::MAX).into_argument().formatted_len(),
284            );
285        }
286
287        impl FormatArgument for $uint {
288            type Details = ();
289            const MAX_BYTES_PER_CHAR: usize = 1;
290        }
291        )+
292    };
293}
294
295impl_max_width_for_uint!(u8, u16, u32, u64, u128, usize);
296
297macro_rules! impl_max_width_for_int {
298    ($($int:ty),+) => {
299        $(
300        impl MaxLength for $int {
301            const MAX_LENGTH: StrLength = StrLength::both(
302                crate::ArgumentWrapper::new(Self::MIN).into_argument().formatted_len(),
303            );
304        }
305
306        impl FormatArgument for $int {
307            type Details = ();
308            const MAX_BYTES_PER_CHAR: usize = 1;
309        }
310        )+
311    };
312}
313
314impl_max_width_for_int!(i8, i16, i32, i64, i128, isize);
315
316impl MaxLength for char {
317    const MAX_LENGTH: StrLength = StrLength { bytes: 4, chars: 1 };
318}
319
320impl FormatArgument for char {
321    type Details = ();
322    const MAX_BYTES_PER_CHAR: usize = 4;
323}
324
325#[cfg(test)]
326mod tests {
327    use std::string::ToString;
328
329    use super::*;
330
331    #[test]
332    fn max_length_bound_is_correct() {
333        assert_eq!(u8::MAX_LENGTH.bytes, u8::MAX.to_string().len());
334        assert_eq!(u16::MAX_LENGTH.bytes, u16::MAX.to_string().len());
335        assert_eq!(u32::MAX_LENGTH.bytes, u32::MAX.to_string().len());
336        assert_eq!(u64::MAX_LENGTH.bytes, u64::MAX.to_string().len());
337        assert_eq!(u128::MAX_LENGTH.bytes, u128::MAX.to_string().len());
338        assert_eq!(usize::MAX_LENGTH.bytes, usize::MAX.to_string().len());
339
340        assert_eq!(i8::MAX_LENGTH.bytes, i8::MIN.to_string().len());
341        assert_eq!(i16::MAX_LENGTH.bytes, i16::MIN.to_string().len());
342        assert_eq!(i32::MAX_LENGTH.bytes, i32::MIN.to_string().len());
343        assert_eq!(i64::MAX_LENGTH.bytes, i64::MIN.to_string().len());
344        assert_eq!(i128::MAX_LENGTH.bytes, i128::MIN.to_string().len());
345        assert_eq!(isize::MAX_LENGTH.bytes, isize::MIN.to_string().len());
346    }
347
348    #[test]
349    fn capacity_for_padded_format() {
350        let format = fmt::<u8>().pad(Alignment::Right, 8, ' ');
351        assert_eq!(format.capacity(), 8);
352        let format = fmt::<u8>().pad(Alignment::Right, 8, 'ℝ');
353        assert_eq!(format.capacity(), 24); // each padding char is 3 bytes
354        let format = fmt::<u64>().pad(Alignment::Right, 8, ' ');
355        assert_eq!(format.capacity(), u64::MAX.to_string().len()); // original capacity
356
357        let format = clip(8, "").pad(Alignment::Left, 8, ' ');
358        assert_eq!(format.capacity.chars, 8);
359        assert_eq!(format.capacity.bytes, 32);
360        assert_eq!(format.capacity(), 32);
361
362        let format = clip(4, "").pad(Alignment::Left, 8, ' ');
363        assert_eq!(format.capacity.chars, 4);
364        assert_eq!(format.capacity.bytes, 16);
365        assert_eq!(format.capacity(), 20); // 16 + 4 padding chars
366
367        let format = clip(4, "").pad(Alignment::Left, 8, 'ß');
368        assert_eq!(format.capacity.chars, 4);
369        assert_eq!(format.capacity.bytes, 16);
370        assert_eq!(format.capacity(), 24); // 16 + 4 padding chars * 2 bytes each
371
372        let format = clip(4, "…").pad(Alignment::Left, 8, ' ');
373        assert_eq!(format.capacity.chars, 5);
374        assert_eq!(format.capacity.bytes, 16 + "…".len());
375        assert_eq!(format.capacity(), 23); // 20 (5 chars * 4 bytes) + 3 padding chars * 4 bytes each
376    }
377}