1use core::fmt;
4
5use crate::{datetime::DateTime, timezone::TimeZone};
6
7const MAX_FORMATTED_LENGTH: usize = 96;
16
17#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
30#[non_exhaustive]
31pub enum Format {
32 Rfc3339,
35 Rfc9557,
38 Iso8601,
41 Rfc2822,
44 HttpDate,
48 DateOnly,
50 TimeOnly,
53}
54
55pub struct Formatted<'a> {
57 dt: &'a DateTime,
58 format: Format,
59}
60
61impl<'a> Formatted<'a> {
62 pub(crate) fn new(dt: &'a DateTime, format: Format) -> Formatted<'a> {
63 Formatted {
64 dt,
65 format,
66 }
67 }
68
69 pub fn write_to(&self, buffer: &mut [u8]) -> Result<usize, crate::Error> {
77 format_into(buffer, self.dt, self.format)
78 .ok_or_else(|| crate::error::buffer_too_small("buffer is too small for the formatted value"))
79 }
80}
81
82struct Buf<'a> {
85 bytes: &'a mut [u8],
86 len: usize,
87}
88
89impl Buf<'_> {
90 fn byte(&mut self, byte: u8) -> bool {
91 if self.len < self.bytes.len() {
92 self.bytes[self.len] = byte;
93 self.len += 1;
94 true
95 } else {
96 false
97 }
98 }
99
100 fn bytes(&mut self, bytes: &[u8]) -> bool {
101 if self.len + bytes.len() <= self.bytes.len() {
102 self.bytes[self.len..self.len + bytes.len()].copy_from_slice(bytes);
103 self.len += bytes.len();
104 true
105 } else {
106 false
107 }
108 }
109
110 fn pad(&mut self, mut value: u32, width: usize) -> bool {
112 let mut scratch = [b'0'; 10];
113 let mut index = scratch.len();
114 loop {
115 index -= 1;
116 scratch[index] = b'0' + (value % 10) as u8;
117 value /= 10;
118 if value == 0 {
119 break;
120 }
121 }
122 let digits = scratch.len() - index;
123 if digits > width {
124 return false;
125 }
126 let padding = [b'0'; 10];
127 let pad = width - digits;
128 self.bytes(&padding[..pad]) && self.bytes(&scratch[index..])
129 }
130
131 fn year(&mut self, year: i32) -> bool {
133 if (0..=9999).contains(&year) {
134 self.pad(year as u32, 4)
135 } else if year < 0 {
136 self.byte(b'-') && self.pad((-i64::from(year)) as u32, 4)
137 } else {
138 self.byte(b'+') && self.pad(year as u32, 1)
139 }
140 }
141
142 fn offset(&mut self, offset: i32) -> bool {
144 let sign = if offset < 0 { b'-' } else { b'+' };
145 let abs = offset.unsigned_abs();
146 let seconds = abs % 60;
147 self.byte(sign)
148 && self.pad(abs / 3600, 2)
149 && self.byte(b':')
150 && self.pad((abs % 3600) / 60, 2)
151 && (seconds == 0 || (self.byte(b':') && self.pad(seconds, 2)))
152 }
153
154 fn offset_compact(&mut self, offset: i32) -> bool {
156 let sign = if offset < 0 { b'-' } else { b'+' };
157 let abs = offset.unsigned_abs();
158 self.byte(sign) && self.pad(abs / 3600, 2) && self.pad((abs % 3600) / 60, 2)
159 }
160
161 fn fraction(&mut self, nanos: u32) -> bool {
163 if nanos == 0 {
164 true
165 } else if nanos.is_multiple_of(1_000_000) {
166 self.byte(b'.') && self.pad(nanos / 1_000_000, 3)
167 } else if nanos.is_multiple_of(1_000) {
168 self.byte(b'.') && self.pad(nanos / 1_000, 6)
169 } else {
170 self.byte(b'.') && self.pad(nanos, 9)
171 }
172 }
173}
174
175fn month_short_name(month: u8) -> &'static str {
176 month_short(month)
177}
178
179fn format_iso(buf: &mut Buf<'_>, dt: &DateTime, z_suffix: bool, zone: bool) -> bool {
180 let (year, month, day, hour, minute, second, nanos) = dt.parts();
181 let base = buf.year(year)
182 && buf.byte(b'-')
183 && buf.pad(u32::from(month), 2)
184 && buf.byte(b'-')
185 && buf.pad(u32::from(day), 2)
186 && buf.byte(b'T')
187 && buf.pad(u32::from(hour), 2)
188 && buf.byte(b':')
189 && buf.pad(u32::from(minute), 2)
190 && buf.byte(b':')
191 && buf.pad(u32::from(second), 2)
192 && buf.fraction(nanos)
193 && {
194 let offset = dt.offset();
195 if z_suffix && offset == 0 {
196 buf.byte(b'Z')
197 } else {
198 buf.offset(offset)
199 }
200 };
201 if !base || !zone {
202 return base;
203 }
204 match dt.timezone_name() {
205 Some(name) => buf.byte(b'[') && buf.bytes(name.as_bytes()) && buf.byte(b']'),
206 None => true,
207 }
208}
209
210fn format_rfc2822(buf: &mut Buf<'_>, dt: &DateTime) -> bool {
211 let (year, month, day, hour, minute, second, _) = dt.parts();
212 buf.bytes(dt.weekday().short_name().as_bytes())
213 && buf.bytes(b", ")
214 && buf.pad(u32::from(day), 2)
215 && buf.byte(b' ')
216 && buf.bytes(month_short_name(month).as_bytes())
217 && buf.byte(b' ')
218 && buf.year(year)
219 && buf.byte(b' ')
220 && buf.pad(u32::from(hour), 2)
221 && buf.byte(b':')
222 && buf.pad(u32::from(minute), 2)
223 && buf.byte(b':')
224 && buf.pad(u32::from(second), 2)
225 && buf.byte(b' ')
226 && buf.offset_compact(dt.offset())
227}
228
229fn format_http(buf: &mut Buf<'_>, dt: &DateTime) -> bool {
230 let utc = dt.in_timezone(TimeZone::UTC);
231 let (year, month, day, hour, minute, second, _) = utc.parts();
232 buf.bytes(utc.weekday().short_name().as_bytes())
233 && buf.bytes(b", ")
234 && buf.pad(u32::from(day), 2)
235 && buf.byte(b' ')
236 && buf.bytes(month_short_name(month).as_bytes())
237 && buf.byte(b' ')
238 && buf.year(year)
239 && buf.byte(b' ')
240 && buf.pad(u32::from(hour), 2)
241 && buf.byte(b':')
242 && buf.pad(u32::from(minute), 2)
243 && buf.byte(b':')
244 && buf.pad(u32::from(second), 2)
245 && buf.bytes(b" GMT")
246}
247
248fn format_date(buf: &mut Buf<'_>, dt: &DateTime) -> bool {
249 let (year, month, day, ..) = dt.parts();
250 buf.year(year) && buf.byte(b'-') && buf.pad(u32::from(month), 2) && buf.byte(b'-') && buf.pad(u32::from(day), 2)
251}
252
253fn format_time(buf: &mut Buf<'_>, dt: &DateTime) -> bool {
254 let (_, _, _, hour, minute, second, nanos) = dt.parts();
255 buf.pad(u32::from(hour), 2)
256 && buf.byte(b':')
257 && buf.pad(u32::from(minute), 2)
258 && buf.byte(b':')
259 && buf.pad(u32::from(second), 2)
260 && buf.fraction(nanos)
261}
262
263pub(crate) fn format_into(out: &mut [u8], dt: &DateTime, format: Format) -> Option<usize> {
271 let mut buf = Buf {
272 bytes: out,
273 len: 0,
274 };
275 let ok = match format {
276 Format::Rfc3339 => format_iso(&mut buf, dt, true, false),
277 Format::Rfc9557 => format_iso(&mut buf, dt, true, true),
278 Format::Iso8601 => format_iso(&mut buf, dt, false, false),
279 Format::Rfc2822 => format_rfc2822(&mut buf, dt),
280 Format::HttpDate => format_http(&mut buf, dt),
281 Format::DateOnly => format_date(&mut buf, dt),
282 Format::TimeOnly => format_time(&mut buf, dt),
283 };
284 if ok { Some(buf.len) } else { None }
285}
286
287impl fmt::Display for Formatted<'_> {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 let mut buffer = [0u8; MAX_FORMATTED_LENGTH];
290 match format_into(&mut buffer, self.dt, self.format) {
291 Some(len) => f.write_str(core::str::from_utf8(&buffer[..len]).map_err(|_| fmt::Error)?),
292 None => write_format(f, self.dt, self.format),
293 }
294 }
295}
296
297impl fmt::Debug for Formatted<'_> {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 fmt::Display::fmt(self, f)
300 }
301}
302
303pub(crate) fn write_year(f: &mut fmt::Formatter<'_>, year: i32) -> fmt::Result {
304 if (0..=9999).contains(&year) {
305 write!(f, "{year:04}")
306 } else if year < 0 {
307 write!(f, "-{:04}", -i64::from(year))
308 } else {
309 write!(f, "+{year}")
310 }
311}
312
313pub(crate) fn write_offset(f: &mut fmt::Formatter<'_>, offset: i32) -> fmt::Result {
315 let sign = if offset < 0 { '-' } else { '+' };
316 let abs = offset.unsigned_abs();
317 let hours = abs / 3600;
318 let minutes = (abs % 3600) / 60;
319 let seconds = abs % 60;
320 if seconds == 0 {
321 write!(f, "{sign}{hours:02}:{minutes:02}")
322 } else {
323 write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
324 }
325}
326
327fn write_offset_compact(f: &mut fmt::Formatter<'_>, offset: i32) -> fmt::Result {
329 let sign = if offset < 0 { '-' } else { '+' };
330 let abs = offset.unsigned_abs();
331 let hours = abs / 3600;
332 let minutes = (abs % 3600) / 60;
333 write!(f, "{sign}{hours:02}{minutes:02}")
334}
335
336fn write_fraction(f: &mut fmt::Formatter<'_>, nanos: u32) -> fmt::Result {
337 if nanos == 0 {
338 Ok(())
339 } else if nanos.is_multiple_of(1_000_000) {
340 write!(f, ".{:03}", nanos / 1_000_000)
341 } else if nanos.is_multiple_of(1_000) {
342 write!(f, ".{:06}", nanos / 1_000)
343 } else {
344 write!(f, ".{nanos:09}")
345 }
346}
347
348fn month_short(month: u8) -> &'static str {
349 const MONTHS: [&str; 12] = [
350 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
351 ];
352 (month as usize)
353 .checked_sub(1)
354 .and_then(|index| MONTHS.get(index))
355 .copied()
356 .unwrap_or("???")
357}
358
359pub(crate) fn write_datetime(f: &mut fmt::Formatter<'_>, dt: &DateTime) -> fmt::Result {
360 let mut buffer = [0u8; MAX_FORMATTED_LENGTH];
361 if let Some(len) = format_into(&mut buffer, dt, Format::Rfc9557) {
362 return f.write_str(core::str::from_utf8(&buffer[..len]).map_err(|_| fmt::Error)?);
363 }
364 let (year, month, day, hour, minute, second, nanos) = dt.parts();
366 write_year(f, year)?;
367 write!(f, "-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}")?;
368 write_fraction(f, nanos)?;
369 if dt.offset() == 0 {
370 f.write_str("Z")?;
371 } else {
372 write_offset(f, dt.offset())?;
373 }
374 if let Some(name) = dt.timezone_name() {
375 write!(f, "[{name}]")?;
376 }
377 Ok(())
378}
379
380fn write_format(f: &mut fmt::Formatter<'_>, dt: &DateTime, format: Format) -> fmt::Result {
381 match format {
382 Format::Rfc3339 => write_iso(f, dt, true, false),
383 Format::Rfc9557 => write_iso(f, dt, true, true),
384 Format::Iso8601 => write_iso(f, dt, false, false),
385 Format::Rfc2822 => write_rfc2822(f, dt),
386 Format::HttpDate => write_http_date(f, dt),
387 Format::DateOnly => write_date(f, dt),
388 Format::TimeOnly => write_time(f, dt),
389 }
390}
391
392fn write_iso(f: &mut fmt::Formatter<'_>, dt: &DateTime, z_suffix: bool, zone: bool) -> fmt::Result {
393 let (year, month, day, hour, minute, second, nanos) = dt.parts();
394 write_year(f, year)?;
395 write!(f, "-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}")?;
396 write_fraction(f, nanos)?;
397 let offset = dt.offset();
398 if z_suffix && offset == 0 {
399 f.write_str("Z")?;
400 } else {
401 write_offset(f, offset)?;
402 }
403 if zone {
404 if let Some(name) = dt.timezone_name() {
405 write!(f, "[{name}]")?;
406 }
407 }
408 Ok(())
409}
410
411fn write_date(f: &mut fmt::Formatter<'_>, dt: &DateTime) -> fmt::Result {
412 let (year, month, day, ..) = dt.parts();
413 write_year(f, year)?;
414 write!(f, "-{month:02}-{day:02}")
415}
416
417fn write_time(f: &mut fmt::Formatter<'_>, dt: &DateTime) -> fmt::Result {
418 let (_, _, _, hour, minute, second, nanos) = dt.parts();
419 write!(f, "{hour:02}:{minute:02}:{second:02}")?;
420 write_fraction(f, nanos)
421}
422
423fn write_rfc2822(f: &mut fmt::Formatter<'_>, dt: &DateTime) -> fmt::Result {
424 let (year, month, day, hour, minute, second, _) = dt.parts();
425 write!(f, "{}, {day:02} {} ", dt.weekday().short_name(), month_short(month))?;
426 write_year(f, year)?;
427 write!(f, " {hour:02}:{minute:02}:{second:02} ")?;
428 write_offset_compact(f, dt.offset())
429}
430
431fn write_http_date(f: &mut fmt::Formatter<'_>, dt: &DateTime) -> fmt::Result {
432 let utc = dt.in_timezone(TimeZone::UTC);
433 let (year, month, day, hour, minute, second, _) = utc.parts();
434 write!(f, "{}, {day:02} {} ", utc.weekday().short_name(), month_short(month))?;
435 write_year(f, year)?;
436 write!(f, " {hour:02}:{minute:02}:{second:02} GMT")
437}