Skip to main content

time/
format.rs

1//! Formatting of [`DateTime`] values.
2
3use core::fmt;
4
5use crate::{datetime::DateTime, timezone::TimeZone};
6
7/// Stack buffer size for the allocation-free fast path used by the `Display`
8/// implementations.
9///
10/// The longest well-formed value this crate can produce is about 74 bytes:
11/// a six-byte out-of-range year, the date and time, nine fractional digits,
12/// a seconds-bearing offset and a 32-byte IANA annotation (`[` + name + `]`).
13/// 96 leaves headroom while staying small; only a custom zone name longer
14/// than this falls back to the `fmt`-based path.
15const MAX_FORMATTED_LENGTH: usize = 96;
16
17/// A well-known datetime representation.
18///
19/// # Representability
20///
21/// [`DateTime::format`] is infallible and best effort: it can emit a
22/// non-conforming string for out-of-domain values (for example `-0001` for a
23/// negative year, or `+00:19:32` for a historical offset with a seconds
24/// component). Use [`DateTime::try_format`] or [`DateTime::format_to`] when
25/// the value must be strictly valid for the requested format; both reject a
26/// year outside `0000..=9999` (except for `TimeOnly`) and, for `Rfc3339`,
27/// `Rfc9557`, `Iso8601` and `Rfc2822`, an offset with a seconds component.
28/// `TimeOnly` is always valid.
29#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
30#[non_exhaustive]
31pub enum Format {
32    /// RFC 3339 / ISO 8601, for example `2006-01-02T15:04:05Z` or
33    /// `2006-01-02T15:04:05+07:00`.
34    Rfc3339,
35    /// RFC 3339 plus an RFC 9557 time zone annotation, for example
36    /// `2006-01-02T15:04:05-05:00[America/New_York]`.
37    Rfc9557,
38    /// RFC 3339, except that a zero offset is written as `+00:00` rather than
39    /// `Z`.
40    Iso8601,
41    /// RFC 2822 (email) date, for example
42    /// `Mon, 02 Jan 2006 15:04:05 +0000`.
43    Rfc2822,
44    /// HTTP `IMF-fixdate`, for example `Mon, 02 Jan 2006 15:04:05 GMT`.
45    ///
46    /// The value is always converted to UTC.
47    HttpDate,
48    /// Just the date, for example `2006-01-02`.
49    DateOnly,
50    /// Just the time, for example `15:04:05` (with fractional seconds when
51    /// they are non-zero).
52    TimeOnly,
53}
54
55/// A [`Display`](core::fmt::Display) adapter returned by [`DateTime::format`].
56pub 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    /// Writes the formatted value into `buffer` without allocating, returning
70    /// the number of bytes written.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`ErrorKind::BufferTooSmall`](crate::ErrorKind::BufferTooSmall)
75    /// if `buffer` cannot hold the result.
76    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
82/// A fixed-buffer writer used by the allocation-free rendering fast path.
83/// Every method returns `false` when the buffer is full.
84struct 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    /// Writes `value` as exactly `width` zero-padded decimal digits.
111    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    /// Writes a year exactly as [`write_year`] does.
132    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    /// Writes a UTC offset as `+HH:MM` or `+HH:MM:SS`.
143    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    /// Writes a UTC offset as `+HHMM` (the RFC 2822 form).
155    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    /// Writes fractional seconds exactly as [`write_fraction`] does.
162    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
263/// Renders `dt` in `format` into `out`, returning the number of bytes written,
264/// or `None` when `out` is too small.
265///
266/// This is the allocation-free fast path. It can only fail for a buffer that is
267/// too small (including the internal stack buffer used by `Display` when a time
268/// zone name is unusually long); callers fall back to the `fmt`-based
269/// [`write_format`] in that case.
270pub(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
313/// Writes a UTC offset as `+HH:MM` or `+HH:MM:SS`.
314pub(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
327/// Writes a UTC offset as `+HHMM` (the RFC 2822 form).
328fn 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    // Fallback for an unusually long time zone name.
365    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}