1 | //! A trait that can be used to format an item from its components. |
2 | |
3 | use alloc::string::String; |
4 | use alloc::vec::Vec; |
5 | use core::ops::Deref; |
6 | use std::io; |
7 | |
8 | use num_conv::prelude::*; |
9 | |
10 | use crate::format_description::well_known::iso8601::EncodedConfig; |
11 | use crate::format_description::well_known::{Iso8601, Rfc2822, Rfc3339}; |
12 | use crate::format_description::{BorrowedFormatItem, OwnedFormatItem}; |
13 | use crate::formatting::{ |
14 | format_component, format_number_pad_zero, iso8601, write, MONTH_NAMES, WEEKDAY_NAMES, |
15 | }; |
16 | use 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))] |
25 | pub trait Formattable: sealed::Sealed {} |
26 | impl Formattable for BorrowedFormatItem<'_> {} |
27 | impl Formattable for [BorrowedFormatItem<'_>] {} |
28 | impl Formattable for OwnedFormatItem {} |
29 | impl Formattable for [OwnedFormatItem] {} |
30 | impl Formattable for Rfc3339 {} |
31 | impl Formattable for Rfc2822 {} |
32 | impl<const CONFIG: EncodedConfig> Formattable for Iso8601<CONFIG> {} |
33 | impl<T: Deref> Formattable for T where T::Target: Formattable {} |
34 | |
35 | /// Seal the trait to prevent downstream users from implementing it. |
36 | mod 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, |
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 |
66 | impl sealed::Sealed for BorrowedFormatItem<'_> { |
67 | fn format_into( |
68 | &self, |
69 | output: &mut impl io::Write, |
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: &[u8]) => write(output, bytes:literal)?, |
76 | Self::Component(component: Component) => format_component(output, component, date, time, offset)?, |
77 | Self::Compound(items: &[BorrowedFormatItem<'_>]) => items.format_into(output, date, time, offset)?, |
78 | Self::Optional(item: &BorrowedFormatItem<'_>) => item.format_into(output, date, time, offset)?, |
79 | Self::First(items: &[BorrowedFormatItem<'_>]) => match items { |
80 | [] => 0, |
81 | [item: &BorrowedFormatItem<'_>, ..] => item.format_into(output, date, time, offset)?, |
82 | }, |
83 | }) |
84 | } |
85 | } |
86 | |
87 | impl sealed::Sealed for [BorrowedFormatItem<'_>] { |
88 | fn format_into( |
89 | &self, |
90 | output: &mut impl io::Write, |
91 | date: Option<Date>, |
92 | time: Option<Time>, |
93 | offset: Option<UtcOffset>, |
94 | ) -> Result<usize, error::Format> { |
95 | let mut bytes: usize = 0; |
96 | for item: &BorrowedFormatItem<'_> in self.iter() { |
97 | bytes += item.format_into(output, date, time, offset)?; |
98 | } |
99 | Ok(bytes) |
100 | } |
101 | } |
102 | |
103 | impl sealed::Sealed for OwnedFormatItem { |
104 | fn format_into( |
105 | &self, |
106 | output: &mut impl io::Write, |
107 | date: Option<Date>, |
108 | time: Option<Time>, |
109 | offset: Option<UtcOffset>, |
110 | ) -> Result<usize, error::Format> { |
111 | match self { |
112 | Self::Literal(literal: &Box<[u8]>) => Ok(write(output, bytes:literal)?), |
113 | Self::Component(component: &Component) => format_component(output, *component, date, time, offset), |
114 | Self::Compound(items: &Box<[OwnedFormatItem]>) => items.format_into(output, date, time, offset), |
115 | Self::Optional(item: &Box) => item.format_into(output, date, time, offset), |
116 | Self::First(items: &Box<[OwnedFormatItem]>) => match &**items { |
117 | [] => Ok(0), |
118 | [item: &OwnedFormatItem, ..] => item.format_into(output, date, time, offset), |
119 | }, |
120 | } |
121 | } |
122 | } |
123 | |
124 | impl sealed::Sealed for [OwnedFormatItem] { |
125 | fn format_into( |
126 | &self, |
127 | output: &mut impl io::Write, |
128 | date: Option<Date>, |
129 | time: Option<Time>, |
130 | offset: Option<UtcOffset>, |
131 | ) -> Result<usize, error::Format> { |
132 | let mut bytes: usize = 0; |
133 | for item: &OwnedFormatItem in self.iter() { |
134 | bytes += item.format_into(output, date, time, offset)?; |
135 | } |
136 | Ok(bytes) |
137 | } |
138 | } |
139 | |
140 | impl<T: Deref> sealed::Sealed for T |
141 | where |
142 | T::Target: sealed::Sealed, |
143 | { |
144 | fn format_into( |
145 | &self, |
146 | output: &mut impl io::Write, |
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 |
157 | impl sealed::Sealed for Rfc2822 { |
158 | fn format_into( |
159 | &self, |
160 | output: &mut impl io::Write, |
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 | |
208 | impl sealed::Sealed for Rfc3339 { |
209 | fn format_into( |
210 | &self, |
211 | output: &mut impl io::Write, |
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 | #[allow (clippy::if_not_else)] |
247 | if time.nanosecond() != 0 { |
248 | let nanos = time.nanosecond(); |
249 | bytes += write(output, b"." )?; |
250 | bytes += if nanos % 10 != 0 { |
251 | format_number_pad_zero::<9>(output, nanos) |
252 | } else if (nanos / 10) % 10 != 0 { |
253 | format_number_pad_zero::<8>(output, nanos / 10) |
254 | } else if (nanos / 100) % 10 != 0 { |
255 | format_number_pad_zero::<7>(output, nanos / 100) |
256 | } else if (nanos / 1_000) % 10 != 0 { |
257 | format_number_pad_zero::<6>(output, nanos / 1_000) |
258 | } else if (nanos / 10_000) % 10 != 0 { |
259 | format_number_pad_zero::<5>(output, nanos / 10_000) |
260 | } else if (nanos / 100_000) % 10 != 0 { |
261 | format_number_pad_zero::<4>(output, nanos / 100_000) |
262 | } else if (nanos / 1_000_000) % 10 != 0 { |
263 | format_number_pad_zero::<3>(output, nanos / 1_000_000) |
264 | } else if (nanos / 10_000_000) % 10 != 0 { |
265 | format_number_pad_zero::<2>(output, nanos / 10_000_000) |
266 | } else { |
267 | format_number_pad_zero::<1>(output, nanos / 100_000_000) |
268 | }?; |
269 | } |
270 | |
271 | if offset == UtcOffset::UTC { |
272 | bytes += write(output, b"Z" )?; |
273 | return Ok(bytes); |
274 | } |
275 | |
276 | bytes += write(output, if offset.is_negative() { b"-" } else { b"+" })?; |
277 | bytes += format_number_pad_zero::<2>(output, offset.whole_hours().unsigned_abs())?; |
278 | bytes += write(output, b":" )?; |
279 | bytes += format_number_pad_zero::<2>(output, offset.minutes_past_hour().unsigned_abs())?; |
280 | |
281 | Ok(bytes) |
282 | } |
283 | } |
284 | |
285 | impl<const CONFIG: EncodedConfig> sealed::Sealed for Iso8601<CONFIG> { |
286 | fn format_into( |
287 | &self, |
288 | output: &mut impl io::Write, |
289 | date: Option<Date>, |
290 | time: Option<Time>, |
291 | offset: Option<UtcOffset>, |
292 | ) -> Result<usize, error::Format> { |
293 | let mut bytes = 0; |
294 | |
295 | if Self::FORMAT_DATE { |
296 | let date = date.ok_or(error::Format::InsufficientTypeInformation)?; |
297 | bytes += iso8601::format_date::<CONFIG>(output, date)?; |
298 | } |
299 | if Self::FORMAT_TIME { |
300 | let time = time.ok_or(error::Format::InsufficientTypeInformation)?; |
301 | bytes += iso8601::format_time::<CONFIG>(output, time)?; |
302 | } |
303 | if Self::FORMAT_OFFSET { |
304 | let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?; |
305 | bytes += iso8601::format_offset::<CONFIG>(output, offset)?; |
306 | } |
307 | |
308 | if bytes == 0 { |
309 | // The only reason there would be no bytes written is if the format was only for |
310 | // parsing. |
311 | panic!("attempted to format a parsing-only format description" ); |
312 | } |
313 | |
314 | Ok(bytes) |
315 | } |
316 | } |
317 | // endregion well-known formats |
318 | |