time/formatting/
mod.rs

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