Skip to main content

time/formatting/
formattable.rs

1//! A trait that can be used to format an item from its components.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use core::ops::Deref;
6use std::io;
7
8use deranged::{ru8, ru16};
9use num_conv::prelude::*;
10
11use crate::format_description::modifier::Padding;
12use crate::format_description::well_known::iso8601::EncodedConfig;
13use crate::format_description::well_known::{Iso8601, Rfc2822, Rfc3339};
14use crate::format_description::{
15    BorrowedFormatItem, FormatDescriptionV3, OwnedFormatItem, format_description_v3,
16};
17use crate::formatting::{
18    ComponentProvider, MONTH_NAMES, WEEKDAY_NAMES, fmt_component_v3, format_four_digits_pad_zero,
19    format_two_digits, iso8601, write, write_bytes, write_if_else,
20};
21use crate::internal_macros::try_likely_ok;
22use crate::{error, num_fmt};
23
24/// A type that describes a format.
25///
26/// Implementors of [`Formattable`] are [format descriptions](crate::format_description).
27///
28/// To format a value into a String, use the `format` method on the respective type.
29#[cfg_attr(docsrs, doc(notable_trait))]
30pub trait Formattable: sealed::Sealed {}
31impl Formattable for FormatDescriptionV3<'_> {}
32impl Formattable for BorrowedFormatItem<'_> {}
33impl Formattable for [BorrowedFormatItem<'_>] {}
34impl Formattable for OwnedFormatItem {}
35impl Formattable for [OwnedFormatItem] {}
36impl Formattable for Rfc3339 {}
37impl Formattable for Rfc2822 {}
38impl<const CONFIG: EncodedConfig> Formattable for Iso8601<CONFIG> {}
39impl<T> Formattable for T where T: Deref<Target: Formattable> {}
40
41/// Seal the trait to prevent downstream users from implementing it.
42mod sealed {
43    use super::*;
44    use crate::formatting::ComponentProvider;
45    use crate::formatting::metadata::ComputeMetadata;
46
47    /// Format the item using a format description, the intended output, and the various components.
48    #[expect(
49        private_bounds,
50        private_interfaces,
51        reason = "irrelevant due to being a sealed trait"
52    )]
53    pub trait Sealed: ComputeMetadata {
54        /// Format the item into the provided output, returning the number of bytes written.
55        fn format_into<V>(
56            &self,
57            output: &mut (impl io::Write + ?Sized),
58            value: &V,
59            state: &mut V::State,
60        ) -> Result<usize, error::Format>
61        where
62            V: ComponentProvider;
63
64        /// Format the item directly to a `String`.
65        #[inline]
66        fn format<V>(&self, value: &V, state: &mut V::State) -> Result<String, error::Format>
67        where
68            V: ComponentProvider,
69        {
70            let crate::formatting::metadata::Metadata {
71                max_bytes_needed,
72                guaranteed_utf8,
73            } = self.compute_metadata();
74
75            let mut buf = Vec::with_capacity(max_bytes_needed);
76            try_likely_ok!(self.format_into(&mut buf, value, state));
77            Ok(if guaranteed_utf8 {
78                // Safety: The output is guaranteed to be UTF-8.
79                unsafe { String::from_utf8_unchecked(buf) }
80            } else {
81                String::from_utf8_lossy(&buf).into_owned()
82            })
83        }
84    }
85}
86
87impl sealed::Sealed for FormatDescriptionV3<'_> {
88    #[expect(
89        private_bounds,
90        private_interfaces,
91        reason = "irrelevant due to being a sealed trait"
92    )]
93    #[inline]
94    fn format_into<V>(
95        &self,
96        output: &mut (impl io::Write + ?Sized),
97        value: &V,
98        state: &mut V::State,
99    ) -> Result<usize, error::Format>
100    where
101        V: ComponentProvider,
102    {
103        self.inner.format_into(output, value, state)
104    }
105}
106
107impl sealed::Sealed for format_description_v3::FormatDescriptionV3Inner<'_> {
108    #[expect(
109        private_bounds,
110        private_interfaces,
111        reason = "irrelevant due to being a sealed trait"
112    )]
113    #[inline]
114    fn format_into<V>(
115        &self,
116        output: &mut (impl io::Write + ?Sized),
117        value: &V,
118        state: &mut V::State,
119    ) -> Result<usize, error::Format>
120    where
121        V: ComponentProvider,
122    {
123        match &self {
124            Self::Component(component) => fmt_component_v3(output, value, state, component),
125            Self::BorrowedLiteral(literal) => {
126                write_bytes(output, literal.as_bytes()).map_err(Into::into)
127            }
128            Self::BorrowedCompound(items) => {
129                let mut bytes = 0;
130                for item in *items {
131                    bytes += try_likely_ok!(item.format_into(output, value, state));
132                }
133                Ok(bytes)
134            }
135            Self::BorrowedOptional {
136                format: should_format,
137                item,
138            } => {
139                if *should_format {
140                    item.format_into(output, value, state)
141                } else {
142                    Ok(0)
143                }
144            }
145            Self::BorrowedFirst(items) => match items {
146                [] => Ok(0),
147                [item, ..] => item.format_into(output, value, state),
148            },
149            Self::OwnedLiteral(literal) => {
150                write_bytes(output, literal.as_bytes()).map_err(Into::into)
151            }
152            Self::OwnedCompound(items) => {
153                let mut bytes = 0;
154                for item in &**items {
155                    bytes += try_likely_ok!(item.format_into(output, value, state));
156                }
157                Ok(bytes)
158            }
159            Self::OwnedOptional {
160                format: should_format,
161                item,
162            } => {
163                if *should_format {
164                    item.format_into(output, value, state)
165                } else {
166                    Ok(0)
167                }
168            }
169            Self::OwnedFirst(items) => match &items[..] {
170                [] => Ok(0),
171                [item, ..] => item.format_into(output, value, state),
172            },
173        }
174    }
175}
176
177impl sealed::Sealed for BorrowedFormatItem<'_> {
178    #[expect(
179        private_bounds,
180        private_interfaces,
181        reason = "irrelevant due to being a sealed trait"
182    )]
183    #[inline]
184    fn format_into<V>(
185        &self,
186        output: &mut (impl io::Write + ?Sized),
187        value: &V,
188        state: &mut V::State,
189    ) -> Result<usize, error::Format>
190    where
191        V: ComponentProvider,
192    {
193        Ok(match *self {
194            #[expect(deprecated)]
195            Self::Literal(literal) => try_likely_ok!(write_bytes(output, literal)),
196            Self::StringLiteral(literal) => try_likely_ok!(write(output, literal)),
197            Self::Component(component) => {
198                try_likely_ok!(fmt_component_v3(output, value, state, &component.into()))
199            }
200            Self::Compound(items) => try_likely_ok!((*items).format_into(output, value, state)),
201            Self::Optional(item) => try_likely_ok!((*item).format_into(output, value, state)),
202            Self::First(items) => match items {
203                [] => 0,
204                [item, ..] => try_likely_ok!((*item).format_into(output, value, state)),
205            },
206        })
207    }
208}
209
210impl sealed::Sealed for [BorrowedFormatItem<'_>] {
211    #[expect(
212        private_bounds,
213        private_interfaces,
214        reason = "irrelevant due to being a sealed trait"
215    )]
216    #[inline]
217    fn format_into<V>(
218        &self,
219        output: &mut (impl io::Write + ?Sized),
220        value: &V,
221        state: &mut V::State,
222    ) -> Result<usize, error::Format>
223    where
224        V: ComponentProvider,
225    {
226        let mut bytes = 0;
227        for item in self.iter() {
228            bytes += try_likely_ok!(item.format_into(output, value, state));
229        }
230        Ok(bytes)
231    }
232}
233
234impl sealed::Sealed for OwnedFormatItem {
235    #[expect(
236        private_bounds,
237        private_interfaces,
238        reason = "irrelevant due to being a sealed trait"
239    )]
240    #[inline]
241    fn format_into<V>(
242        &self,
243        output: &mut (impl io::Write + ?Sized),
244        value: &V,
245        state: &mut V::State,
246    ) -> Result<usize, error::Format>
247    where
248        V: ComponentProvider,
249    {
250        match self {
251            #[expect(deprecated)]
252            Self::Literal(literal) => Ok(try_likely_ok!(write_bytes(output, literal))),
253            Self::StringLiteral(literal) => Ok(try_likely_ok!(write(output, literal))),
254            Self::Component(component) => {
255                fmt_component_v3(output, value, state, &((*component).into()))
256            }
257            Self::Compound(items) => (**items).format_into(output, value, state),
258            Self::Optional(item) => (**item).format_into(output, value, state),
259            Self::First(items) => match &**items {
260                [] => Ok(0),
261                [item, ..] => (*item).format_into(output, value, state),
262            },
263        }
264    }
265}
266
267impl sealed::Sealed for [OwnedFormatItem] {
268    #[expect(
269        private_bounds,
270        private_interfaces,
271        reason = "irrelevant due to being a sealed trait"
272    )]
273    #[inline]
274    fn format_into<V>(
275        &self,
276        output: &mut (impl io::Write + ?Sized),
277        value: &V,
278        state: &mut V::State,
279    ) -> Result<usize, error::Format>
280    where
281        V: ComponentProvider,
282    {
283        let mut bytes = 0;
284        for item in self.iter() {
285            bytes += try_likely_ok!(item.format_into(output, value, state));
286        }
287        Ok(bytes)
288    }
289}
290
291impl<T> sealed::Sealed for T
292where
293    T: Deref<Target: sealed::Sealed>,
294{
295    #[expect(
296        private_bounds,
297        private_interfaces,
298        reason = "irrelevant due to being a sealed trait"
299    )]
300    #[inline]
301    fn format_into<V>(
302        &self,
303        output: &mut (impl io::Write + ?Sized),
304        value: &V,
305        state: &mut V::State,
306    ) -> Result<usize, error::Format>
307    where
308        V: ComponentProvider,
309    {
310        self.deref().format_into(output, value, state)
311    }
312}
313
314#[expect(
315    private_bounds,
316    private_interfaces,
317    reason = "irrelevant due to being a sealed trait"
318)]
319impl sealed::Sealed for Rfc2822 {
320    fn format_into<V>(
321        &self,
322        output: &mut (impl io::Write + ?Sized),
323        value: &V,
324        state: &mut V::State,
325    ) -> Result<usize, error::Format>
326    where
327        V: ComponentProvider,
328    {
329        const {
330            assert!(
331                V::SUPPLIES_DATE && V::SUPPLIES_TIME && V::SUPPLIES_OFFSET,
332                "Rfc2822 requires date, time, and offset components, but not all can be provided \
333                 by this type"
334            );
335        }
336
337        let mut bytes = 0;
338
339        if value.calendar_year(state).get() < 1900
340            // The RFC requires years be exactly four digits.
341            || (cfg!(feature = "large-dates") && value.calendar_year(state).get() >= 10_000)
342        {
343            crate::hint::cold_path();
344            return Err(error::Format::InvalidComponent("year"));
345        }
346        if value.offset_second(state).get() != 0 {
347            crate::hint::cold_path();
348            return Err(error::Format::InvalidComponent("offset_second"));
349        }
350
351        // Safety: All weekday names are at least 3 bytes long.
352        bytes += try_likely_ok!(write(output, unsafe {
353            WEEKDAY_NAMES[value
354                .weekday(state)
355                .number_days_from_monday()
356                .extend::<usize>()]
357            .get_unchecked(..3)
358        }));
359        bytes += try_likely_ok!(write(output, ", "));
360        bytes += try_likely_ok!(format_two_digits(
361            output,
362            value.day(state).expand(),
363            Padding::Zero
364        ));
365        bytes += try_likely_ok!(write(output, " "));
366        // Safety: All month names are at least 3 bytes long.
367        bytes += try_likely_ok!(write(output, unsafe {
368            MONTH_NAMES[u8::from(value.month(state)).extend::<usize>() - 1].get_unchecked(..3)
369        }));
370        bytes += try_likely_ok!(write(output, " "));
371        // Safety: Years with five or more digits were rejected above. Likewise with negative years.
372        bytes += try_likely_ok!(format_four_digits_pad_zero(output, unsafe {
373            ru16::new_unchecked(value.calendar_year(state).get().cast_unsigned().truncate())
374        }));
375        bytes += try_likely_ok!(write(output, " "));
376        bytes += try_likely_ok!(format_two_digits(
377            output,
378            value.hour(state).expand(),
379            Padding::Zero
380        ));
381        bytes += try_likely_ok!(write(output, ":"));
382        bytes += try_likely_ok!(format_two_digits(
383            output,
384            value.minute(state).expand(),
385            Padding::Zero
386        ));
387        bytes += try_likely_ok!(write(output, ":"));
388        bytes += try_likely_ok!(format_two_digits(
389            output,
390            value.second(state).expand(),
391            Padding::Zero
392        ));
393        bytes += try_likely_ok!(write(output, " "));
394        bytes += try_likely_ok!(write_if_else(
395            output,
396            value.offset_is_negative(state),
397            "-",
398            "+"
399        ));
400        bytes += try_likely_ok!(format_two_digits(
401            output,
402            // Safety: `OffsetHours` is guaranteed to be in the range `-25..=25`, so the absolute
403            // value is guaranteed to be in the range `0..=25`.
404            unsafe { ru8::new_unchecked(value.offset_hour(state).get().unsigned_abs()) },
405            Padding::Zero,
406        ));
407        bytes += try_likely_ok!(format_two_digits(
408            output,
409            // Safety: `OffsetMinutes` is guaranteed to be in the range `-59..=59`, so the absolute
410            // value is guaranteed to be in the range `0..=59`.
411            unsafe { ru8::new_unchecked(value.offset_minute(state).get().unsigned_abs()) },
412            Padding::Zero,
413        ));
414
415        Ok(bytes)
416    }
417}
418
419#[expect(
420    private_bounds,
421    private_interfaces,
422    reason = "irrelevant due to being a sealed trait"
423)]
424impl sealed::Sealed for Rfc3339 {
425    fn format_into<V>(
426        &self,
427        output: &mut (impl io::Write + ?Sized),
428        value: &V,
429        state: &mut V::State,
430    ) -> Result<usize, error::Format>
431    where
432        V: ComponentProvider,
433    {
434        const {
435            assert!(
436                V::SUPPLIES_DATE && V::SUPPLIES_TIME && V::SUPPLIES_OFFSET,
437                "Rfc3339 requires date, time, and offset components, but not all can be provided \
438                 by this type"
439            );
440        }
441
442        let offset_hour = value.offset_hour(state);
443        let mut bytes = 0;
444
445        if !(0..10_000).contains(&value.calendar_year(state).get()) {
446            crate::hint::cold_path();
447            return Err(error::Format::InvalidComponent("year"));
448        }
449        if offset_hour.get().unsigned_abs() > 23 {
450            crate::hint::cold_path();
451            return Err(error::Format::InvalidComponent("offset_hour"));
452        }
453        if value.offset_second(state).get() != 0 {
454            crate::hint::cold_path();
455            return Err(error::Format::InvalidComponent("offset_second"));
456        }
457
458        // Safety: Years outside this range were rejected above.
459        bytes += try_likely_ok!(format_four_digits_pad_zero(output, unsafe {
460            ru16::new_unchecked(value.calendar_year(state).get().cast_unsigned().truncate())
461        }));
462        bytes += try_likely_ok!(write(output, "-"));
463        bytes += try_likely_ok!(format_two_digits(
464            output,
465            // Safety: `month` is guaranteed to be in the range `1..=12`.
466            unsafe { ru8::new_unchecked(u8::from(value.month(state))) },
467            Padding::Zero,
468        ));
469        bytes += try_likely_ok!(write(output, "-"));
470        bytes += try_likely_ok!(format_two_digits(
471            output,
472            value.day(state).expand(),
473            Padding::Zero
474        ));
475        bytes += try_likely_ok!(write(output, "T"));
476        bytes += try_likely_ok!(format_two_digits(
477            output,
478            value.hour(state).expand(),
479            Padding::Zero
480        ));
481        bytes += try_likely_ok!(write(output, ":"));
482        bytes += try_likely_ok!(format_two_digits(
483            output,
484            value.minute(state).expand(),
485            Padding::Zero
486        ));
487        bytes += try_likely_ok!(write(output, ":"));
488        bytes += try_likely_ok!(format_two_digits(
489            output,
490            value.second(state).expand(),
491            Padding::Zero
492        ));
493
494        let nanos = value.nanosecond(state);
495        if nanos.get() != 0 {
496            bytes += try_likely_ok!(write(output, "."));
497            try_likely_ok!(write(
498                output,
499                &num_fmt::truncated_subsecond_from_nanos(nanos)
500            ));
501        }
502
503        if value.offset_is_utc(state) {
504            bytes += try_likely_ok!(write(output, "Z"));
505            return Ok(bytes);
506        }
507
508        bytes += try_likely_ok!(write_if_else(
509            output,
510            value.offset_is_negative(state),
511            "-",
512            "+"
513        ));
514        bytes += try_likely_ok!(format_two_digits(
515            output,
516            // Safety: `OffsetHours` is guaranteed to be in the range `-23..=23`, so the absolute
517            // value is guaranteed to be in the range `0..=23`.
518            unsafe { ru8::new_unchecked(offset_hour.get().unsigned_abs()) },
519            Padding::Zero,
520        ));
521        bytes += try_likely_ok!(write(output, ":"));
522        bytes += try_likely_ok!(format_two_digits(
523            output,
524            // Safety: `OffsetMinutes` is guaranteed to be in the range `-59..=59`, so the absolute
525            // value is guaranteed to be in the range `0..=59`.
526            unsafe { ru8::new_unchecked(value.offset_minute(state).get().unsigned_abs()) },
527            Padding::Zero,
528        ));
529
530        Ok(bytes)
531    }
532}
533
534impl<const CONFIG: EncodedConfig> sealed::Sealed for Iso8601<CONFIG> {
535    #[expect(
536        private_bounds,
537        private_interfaces,
538        reason = "irrelevant due to being a sealed trait"
539    )]
540    #[inline]
541    fn format_into<V>(
542        &self,
543        output: &mut (impl io::Write + ?Sized),
544        value: &V,
545        state: &mut V::State,
546    ) -> Result<usize, error::Format>
547    where
548        V: ComponentProvider,
549    {
550        let mut bytes = 0;
551
552        const {
553            assert!(
554                !Self::FORMAT_DATE || V::SUPPLIES_DATE,
555                "this Iso8601 configuration formats date components, but this type cannot provide \
556                 them"
557            );
558            assert!(
559                !Self::FORMAT_TIME || V::SUPPLIES_TIME,
560                "this Iso8601 configuration formats time components, but this type cannot provide \
561                 them"
562            );
563            assert!(
564                !Self::FORMAT_OFFSET || V::SUPPLIES_OFFSET,
565                "this Iso8601 configuration formats offset components, but this type cannot \
566                 provide them"
567            );
568            assert!(
569                Self::FORMAT_DATE || Self::FORMAT_TIME || Self::FORMAT_OFFSET,
570                "this Iso8601 configuration does not format any components"
571            );
572        }
573
574        if Self::FORMAT_DATE {
575            bytes += try_likely_ok!(iso8601::format_date::<_, CONFIG>(output, value, state));
576        }
577        if Self::FORMAT_TIME {
578            bytes += try_likely_ok!(iso8601::format_time::<_, CONFIG>(output, value, state));
579        }
580        if Self::FORMAT_OFFSET {
581            bytes += try_likely_ok!(iso8601::format_offset::<_, CONFIG>(output, value, state));
582        }
583
584        Ok(bytes)
585    }
586}