compile_fmt/
argument.rs

1//! [`Argument`] and related types.
2
3use core::fmt;
4
5use crate::{
6    format::{Fmt, FormatArgument, Pad, StrFormat, StrLength},
7    utils::{assert_is_ascii, count_chars, ClippedStr},
8    CompileArgs,
9};
10
11#[derive(Debug, Clone, Copy)]
12enum ArgumentInner<'a> {
13    Str(&'a str, Option<StrFormat>),
14    Char(char),
15    Int(i128),
16    UnsignedInt(u128),
17}
18
19impl ArgumentInner<'_> {
20    const fn formatted_len(&self) -> StrLength {
21        match self {
22            Self::Str(s, None) => StrLength::for_str(s),
23            Self::Str(s, Some(fmt)) => match ClippedStr::new(s, fmt.clip_at) {
24                ClippedStr::Full(_) => StrLength::for_str(s),
25                ClippedStr::Clipped(bytes) => StrLength {
26                    bytes: bytes.len() + fmt.using.len(),
27                    chars: fmt.clip_at + count_chars(fmt.using),
28                },
29            },
30            Self::Char(c) => StrLength::for_char(*c),
31            Self::Int(value) => {
32                let bytes = (*value < 0) as usize + log_10_ceil(value.unsigned_abs());
33                StrLength::both(bytes)
34            }
35            Self::UnsignedInt(value) => StrLength::both(log_10_ceil(*value)),
36        }
37    }
38}
39
40/// Generalized argument in crate macros.
41#[doc(hidden)] // implementation detail of crate macros
42#[derive(Debug, Clone, Copy)]
43pub struct Argument<'a> {
44    inner: ArgumentInner<'a>,
45    pad: Option<Pad>,
46}
47
48impl Argument<'_> {
49    /// Returns the formatted length of the argument in bytes.
50    pub const fn formatted_len(&self) -> usize {
51        let non_padded_len = self.inner.formatted_len();
52        if let Some(pad) = &self.pad {
53            if pad.width > non_padded_len.chars {
54                let pad_char_count = pad.width - non_padded_len.chars;
55                pad_char_count * pad.using.len_utf8() + non_padded_len.bytes
56            } else {
57                // The non-padded string is longer than the pad width; it won't be padded
58                non_padded_len.bytes
59            }
60        } else {
61            non_padded_len.bytes
62        }
63    }
64}
65
66const fn log_10_ceil(mut value: u128) -> usize {
67    if value == 0 {
68        return 1;
69    }
70
71    let mut log = 0;
72    while value > 0 {
73        value /= 10;
74        log += 1;
75    }
76    log
77}
78
79impl<const CAP: usize> CompileArgs<CAP> {
80    const fn write_u128(self, mut value: u128) -> Self {
81        let new_len = self.len + log_10_ceil(value);
82        let mut buffer = self.buffer;
83        let mut pos = new_len - 1;
84
85        loop {
86            buffer[pos] = b'0' + (value % 10) as u8;
87            if pos == self.len {
88                break;
89            }
90            value /= 10;
91            pos -= 1;
92        }
93        Self {
94            buffer,
95            len: new_len,
96        }
97    }
98
99    const fn write_i128(self, value: i128) -> Self {
100        let this = if value < 0 {
101            self.write_char('-')
102        } else {
103            self
104        };
105        this.write_u128(value.unsigned_abs())
106    }
107
108    pub(crate) const fn format_arg(mut self, arg: Argument) -> Self {
109        let pad_after = 'compute_pad: {
110            if let Some(pad) = &arg.pad {
111                // Check if the argument must be padded.
112                let non_padded_len = arg.inner.formatted_len();
113                if pad.width > non_padded_len.chars {
114                    let (pad_before, pad_after) = pad.compute_padding(non_padded_len.chars);
115                    let mut count = 0;
116                    while count < pad_before {
117                        self = self.write_char(pad.using);
118                        count += 1;
119                    }
120                    break 'compute_pad Some((pad_after, pad.using));
121                }
122            }
123            None
124        };
125
126        self = match arg.inner {
127            ArgumentInner::Str(s, fmt) => self.write_str(s, fmt),
128            // chars and ints are not affected by format so far (i.e., not clipped)
129            ArgumentInner::Char(c) => self.write_char(c),
130            ArgumentInner::Int(value) => self.write_i128(value),
131            ArgumentInner::UnsignedInt(value) => self.write_u128(value),
132        };
133        if let Some((pad_after, using)) = pad_after {
134            let mut count = 0;
135            while count < pad_after {
136                self = self.write_char(using);
137                count += 1;
138            }
139        }
140        self
141    }
142}
143
144/// ASCII string wrapper.
145///
146/// This wrapper is useful for non-constant strings if it can be ensured that a string consists
147/// entirely of ASCII chars. This allows decreasing capacity requirements for [`CompileArgs`]
148/// involving such strings. In the general case, `CompileArgs` logic must assume that each char
149/// in a string can require up to 4 bytes; in case of `Ascii` strings, this is reduced to
150/// 1 byte per char.
151///
152/// # Examples
153///
154/// ```
155/// use compile_fmt::{clip, clip_ascii, compile_args, Ascii, CompileArgs};
156///
157/// let s: CompileArgs<10> = compile_args!(
158///     "[", Ascii::new("test") => clip_ascii(8, "").pad_left(8, ' '), "]"
159/// );
160/// assert_eq!(s.as_str(), "[test    ]");
161///
162/// // The necessary capacity for generic UTF-8 strings is greater
163/// // (34 bytes instead of 10):
164/// let s: CompileArgs<34> = compile_args!(
165///     "[", "test" => clip(8, "").pad_left(8, ' '), "]"
166/// );
167/// assert_eq!(s.as_str(), "[test    ]");
168/// ```
169#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
170pub struct Ascii<'a>(pub(crate) &'a str);
171
172impl<'a> Ascii<'a> {
173    /// Wraps the provided string if it consists entirely of ASCII chars.
174    ///
175    /// # Panics
176    ///
177    /// Panics if the string contains non-ASCII chars.
178    pub const fn new(s: &'a str) -> Self {
179        assert_is_ascii(s);
180        Self(s)
181    }
182}
183
184/// Wrapper for an admissible argument type allowing to convert it to an [`Argument`] in compile time.
185#[doc(hidden)] // implementation detail of crate macros
186pub struct ArgumentWrapper<T: FormatArgument> {
187    value: T,
188    fmt: Option<Fmt<T>>,
189}
190
191impl<T> fmt::Debug for ArgumentWrapper<T>
192where
193    T: FormatArgument + fmt::Debug,
194    T::Details: fmt::Debug,
195{
196    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197        formatter
198            .debug_struct("ArgumentWrapper")
199            .field("value", &self.value)
200            .field("fmt", &self.fmt)
201            .finish()
202    }
203}
204
205impl<T: FormatArgument> ArgumentWrapper<T> {
206    pub const fn new(value: T) -> Self {
207        Self { value, fmt: None }
208    }
209
210    #[must_use]
211    pub const fn with_fmt(mut self, fmt: Fmt<T>) -> Self {
212        self.fmt = Some(fmt);
213        self
214    }
215}
216
217impl<'a> ArgumentWrapper<&'a str> {
218    /// Performs the conversion.
219    pub const fn into_argument(self) -> Argument<'a> {
220        let (str_fmt, pad) = match self.fmt {
221            Some(Fmt { details, pad, .. }) => (Some(details), pad),
222            None => (None, None),
223        };
224        Argument {
225            inner: ArgumentInner::Str(self.value, str_fmt),
226            pad,
227        }
228    }
229}
230
231impl<'a> ArgumentWrapper<Ascii<'a>> {
232    /// Performs the conversion.
233    pub const fn into_argument(self) -> Argument<'a> {
234        let (str_fmt, pad) = match self.fmt {
235            Some(Fmt { details, pad, .. }) => (Some(details), pad),
236            None => (None, None),
237        };
238        Argument {
239            inner: ArgumentInner::Str(self.value.0, str_fmt),
240            pad,
241        }
242    }
243}
244
245impl<'a, const CAP: usize> ArgumentWrapper<&'a CompileArgs<CAP>> {
246    /// Performs the conversion.
247    pub const fn into_argument(self) -> Argument<'a> {
248        Argument {
249            inner: ArgumentInner::Str(self.value.as_str(), None),
250            pad: None,
251        }
252    }
253}
254
255impl ArgumentWrapper<i128> {
256    /// Performs the conversion.
257    pub const fn into_argument(self) -> Argument<'static> {
258        let pad = match self.fmt {
259            Some(Fmt { pad, .. }) => pad,
260            None => None,
261        };
262        Argument {
263            inner: ArgumentInner::Int(self.value),
264            pad,
265        }
266    }
267}
268
269macro_rules! impl_argument_wrapper_for_int {
270    ($int:ty) => {
271        impl ArgumentWrapper<$int> {
272            /// Performs the conversion.
273            pub const fn into_argument(self) -> Argument<'static> {
274                let pad = match self.fmt {
275                    Some(Fmt { pad, .. }) => pad,
276                    None => None,
277                };
278                Argument {
279                    inner: ArgumentInner::Int(self.value as i128),
280                    pad,
281                }
282            }
283        }
284    };
285}
286
287impl_argument_wrapper_for_int!(i8);
288impl_argument_wrapper_for_int!(i16);
289impl_argument_wrapper_for_int!(i32);
290impl_argument_wrapper_for_int!(i64);
291impl_argument_wrapper_for_int!(isize);
292
293impl ArgumentWrapper<u128> {
294    /// Performs the conversion.
295    pub const fn into_argument(self) -> Argument<'static> {
296        let pad = match self.fmt {
297            Some(Fmt { pad, .. }) => pad,
298            None => None,
299        };
300        Argument {
301            inner: ArgumentInner::UnsignedInt(self.value),
302            pad,
303        }
304    }
305}
306
307macro_rules! impl_argument_wrapper_for_uint {
308    ($uint:ty) => {
309        impl ArgumentWrapper<$uint> {
310            /// Performs the conversion.
311            pub const fn into_argument(self) -> Argument<'static> {
312                let pad = match self.fmt {
313                    Some(Fmt { pad, .. }) => pad,
314                    None => None,
315                };
316                Argument {
317                    inner: ArgumentInner::UnsignedInt(self.value as u128),
318                    pad,
319                }
320            }
321        }
322    };
323}
324
325impl_argument_wrapper_for_uint!(u8);
326impl_argument_wrapper_for_uint!(u16);
327impl_argument_wrapper_for_uint!(u32);
328impl_argument_wrapper_for_uint!(u64);
329impl_argument_wrapper_for_uint!(usize);
330
331impl ArgumentWrapper<char> {
332    /// Performs the conversion.
333    pub const fn into_argument(self) -> Argument<'static> {
334        let pad = match self.fmt {
335            Some(Fmt { pad, .. }) => pad,
336            None => None,
337        };
338        Argument {
339            inner: ArgumentInner::Char(self.value),
340            pad,
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use rand::{rngs::StdRng, Rng, SeedableRng};
348
349    use core::fmt::Alignment;
350    use std::string::ToString;
351
352    use super::*;
353
354    #[test]
355    fn length_estimation_for_small_ints() {
356        for i in 0_u8..=u8::MAX {
357            assert_eq!(
358                ArgumentWrapper::new(i).into_argument().formatted_len(),
359                i.to_string().len(),
360                "Formatted length estimated incorrectly for {i}"
361            );
362        }
363        for i in 0_u16..=u16::MAX {
364            assert_eq!(
365                ArgumentWrapper::new(i).into_argument().formatted_len(),
366                i.to_string().len(),
367                "Formatted length estimated incorrectly for {i}"
368            );
369        }
370        for i in i8::MIN..=i8::MAX {
371            assert_eq!(
372                ArgumentWrapper::new(i).into_argument().formatted_len(),
373                i.to_string().len(),
374                "Formatted length estimated incorrectly for {i}"
375            );
376        }
377        for i in i16::MIN..=i16::MAX {
378            assert_eq!(
379                ArgumentWrapper::new(i).into_argument().formatted_len(),
380                i.to_string().len(),
381                "Formatted length estimated incorrectly for {i}"
382            );
383        }
384    }
385
386    #[test]
387    fn length_estimation_for_large_ints() {
388        const RNG_SEED: u64 = 123;
389        const SAMPLE_COUNT: usize = 100_000;
390
391        let mut rng = StdRng::seed_from_u64(RNG_SEED);
392        for _ in 0..SAMPLE_COUNT {
393            let i: u32 = rng.gen();
394            assert_eq!(
395                ArgumentWrapper::new(i).into_argument().formatted_len(),
396                i.to_string().len(),
397                "Formatted length estimated incorrectly for {i}"
398            );
399        }
400        for _ in 0..SAMPLE_COUNT {
401            let i: u64 = rng.gen();
402            assert_eq!(
403                ArgumentWrapper::new(i).into_argument().formatted_len(),
404                i.to_string().len(),
405                "Formatted length estimated incorrectly for {i}"
406            );
407        }
408        for _ in 0..SAMPLE_COUNT {
409            let i: u128 = rng.gen();
410            assert_eq!(
411                ArgumentWrapper::new(i).into_argument().formatted_len(),
412                i.to_string().len(),
413                "Formatted length estimated incorrectly for {i}"
414            );
415        }
416        for _ in 0..SAMPLE_COUNT {
417            let i: usize = rng.gen();
418            assert_eq!(
419                ArgumentWrapper::new(i).into_argument().formatted_len(),
420                i.to_string().len(),
421                "Formatted length estimated incorrectly for {i}"
422            );
423        }
424
425        for _ in 0..SAMPLE_COUNT {
426            let i: i32 = rng.gen();
427            assert_eq!(
428                ArgumentWrapper::new(i).into_argument().formatted_len(),
429                i.to_string().len(),
430                "Formatted length estimated incorrectly for {i}"
431            );
432        }
433        for _ in 0..SAMPLE_COUNT {
434            let i: i64 = rng.gen();
435            assert_eq!(
436                ArgumentWrapper::new(i).into_argument().formatted_len(),
437                i.to_string().len(),
438                "Formatted length estimated incorrectly for {i}"
439            );
440        }
441        for _ in 0..SAMPLE_COUNT {
442            let i: i128 = rng.gen();
443            assert_eq!(
444                ArgumentWrapper::new(i).into_argument().formatted_len(),
445                i.to_string().len(),
446                "Formatted length estimated incorrectly for {i}"
447            );
448        }
449        for _ in 0..SAMPLE_COUNT {
450            let i: isize = rng.gen();
451            assert_eq!(
452                ArgumentWrapper::new(i).into_argument().formatted_len(),
453                i.to_string().len(),
454                "Formatted length estimated incorrectly for {i}"
455            );
456        }
457    }
458
459    #[test]
460    fn formatted_len_for_clipped_strings() {
461        let arg = ArgumentInner::Str(
462            "teßt",
463            Some(StrFormat {
464                clip_at: 2,
465                using: "",
466            }),
467        );
468        assert_eq!(arg.formatted_len(), StrLength::for_str("te"));
469
470        let arg = ArgumentInner::Str(
471            "teßt",
472            Some(StrFormat {
473                clip_at: 2,
474                using: "...",
475            }),
476        );
477        assert_eq!(arg.formatted_len(), StrLength::for_str("te..."));
478
479        let arg = ArgumentInner::Str(
480            "teßt",
481            Some(StrFormat {
482                clip_at: 2,
483                using: "…",
484            }),
485        );
486        assert_eq!(arg.formatted_len(), StrLength::for_str("te…"));
487
488        let arg = ArgumentInner::Str(
489            "teßt",
490            Some(StrFormat {
491                clip_at: 3,
492                using: "",
493            }),
494        );
495        assert_eq!(arg.formatted_len(), StrLength::for_str("teß"));
496
497        let arg = ArgumentInner::Str(
498            "teßt",
499            Some(StrFormat {
500                clip_at: 3,
501                using: "…",
502            }),
503        );
504        assert_eq!(arg.formatted_len(), StrLength::for_str("teß…"));
505
506        let arg = ArgumentInner::Str(
507            "teßt",
508            Some(StrFormat {
509                clip_at: 3,
510                using: "-",
511            }),
512        );
513        assert_eq!(arg.formatted_len(), StrLength::for_str("teß-"));
514
515        for clip_at in [4, 5, 16] {
516            for using in ["", "...", "…"] {
517                let arg = ArgumentInner::Str("teßt", Some(StrFormat { clip_at, using }));
518                assert_eq!(arg.formatted_len(), StrLength::for_str("teßt"));
519            }
520        }
521    }
522
523    #[test]
524    fn formatted_len_with_padding() {
525        let argument = Argument {
526            inner: ArgumentInner::Str("teßt", None),
527            pad: Some(Pad {
528                align: Alignment::Left,
529                width: 8,
530                using: ' ',
531            }),
532        };
533        assert_eq!(argument.formatted_len(), "teßt    ".len());
534
535        let argument = Argument {
536            inner: ArgumentInner::Str("teßt", None),
537            pad: Some(Pad {
538                align: Alignment::Left,
539                width: 8,
540                using: '💣',
541            }),
542        };
543        assert_eq!(argument.formatted_len(), "teßt💣💣💣💣".len());
544
545        for pad_width in 1..=4 {
546            let argument = Argument {
547                inner: ArgumentInner::Str("teßt", None),
548                pad: Some(Pad {
549                    align: Alignment::Left,
550                    width: pad_width,
551                    using: ' ',
552                }),
553            };
554            assert_eq!(argument.formatted_len(), "teßt".len());
555        }
556    }
557
558    #[test]
559    fn formatted_len_with_padding_and_clipping() {
560        let inner = ArgumentInner::Str(
561            "teßt",
562            Some(StrFormat {
563                clip_at: 3,
564                using: "…",
565            }),
566        );
567        let argument = Argument {
568            inner,
569            pad: Some(Pad {
570                align: Alignment::Left,
571                width: 8,
572                using: ' ',
573            }),
574        };
575        assert_eq!(argument.formatted_len(), "teß…    ".len());
576
577        let argument = Argument {
578            inner,
579            pad: Some(Pad {
580                align: Alignment::Left,
581                width: 8,
582                using: '💣',
583            }),
584        };
585        assert_eq!(argument.formatted_len(), "teß…💣💣💣💣".len());
586
587        for pad_width in 1..=4 {
588            let argument = Argument {
589                inner,
590                pad: Some(Pad {
591                    align: Alignment::Left,
592                    width: pad_width,
593                    using: ' ',
594                }),
595            };
596            assert_eq!(argument.formatted_len(), "teß…".len());
597        }
598    }
599
600    #[test]
601    #[should_panic(expected = "String 'teß…' contains non-ASCII chars; first at position 2")]
602    fn ascii_panic() {
603        Ascii::new("teß…");
604    }
605}