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