1 | //! Helpers for implementing formatting for ISO 8601. |
2 | |
3 | use std::io; |
4 | |
5 | use crate::convert::*; |
6 | use crate::format_description::well_known::iso8601::{ |
7 | DateKind, EncodedConfig, OffsetPrecision, TimePrecision, |
8 | }; |
9 | use crate::format_description::well_known::Iso8601; |
10 | use crate::formatting::{format_float, format_number_pad_zero, write, write_if, write_if_else}; |
11 | use crate::{error, Date, Time, UtcOffset}; |
12 | |
13 | /// Format the date portion of ISO 8601. |
14 | pub(super) fn format_date<const CONFIG: EncodedConfig>( |
15 | output: &mut impl io::Write, |
16 | date: Date, |
17 | ) -> Result<usize, error::Format> { |
18 | let mut bytes = 0; |
19 | |
20 | match Iso8601::<CONFIG>::DATE_KIND { |
21 | DateKind::Calendar => { |
22 | let (year, month, day) = date.to_calendar_date(); |
23 | if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS { |
24 | bytes += write_if_else(output, year < 0, b"-" , b"+" )?; |
25 | bytes += format_number_pad_zero::<6>(output, year.unsigned_abs())?; |
26 | } else if !(0..=9999).contains(&year) { |
27 | return Err(error::Format::InvalidComponent("year" )); |
28 | } else { |
29 | bytes += format_number_pad_zero::<4>(output, year as u32)?; |
30 | } |
31 | bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-" )?; |
32 | bytes += format_number_pad_zero::<2>(output, month as u8)?; |
33 | bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-" )?; |
34 | bytes += format_number_pad_zero::<2>(output, day)?; |
35 | } |
36 | DateKind::Week => { |
37 | let (year, week, day) = date.to_iso_week_date(); |
38 | if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS { |
39 | bytes += write_if_else(output, year < 0, b"-" , b"+" )?; |
40 | bytes += format_number_pad_zero::<6>(output, year.unsigned_abs())?; |
41 | } else if !(0..=9999).contains(&year) { |
42 | return Err(error::Format::InvalidComponent("year" )); |
43 | } else { |
44 | bytes += format_number_pad_zero::<4>(output, year as u32)?; |
45 | } |
46 | bytes += write_if_else(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-W" , b"W" )?; |
47 | bytes += format_number_pad_zero::<2>(output, week)?; |
48 | bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-" )?; |
49 | bytes += format_number_pad_zero::<1>(output, day.number_from_monday())?; |
50 | } |
51 | DateKind::Ordinal => { |
52 | let (year, day) = date.to_ordinal_date(); |
53 | if Iso8601::<CONFIG>::YEAR_IS_SIX_DIGITS { |
54 | bytes += write_if_else(output, year < 0, b"-" , b"+" )?; |
55 | bytes += format_number_pad_zero::<6>(output, year.unsigned_abs())?; |
56 | } else if !(0..=9999).contains(&year) { |
57 | return Err(error::Format::InvalidComponent("year" )); |
58 | } else { |
59 | bytes += format_number_pad_zero::<4>(output, year as u32)?; |
60 | } |
61 | bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b"-" )?; |
62 | bytes += format_number_pad_zero::<3>(output, day)?; |
63 | } |
64 | } |
65 | |
66 | Ok(bytes) |
67 | } |
68 | |
69 | /// Format the time portion of ISO 8601. |
70 | pub(super) fn format_time<const CONFIG: EncodedConfig>( |
71 | output: &mut impl io::Write, |
72 | time: Time, |
73 | ) -> Result<usize, error::Format> { |
74 | let mut bytes = 0; |
75 | |
76 | // The "T" can only be omitted in extended format where there is no date being formatted. |
77 | bytes += write_if( |
78 | output, |
79 | Iso8601::<CONFIG>::USE_SEPARATORS || Iso8601::<CONFIG>::FORMAT_DATE, |
80 | b"T" , |
81 | )?; |
82 | |
83 | let (hours, minutes, seconds, nanoseconds) = time.as_hms_nano(); |
84 | |
85 | match Iso8601::<CONFIG>::TIME_PRECISION { |
86 | TimePrecision::Hour { decimal_digits } => { |
87 | let hours = (hours as f64) |
88 | + (minutes as f64) / Minute.per(Hour) as f64 |
89 | + (seconds as f64) / Second.per(Hour) as f64 |
90 | + (nanoseconds as f64) / Nanosecond.per(Hour) as f64; |
91 | format_float(output, hours, 2, decimal_digits)?; |
92 | } |
93 | TimePrecision::Minute { decimal_digits } => { |
94 | bytes += format_number_pad_zero::<2>(output, hours)?; |
95 | bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":" )?; |
96 | let minutes = (minutes as f64) |
97 | + (seconds as f64) / Second.per(Minute) as f64 |
98 | + (nanoseconds as f64) / Nanosecond.per(Minute) as f64; |
99 | bytes += format_float(output, minutes, 2, decimal_digits)?; |
100 | } |
101 | TimePrecision::Second { decimal_digits } => { |
102 | bytes += format_number_pad_zero::<2>(output, hours)?; |
103 | bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":" )?; |
104 | bytes += format_number_pad_zero::<2>(output, minutes)?; |
105 | bytes += write_if(output, Iso8601::<CONFIG>::USE_SEPARATORS, b":" )?; |
106 | let seconds = (seconds as f64) + (nanoseconds as f64) / Nanosecond.per(Second) as f64; |
107 | bytes += format_float(output, seconds, 2, decimal_digits)?; |
108 | } |
109 | } |
110 | |
111 | Ok(bytes) |
112 | } |
113 | |
114 | /// Format the UTC offset portion of ISO 8601. |
115 | pub(super) fn format_offset<const CONFIG: EncodedConfig>( |
116 | output: &mut impl io::Write, |
117 | offset: UtcOffset, |
118 | ) -> Result<usize, error::Format> { |
119 | if Iso8601::<CONFIG>::FORMAT_TIME && offset.is_utc() { |
120 | return Ok(write(output, bytes:b"Z" )?); |
121 | } |
122 | |
123 | let mut bytes: usize = 0; |
124 | |
125 | let (hours: i8, minutes: i8, seconds: i8) = offset.as_hms(); |
126 | if seconds != 0 { |
127 | return Err(error::Format::InvalidComponent("offset_second" )); |
128 | } |
129 | bytes += write_if_else(output, pred:offset.is_negative(), true_bytes:b"-" , false_bytes:b"+" )?; |
130 | bytes += format_number_pad_zero::<2>(output, value:hours.unsigned_abs())?; |
131 | |
132 | if Iso8601::<CONFIG>::OFFSET_PRECISION == OffsetPrecision::Hour && minutes != 0 { |
133 | return Err(error::Format::InvalidComponent("offset_minute" )); |
134 | } else if Iso8601::<CONFIG>::OFFSET_PRECISION == OffsetPrecision::Minute { |
135 | bytes += write_if(output, pred:Iso8601::<CONFIG>::USE_SEPARATORS, bytes:b":" )?; |
136 | bytes += format_number_pad_zero::<2>(output, value:minutes.unsigned_abs())?; |
137 | } |
138 | |
139 | Ok(bytes) |
140 | } |
141 | |