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