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 num_conv::prelude::*;
9
10use crate::format_description::well_known::iso8601::EncodedConfig;
11use crate::format_description::well_known::{Iso8601, Rfc2822, Rfc3339};
12use crate::format_description::{BorrowedFormatItem, OwnedFormatItem};
13use crate::formatting::{
14    format_component, format_number_pad_zero, iso8601, write, MONTH_NAMES, WEEKDAY_NAMES,
15};
16use crate::{error, Date, Time, UtcOffset};
17
18/// A type that describes a format.
19///
20/// Implementors of [`Formattable`] are [format descriptions](crate::format_description).
21///
22/// [`Date::format`] and [`Time::format`] each use a format description to generate
23/// a String from their data. See the respective methods for usage examples.
24#[cfg_attr(docsrs, doc(notable_trait))]
25pub trait Formattable: sealed::Sealed {}
26impl Formattable for BorrowedFormatItem<'_> {}
27impl Formattable for [BorrowedFormatItem<'_>] {}
28impl Formattable for OwnedFormatItem {}
29impl Formattable for [OwnedFormatItem] {}
30impl Formattable for Rfc3339 {}
31impl Formattable for Rfc2822 {}
32impl<const CONFIG: EncodedConfig> Formattable for Iso8601<CONFIG> {}
33impl<T: Deref> Formattable for T where T::Target: Formattable {}
34
35/// Seal the trait to prevent downstream users from implementing it.
36mod sealed {
37    #[allow(clippy::wildcard_imports)]
38    use super::*;
39
40    /// Format the item using a format description, the intended output, and the various components.
41    pub trait Sealed {
42        /// Format the item into the provided output, returning the number of bytes written.
43        fn format_into(
44            &self,
45            output: &mut (impl io::Write + ?Sized),
46            date: Option<Date>,
47            time: Option<Time>,
48            offset: Option<UtcOffset>,
49        ) -> Result<usize, error::Format>;
50
51        /// Format the item directly to a `String`.
52        fn format(
53            &self,
54            date: Option<Date>,
55            time: Option<Time>,
56            offset: Option<UtcOffset>,
57        ) -> Result<String, error::Format> {
58            let mut buf = Vec::new();
59            self.format_into(&mut buf, date, time, offset)?;
60            Ok(String::from_utf8_lossy(&buf).into_owned())
61        }
62    }
63}
64
65// region: custom formats
66impl sealed::Sealed for BorrowedFormatItem<'_> {
67    fn format_into(
68        &self,
69        output: &mut (impl io::Write + ?Sized),
70        date: Option<Date>,
71        time: Option<Time>,
72        offset: Option<UtcOffset>,
73    ) -> Result<usize, error::Format> {
74        Ok(match *self {
75            Self::Literal(literal) => write(output, literal)?,
76            Self::Component(component) => format_component(output, component, date, time, offset)?,
77            Self::Compound(items) => items.format_into(output, date, time, offset)?,
78            Self::Optional(item) => item.format_into(output, date, time, offset)?,
79            Self::First(items) => match items {
80                [] => 0,
81                [item, ..] => item.format_into(output, date, time, offset)?,
82            },
83        })
84    }
85}
86
87impl sealed::Sealed for [BorrowedFormatItem<'_>] {
88    fn format_into(
89        &self,
90        output: &mut (impl io::Write + ?Sized),
91        date: Option<Date>,
92        time: Option<Time>,
93        offset: Option<UtcOffset>,
94    ) -> Result<usize, error::Format> {
95        let mut bytes = 0;
96        for item in self.iter() {
97            bytes += item.format_into(output, date, time, offset)?;
98        }
99        Ok(bytes)
100    }
101}
102
103impl sealed::Sealed for OwnedFormatItem {
104    fn format_into(
105        &self,
106        output: &mut (impl io::Write + ?Sized),
107        date: Option<Date>,
108        time: Option<Time>,
109        offset: Option<UtcOffset>,
110    ) -> Result<usize, error::Format> {
111        match self {
112            Self::Literal(literal) => Ok(write(output, literal)?),
113            Self::Component(component) => format_component(output, *component, date, time, offset),
114            Self::Compound(items) => items.format_into(output, date, time, offset),
115            Self::Optional(item) => item.format_into(output, date, time, offset),
116            Self::First(items) => match &**items {
117                [] => Ok(0),
118                [item, ..] => item.format_into(output, date, time, offset),
119            },
120        }
121    }
122}
123
124impl sealed::Sealed for [OwnedFormatItem] {
125    fn format_into(
126        &self,
127        output: &mut (impl io::Write + ?Sized),
128        date: Option<Date>,
129        time: Option<Time>,
130        offset: Option<UtcOffset>,
131    ) -> Result<usize, error::Format> {
132        let mut bytes = 0;
133        for item in self.iter() {
134            bytes += item.format_into(output, date, time, offset)?;
135        }
136        Ok(bytes)
137    }
138}
139
140impl<T: Deref> sealed::Sealed for T
141where
142    T::Target: sealed::Sealed,
143{
144    fn format_into(
145        &self,
146        output: &mut (impl io::Write + ?Sized),
147        date: Option<Date>,
148        time: Option<Time>,
149        offset: Option<UtcOffset>,
150    ) -> Result<usize, error::Format> {
151        self.deref().format_into(output, date, time, offset)
152    }
153}
154// endregion custom formats
155
156// region: well-known formats
157impl sealed::Sealed for Rfc2822 {
158    fn format_into(
159        &self,
160        output: &mut (impl io::Write + ?Sized),
161        date: Option<Date>,
162        time: Option<Time>,
163        offset: Option<UtcOffset>,
164    ) -> Result<usize, error::Format> {
165        let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
166        let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
167        let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;
168
169        let mut bytes = 0;
170
171        let (year, month, day) = date.to_calendar_date();
172
173        if year < 1900 {
174            return Err(error::Format::InvalidComponent("year"));
175        }
176        if offset.seconds_past_minute() != 0 {
177            return Err(error::Format::InvalidComponent("offset_second"));
178        }
179
180        bytes += write(
181            output,
182            &WEEKDAY_NAMES[date.weekday().number_days_from_monday().extend::<usize>()][..3],
183        )?;
184        bytes += write(output, b", ")?;
185        bytes += format_number_pad_zero::<2>(output, day)?;
186        bytes += write(output, b" ")?;
187        bytes += write(
188            output,
189            &MONTH_NAMES[u8::from(month).extend::<usize>() - 1][..3],
190        )?;
191        bytes += write(output, b" ")?;
192        bytes += format_number_pad_zero::<4>(output, year.cast_unsigned())?;
193        bytes += write(output, b" ")?;
194        bytes += format_number_pad_zero::<2>(output, time.hour())?;
195        bytes += write(output, b":")?;
196        bytes += format_number_pad_zero::<2>(output, time.minute())?;
197        bytes += write(output, b":")?;
198        bytes += format_number_pad_zero::<2>(output, time.second())?;
199        bytes += write(output, b" ")?;
200        bytes += write(output, if offset.is_negative() { b"-" } else { b"+" })?;
201        bytes += format_number_pad_zero::<2>(output, offset.whole_hours().unsigned_abs())?;
202        bytes += format_number_pad_zero::<2>(output, offset.minutes_past_hour().unsigned_abs())?;
203
204        Ok(bytes)
205    }
206}
207
208impl sealed::Sealed for Rfc3339 {
209    fn format_into(
210        &self,
211        output: &mut (impl io::Write + ?Sized),
212        date: Option<Date>,
213        time: Option<Time>,
214        offset: Option<UtcOffset>,
215    ) -> Result<usize, error::Format> {
216        let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
217        let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
218        let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;
219
220        let mut bytes = 0;
221
222        let year = date.year();
223
224        if !(0..10_000).contains(&year) {
225            return Err(error::Format::InvalidComponent("year"));
226        }
227        if offset.whole_hours().unsigned_abs() > 23 {
228            return Err(error::Format::InvalidComponent("offset_hour"));
229        }
230        if offset.seconds_past_minute() != 0 {
231            return Err(error::Format::InvalidComponent("offset_second"));
232        }
233
234        bytes += format_number_pad_zero::<4>(output, year.cast_unsigned())?;
235        bytes += write(output, b"-")?;
236        bytes += format_number_pad_zero::<2>(output, u8::from(date.month()))?;
237        bytes += write(output, b"-")?;
238        bytes += format_number_pad_zero::<2>(output, date.day())?;
239        bytes += write(output, b"T")?;
240        bytes += format_number_pad_zero::<2>(output, time.hour())?;
241        bytes += write(output, b":")?;
242        bytes += format_number_pad_zero::<2>(output, time.minute())?;
243        bytes += write(output, b":")?;
244        bytes += format_number_pad_zero::<2>(output, time.second())?;
245
246        if time.nanosecond() != 0 {
247            let nanos = time.nanosecond();
248            bytes += write(output, b".")?;
249            bytes += if nanos % 10 != 0 {
250                format_number_pad_zero::<9>(output, nanos)
251            } else if (nanos / 10) % 10 != 0 {
252                format_number_pad_zero::<8>(output, nanos / 10)
253            } else if (nanos / 100) % 10 != 0 {
254                format_number_pad_zero::<7>(output, nanos / 100)
255            } else if (nanos / 1_000) % 10 != 0 {
256                format_number_pad_zero::<6>(output, nanos / 1_000)
257            } else if (nanos / 10_000) % 10 != 0 {
258                format_number_pad_zero::<5>(output, nanos / 10_000)
259            } else if (nanos / 100_000) % 10 != 0 {
260                format_number_pad_zero::<4>(output, nanos / 100_000)
261            } else if (nanos / 1_000_000) % 10 != 0 {
262                format_number_pad_zero::<3>(output, nanos / 1_000_000)
263            } else if (nanos / 10_000_000) % 10 != 0 {
264                format_number_pad_zero::<2>(output, nanos / 10_000_000)
265            } else {
266                format_number_pad_zero::<1>(output, nanos / 100_000_000)
267            }?;
268        }
269
270        if offset == UtcOffset::UTC {
271            bytes += write(output, b"Z")?;
272            return Ok(bytes);
273        }
274
275        bytes += write(output, if offset.is_negative() { b"-" } else { b"+" })?;
276        bytes += format_number_pad_zero::<2>(output, offset.whole_hours().unsigned_abs())?;
277        bytes += write(output, b":")?;
278        bytes += format_number_pad_zero::<2>(output, offset.minutes_past_hour().unsigned_abs())?;
279
280        Ok(bytes)
281    }
282}
283
284impl<const CONFIG: EncodedConfig> sealed::Sealed for Iso8601<CONFIG> {
285    fn format_into(
286        &self,
287        output: &mut (impl io::Write + ?Sized),
288        date: Option<Date>,
289        time: Option<Time>,
290        offset: Option<UtcOffset>,
291    ) -> Result<usize, error::Format> {
292        let mut bytes = 0;
293
294        if Self::FORMAT_DATE {
295            let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
296            bytes += iso8601::format_date::<CONFIG>(output, date)?;
297        }
298        if Self::FORMAT_TIME {
299            let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
300            bytes += iso8601::format_time::<CONFIG>(output, time)?;
301        }
302        if Self::FORMAT_OFFSET {
303            let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;
304            bytes += iso8601::format_offset::<CONFIG>(output, offset)?;
305        }
306
307        if bytes == 0 {
308            // The only reason there would be no bytes written is if the format was only for
309            // parsing.
310            panic!("attempted to format a parsing-only format description");
311        }
312
313        Ok(bytes)
314    }
315}
316// endregion well-known formats