time/formatting/
mod.rs

1//! Formatting for various types.
2
3pub(crate) mod formattable;
4mod iso8601;
5use core::num::NonZeroU8;
6use std::io;
7
8use num_conv::prelude::*;
9
10pub use self::formattable::Formattable;
11use crate::convert::*;
12use crate::ext::DigitCount;
13use crate::format_description::{modifier, Component};
14use crate::{error, Date, OffsetDateTime, Time, UtcOffset};
15
16#[allow(clippy::missing_docs_in_private_items)]
17const MONTH_NAMES: [&[u8]; 12] = [
18    b"January",
19    b"February",
20    b"March",
21    b"April",
22    b"May",
23    b"June",
24    b"July",
25    b"August",
26    b"September",
27    b"October",
28    b"November",
29    b"December",
30];
31
32#[allow(clippy::missing_docs_in_private_items)]
33const WEEKDAY_NAMES: [&[u8]; 7] = [
34    b"Monday",
35    b"Tuesday",
36    b"Wednesday",
37    b"Thursday",
38    b"Friday",
39    b"Saturday",
40    b"Sunday",
41];
42
43/// Write all bytes to the output, returning the number of bytes written.
44pub(crate) fn write(output: &mut impl io::Write, bytes: &[u8]) -> io::Result<usize> {
45    output.write_all(bytes)?;
46    Ok(bytes.len())
47}
48
49/// If `pred` is true, write all bytes to the output, returning the number of bytes written.
50pub(crate) fn write_if(output: &mut impl io::Write, pred: bool, bytes: &[u8]) -> io::Result<usize> {
51    if pred { write(output, bytes) } else { Ok(0) }
52}
53
54/// If `pred` is true, write `true_bytes` to the output. Otherwise, write `false_bytes`.
55pub(crate) fn write_if_else(
56    output: &mut impl io::Write,
57    pred: bool,
58    true_bytes: &[u8],
59    false_bytes: &[u8],
60) -> io::Result<usize> {
61    write(output, if pred { true_bytes } else { false_bytes })
62}
63
64/// Write the floating point number to the output, returning the number of bytes written.
65///
66/// This method accepts the number of digits before and after the decimal. The value will be padded
67/// with zeroes to the left if necessary.
68pub(crate) fn format_float(
69    output: &mut impl io::Write,
70    value: f64,
71    digits_before_decimal: u8,
72    digits_after_decimal: Option<NonZeroU8>,
73) -> io::Result<usize> {
74    match digits_after_decimal {
75        Some(digits_after_decimal) => {
76            let digits_after_decimal = digits_after_decimal.get().extend();
77            let width = digits_before_decimal.extend::<usize>() + 1 + digits_after_decimal;
78            write!(output, "{value:0>width$.digits_after_decimal$}")?;
79            Ok(width)
80        }
81        None => {
82            let value = value.trunc() as u64;
83            let width = digits_before_decimal.extend();
84            write!(output, "{value:0>width$}")?;
85            Ok(width)
86        }
87    }
88}
89
90/// Format a number with the provided padding and width.
91///
92/// The sign must be written by the caller.
93pub(crate) fn format_number<const WIDTH: u8>(
94    output: &mut impl io::Write,
95    value: impl itoa::Integer + DigitCount + Copy,
96    padding: modifier::Padding,
97) -> Result<usize, io::Error> {
98    match padding {
99        modifier::Padding::Space => format_number_pad_space::<WIDTH>(output, value),
100        modifier::Padding::Zero => format_number_pad_zero::<WIDTH>(output, value),
101        modifier::Padding::None => format_number_pad_none(output, value),
102    }
103}
104
105/// Format a number with the provided width and spaces as padding.
106///
107/// The sign must be written by the caller.
108pub(crate) fn format_number_pad_space<const WIDTH: u8>(
109    output: &mut impl io::Write,
110    value: impl itoa::Integer + DigitCount + Copy,
111) -> Result<usize, io::Error> {
112    let mut bytes = 0;
113    for _ in 0..(WIDTH.saturating_sub(value.num_digits())) {
114        bytes += write(output, b" ")?;
115    }
116    bytes += write(output, itoa::Buffer::new().format(value).as_bytes())?;
117    Ok(bytes)
118}
119
120/// Format a number with the provided width and zeros as padding.
121///
122/// The sign must be written by the caller.
123pub(crate) fn format_number_pad_zero<const WIDTH: u8>(
124    output: &mut impl io::Write,
125    value: impl itoa::Integer + DigitCount + Copy,
126) -> Result<usize, io::Error> {
127    let mut bytes = 0;
128    for _ in 0..(WIDTH.saturating_sub(value.num_digits())) {
129        bytes += write(output, b"0")?;
130    }
131    bytes += write(output, itoa::Buffer::new().format(value).as_bytes())?;
132    Ok(bytes)
133}
134
135/// Format a number with no padding.
136///
137/// If the sign is mandatory, the sign must be written by the caller.
138pub(crate) fn format_number_pad_none(
139    output: &mut impl io::Write,
140    value: impl itoa::Integer + Copy,
141) -> Result<usize, io::Error> {
142    write(output, itoa::Buffer::new().format(value).as_bytes())
143}
144
145/// Format the provided component into the designated output. An `Err` will be returned if the
146/// component requires information that it does not provide or if the value cannot be output to the
147/// stream.
148pub(crate) fn format_component(
149    output: &mut impl io::Write,
150    component: Component,
151    date: Option<Date>,
152    time: Option<Time>,
153    offset: Option<UtcOffset>,
154) -> Result<usize, error::Format> {
155    use Component::*;
156    Ok(match (component, date, time, offset) {
157        (Day(modifier), Some(date), ..) => fmt_day(output, date, modifier)?,
158        (Month(modifier), Some(date), ..) => fmt_month(output, date, modifier)?,
159        (Ordinal(modifier), Some(date), ..) => fmt_ordinal(output, date, modifier)?,
160        (Weekday(modifier), Some(date), ..) => fmt_weekday(output, date, modifier)?,
161        (WeekNumber(modifier), Some(date), ..) => fmt_week_number(output, date, modifier)?,
162        (Year(modifier), Some(date), ..) => fmt_year(output, date, modifier)?,
163        (Hour(modifier), _, Some(time), _) => fmt_hour(output, time, modifier)?,
164        (Minute(modifier), _, Some(time), _) => fmt_minute(output, time, modifier)?,
165        (Period(modifier), _, Some(time), _) => fmt_period(output, time, modifier)?,
166        (Second(modifier), _, Some(time), _) => fmt_second(output, time, modifier)?,
167        (Subsecond(modifier), _, Some(time), _) => fmt_subsecond(output, time, modifier)?,
168        (OffsetHour(modifier), .., Some(offset)) => fmt_offset_hour(output, offset, modifier)?,
169        (OffsetMinute(modifier), .., Some(offset)) => fmt_offset_minute(output, offset, modifier)?,
170        (OffsetSecond(modifier), .., Some(offset)) => fmt_offset_second(output, offset, modifier)?,
171        (Ignore(_), ..) => 0,
172        (UnixTimestamp(modifier), Some(date), Some(time), Some(offset)) => {
173            fmt_unix_timestamp(output, date, time, offset, modifier)?
174        }
175        (End(modifier::End {}), ..) => 0,
176
177        // This is functionally the same as a wildcard arm, but it will cause an error if a new
178        // component is added. This is to avoid a bug where a new component, the code compiles, and
179        // formatting fails.
180        // Allow unreachable patterns because some branches may be fully matched above.
181        #[allow(unreachable_patterns)]
182        (
183            Day(_) | Month(_) | Ordinal(_) | Weekday(_) | WeekNumber(_) | Year(_) | Hour(_)
184            | Minute(_) | Period(_) | Second(_) | Subsecond(_) | OffsetHour(_) | OffsetMinute(_)
185            | OffsetSecond(_) | Ignore(_) | UnixTimestamp(_) | End(_),
186            ..,
187        ) => return Err(error::Format::InsufficientTypeInformation),
188    })
189}
190
191// region: date formatters
192/// Format the day into the designated output.
193fn fmt_day(
194    output: &mut impl io::Write,
195    date: Date,
196    modifier::Day { padding }: modifier::Day,
197) -> Result<usize, io::Error> {
198    format_number::<2>(output, date.day(), padding)
199}
200
201/// Format the month into the designated output.
202fn fmt_month(
203    output: &mut impl io::Write,
204    date: Date,
205    modifier::Month {
206        padding,
207        repr,
208        case_sensitive: _, // no effect on formatting
209    }: modifier::Month,
210) -> Result<usize, io::Error> {
211    match repr {
212        modifier::MonthRepr::Numerical => {
213            format_number::<2>(output, u8::from(date.month()), padding)
214        }
215        modifier::MonthRepr::Long => write(
216            output,
217            MONTH_NAMES[u8::from(date.month()).extend::<usize>() - 1],
218        ),
219        modifier::MonthRepr::Short => write(
220            output,
221            &MONTH_NAMES[u8::from(date.month()).extend::<usize>() - 1][..3],
222        ),
223    }
224}
225
226/// Format the ordinal into the designated output.
227fn fmt_ordinal(
228    output: &mut impl io::Write,
229    date: Date,
230    modifier::Ordinal { padding }: modifier::Ordinal,
231) -> Result<usize, io::Error> {
232    format_number::<3>(output, date.ordinal(), padding)
233}
234
235/// Format the weekday into the designated output.
236fn fmt_weekday(
237    output: &mut impl io::Write,
238    date: Date,
239    modifier::Weekday {
240        repr,
241        one_indexed,
242        case_sensitive: _, // no effect on formatting
243    }: modifier::Weekday,
244) -> Result<usize, io::Error> {
245    match repr {
246        modifier::WeekdayRepr::Short => write(
247            output,
248            &WEEKDAY_NAMES[date.weekday().number_days_from_monday().extend::<usize>()][..3],
249        ),
250        modifier::WeekdayRepr::Long => write(
251            output,
252            WEEKDAY_NAMES[date.weekday().number_days_from_monday().extend::<usize>()],
253        ),
254        modifier::WeekdayRepr::Sunday => format_number::<1>(
255            output,
256            date.weekday().number_days_from_sunday() + u8::from(one_indexed),
257            modifier::Padding::None,
258        ),
259        modifier::WeekdayRepr::Monday => format_number::<1>(
260            output,
261            date.weekday().number_days_from_monday() + u8::from(one_indexed),
262            modifier::Padding::None,
263        ),
264    }
265}
266
267/// Format the week number into the designated output.
268fn fmt_week_number(
269    output: &mut impl io::Write,
270    date: Date,
271    modifier::WeekNumber { padding, repr }: modifier::WeekNumber,
272) -> Result<usize, io::Error> {
273    format_number::<2>(
274        output,
275        match repr {
276            modifier::WeekNumberRepr::Iso => date.iso_week(),
277            modifier::WeekNumberRepr::Sunday => date.sunday_based_week(),
278            modifier::WeekNumberRepr::Monday => date.monday_based_week(),
279        },
280        padding,
281    )
282}
283
284/// Format the year into the designated output.
285fn fmt_year(
286    output: &mut impl io::Write,
287    date: Date,
288    modifier::Year {
289        padding,
290        repr,
291        iso_week_based,
292        sign_is_mandatory,
293    }: modifier::Year,
294) -> Result<usize, io::Error> {
295    let full_year = if iso_week_based {
296        date.iso_year_week().0
297    } else {
298        date.year()
299    };
300    let value = match repr {
301        modifier::YearRepr::Full => full_year,
302        modifier::YearRepr::LastTwo => (full_year % 100).abs(),
303    };
304    let format_number = match repr {
305        #[cfg(feature = "large-dates")]
306        modifier::YearRepr::Full if value.abs() >= 100_000 => format_number::<6>,
307        #[cfg(feature = "large-dates")]
308        modifier::YearRepr::Full if value.abs() >= 10_000 => format_number::<5>,
309        modifier::YearRepr::Full => format_number::<4>,
310        modifier::YearRepr::LastTwo => format_number::<2>,
311    };
312    let mut bytes = 0;
313    if repr != modifier::YearRepr::LastTwo {
314        if full_year < 0 {
315            bytes += write(output, b"-")?;
316        } else if sign_is_mandatory || cfg!(feature = "large-dates") && full_year >= 10_000 {
317            bytes += write(output, b"+")?;
318        }
319    }
320    bytes += format_number(output, value.unsigned_abs(), padding)?;
321    Ok(bytes)
322}
323// endregion date formatters
324
325// region: time formatters
326/// Format the hour into the designated output.
327fn fmt_hour(
328    output: &mut impl io::Write,
329    time: Time,
330    modifier::Hour {
331        padding,
332        is_12_hour_clock,
333    }: modifier::Hour,
334) -> Result<usize, io::Error> {
335    let value = match (time.hour(), is_12_hour_clock) {
336        (hour, false) => hour,
337        (0 | 12, true) => 12,
338        (hour, true) if hour < 12 => hour,
339        (hour, true) => hour - 12,
340    };
341    format_number::<2>(output, value, padding)
342}
343
344/// Format the minute into the designated output.
345fn fmt_minute(
346    output: &mut impl io::Write,
347    time: Time,
348    modifier::Minute { padding }: modifier::Minute,
349) -> Result<usize, io::Error> {
350    format_number::<2>(output, time.minute(), padding)
351}
352
353/// Format the period into the designated output.
354fn fmt_period(
355    output: &mut impl io::Write,
356    time: Time,
357    modifier::Period {
358        is_uppercase,
359        case_sensitive: _, // no effect on formatting
360    }: modifier::Period,
361) -> Result<usize, io::Error> {
362    match (time.hour() >= 12, is_uppercase) {
363        (false, false) => write(output, b"am"),
364        (false, true) => write(output, b"AM"),
365        (true, false) => write(output, b"pm"),
366        (true, true) => write(output, b"PM"),
367    }
368}
369
370/// Format the second into the designated output.
371fn fmt_second(
372    output: &mut impl io::Write,
373    time: Time,
374    modifier::Second { padding }: modifier::Second,
375) -> Result<usize, io::Error> {
376    format_number::<2>(output, time.second(), padding)
377}
378
379/// Format the subsecond into the designated output.
380fn fmt_subsecond<W: io::Write>(
381    output: &mut W,
382    time: Time,
383    modifier::Subsecond { digits }: modifier::Subsecond,
384) -> Result<usize, io::Error> {
385    use modifier::SubsecondDigits::*;
386    let nanos = time.nanosecond();
387
388    if digits == Nine || (digits == OneOrMore && nanos % 10 != 0) {
389        format_number_pad_zero::<9>(output, nanos)
390    } else if digits == Eight || (digits == OneOrMore && (nanos / 10) % 10 != 0) {
391        format_number_pad_zero::<8>(output, nanos / 10)
392    } else if digits == Seven || (digits == OneOrMore && (nanos / 100) % 10 != 0) {
393        format_number_pad_zero::<7>(output, nanos / 100)
394    } else if digits == Six || (digits == OneOrMore && (nanos / 1_000) % 10 != 0) {
395        format_number_pad_zero::<6>(output, nanos / 1_000)
396    } else if digits == Five || (digits == OneOrMore && (nanos / 10_000) % 10 != 0) {
397        format_number_pad_zero::<5>(output, nanos / 10_000)
398    } else if digits == Four || (digits == OneOrMore && (nanos / 100_000) % 10 != 0) {
399        format_number_pad_zero::<4>(output, nanos / 100_000)
400    } else if digits == Three || (digits == OneOrMore && (nanos / 1_000_000) % 10 != 0) {
401        format_number_pad_zero::<3>(output, nanos / 1_000_000)
402    } else if digits == Two || (digits == OneOrMore && (nanos / 10_000_000) % 10 != 0) {
403        format_number_pad_zero::<2>(output, nanos / 10_000_000)
404    } else {
405        format_number_pad_zero::<1>(output, nanos / 100_000_000)
406    }
407}
408// endregion time formatters
409
410// region: offset formatters
411/// Format the offset hour into the designated output.
412fn fmt_offset_hour(
413    output: &mut impl io::Write,
414    offset: UtcOffset,
415    modifier::OffsetHour {
416        padding,
417        sign_is_mandatory,
418    }: modifier::OffsetHour,
419) -> Result<usize, io::Error> {
420    let mut bytes = 0;
421    if offset.is_negative() {
422        bytes += write(output, b"-")?;
423    } else if sign_is_mandatory {
424        bytes += write(output, b"+")?;
425    }
426    bytes += format_number::<2>(output, offset.whole_hours().unsigned_abs(), padding)?;
427    Ok(bytes)
428}
429
430/// Format the offset minute into the designated output.
431fn fmt_offset_minute(
432    output: &mut impl io::Write,
433    offset: UtcOffset,
434    modifier::OffsetMinute { padding }: modifier::OffsetMinute,
435) -> Result<usize, io::Error> {
436    format_number::<2>(output, offset.minutes_past_hour().unsigned_abs(), padding)
437}
438
439/// Format the offset second into the designated output.
440fn fmt_offset_second(
441    output: &mut impl io::Write,
442    offset: UtcOffset,
443    modifier::OffsetSecond { padding }: modifier::OffsetSecond,
444) -> Result<usize, io::Error> {
445    format_number::<2>(output, offset.seconds_past_minute().unsigned_abs(), padding)
446}
447// endregion offset formatters
448
449/// Format the Unix timestamp into the designated output.
450fn fmt_unix_timestamp(
451    output: &mut impl io::Write,
452    date: Date,
453    time: Time,
454    offset: UtcOffset,
455    modifier::UnixTimestamp {
456        precision,
457        sign_is_mandatory,
458    }: modifier::UnixTimestamp,
459) -> Result<usize, io::Error> {
460    let date_time = OffsetDateTime::new_in_offset(date, time, offset).to_offset(UtcOffset::UTC);
461
462    if date_time < OffsetDateTime::UNIX_EPOCH {
463        write(output, b"-")?;
464    } else if sign_is_mandatory {
465        write(output, b"+")?;
466    }
467
468    match precision {
469        modifier::UnixTimestampPrecision::Second => {
470            format_number_pad_none(output, date_time.unix_timestamp().unsigned_abs())
471        }
472        modifier::UnixTimestampPrecision::Millisecond => format_number_pad_none(
473            output,
474            (date_time.unix_timestamp_nanos()
475                / Nanosecond::per(Millisecond).cast_signed().extend::<i128>())
476            .unsigned_abs(),
477        ),
478        modifier::UnixTimestampPrecision::Microsecond => format_number_pad_none(
479            output,
480            (date_time.unix_timestamp_nanos()
481                / Nanosecond::per(Microsecond).cast_signed().extend::<i128>())
482            .unsigned_abs(),
483        ),
484        modifier::UnixTimestampPrecision::Nanosecond => {
485            format_number_pad_none(output, date_time.unix_timestamp_nanos().unsigned_abs())
486        }
487    }
488}