1 | //! Formatting for various types. |
2 | |
3 | pub(crate) mod formattable; |
4 | mod iso8601; |
5 | |
6 | use core::num::NonZeroU8; |
7 | use std::io; |
8 | |
9 | use num_conv::prelude::*; |
10 | |
11 | pub use self::formattable::Formattable; |
12 | use crate::convert::*; |
13 | use crate::ext::DigitCount; |
14 | use crate::format_description::{modifier, Component}; |
15 | use crate::{error, Date, OffsetDateTime, Time, UtcOffset}; |
16 | |
17 | #[allow (clippy::missing_docs_in_private_items)] |
18 | const 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)] |
34 | const 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. |
45 | pub(crate) fn write(output: &mut impl io::Write, bytes: &[u8]) -> io::Result<usize> { |
46 | output.write_all(buf:bytes)?; |
47 | Ok(bytes.len()) |
48 | } |
49 | |
50 | /// If `pred` is true, write all bytes to the output, returning the number of bytes written. |
51 | pub(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`. |
60 | pub(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, bytes: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. |
73 | pub(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: NonZero) => { |
81 | // Truncate the decimal points up to the precision |
82 | let trunc_num: f64 = 10_f64.powi(digits_after_decimal.get().cast_signed().extend()); |
83 | let value: f64 = f64::trunc(self:value * trunc_num) / trunc_num; |
84 | |
85 | let digits_after_decimal: usize = digits_after_decimal.get().extend(); |
86 | let width: usize = 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: u64 = value.trunc() as u64; |
92 | let width: usize = 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. |
102 | pub(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. |
117 | pub(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: usize = 0; |
122 | for _ in 0..(WIDTH.saturating_sub(value.num_digits())) { |
123 | bytes += write(output, bytes: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. |
132 | pub(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: usize = 0; |
137 | for _ in 0..(WIDTH.saturating_sub(value.num_digits())) { |
138 | bytes += write(output, bytes: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. |
147 | pub(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. |
157 | pub(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. |
202 | fn fmt_day( |
203 | output: &mut impl io::Write, |
204 | date: Date, |
205 | modifier::Day { padding: Padding }: modifier::Day, |
206 | ) -> Result<usize, io::Error> { |
207 | format_number::<2>(output, value:date.day(), padding) |
208 | } |
209 | |
210 | /// Format the month into the designated output. |
211 | fn fmt_month( |
212 | output: &mut impl io::Write, |
213 | date: Date, |
214 | modifier::Month { |
215 | padding: Padding, |
216 | repr: MonthRepr, |
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, value: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. |
236 | fn fmt_ordinal( |
237 | output: &mut impl io::Write, |
238 | date: Date, |
239 | modifier::Ordinal { padding: Padding }: modifier::Ordinal, |
240 | ) -> Result<usize, io::Error> { |
241 | format_number::<3>(output, value:date.ordinal(), padding) |
242 | } |
243 | |
244 | /// Format the weekday into the designated output. |
245 | fn fmt_weekday( |
246 | output: &mut impl io::Write, |
247 | date: Date, |
248 | modifier::Weekday { |
249 | repr: WeekdayRepr, |
250 | one_indexed: bool, |
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 | value: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 | value: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. |
277 | fn fmt_week_number( |
278 | output: &mut impl io::Write, |
279 | date: Date, |
280 | modifier::WeekNumber { padding: Padding, repr: WeekNumberRepr }: modifier::WeekNumber, |
281 | ) -> Result<usize, io::Error> { |
282 | format_number::<2>( |
283 | output, |
284 | value: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. |
294 | fn fmt_year( |
295 | output: &mut impl io::Write, |
296 | date: Date, |
297 | modifier::Year { |
298 | padding: Padding, |
299 | repr: YearRepr, |
300 | iso_week_based: bool, |
301 | sign_is_mandatory: bool, |
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. |
341 | fn fmt_hour( |
342 | output: &mut impl io::Write, |
343 | time: Time, |
344 | modifier::Hour { |
345 | padding: Padding, |
346 | is_12_hour_clock: bool, |
347 | }: modifier::Hour, |
348 | ) -> Result<usize, io::Error> { |
349 | let value: u8 = match (time.hour(), is_12_hour_clock) { |
350 | (hour: u8, false) => hour, |
351 | (0 | 12, true) => 12, |
352 | (hour: u8, true) if hour < 12 => hour, |
353 | (hour: u8, true) => hour - 12, |
354 | }; |
355 | format_number::<2>(output, value, padding) |
356 | } |
357 | |
358 | /// Format the minute into the designated output. |
359 | fn fmt_minute( |
360 | output: &mut impl io::Write, |
361 | time: Time, |
362 | modifier::Minute { padding: Padding }: modifier::Minute, |
363 | ) -> Result<usize, io::Error> { |
364 | format_number::<2>(output, value:time.minute(), padding) |
365 | } |
366 | |
367 | /// Format the period into the designated output. |
368 | fn fmt_period( |
369 | output: &mut impl io::Write, |
370 | time: Time, |
371 | modifier::Period { |
372 | is_uppercase: bool, |
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, bytes:b"am" ), |
378 | (false, true) => write(output, bytes:b"AM" ), |
379 | (true, false) => write(output, bytes:b"pm" ), |
380 | (true, true) => write(output, bytes:b"PM" ), |
381 | } |
382 | } |
383 | |
384 | /// Format the second into the designated output. |
385 | fn fmt_second( |
386 | output: &mut impl io::Write, |
387 | time: Time, |
388 | modifier::Second { padding: Padding }: modifier::Second, |
389 | ) -> Result<usize, io::Error> { |
390 | format_number::<2>(output, value:time.second(), padding) |
391 | } |
392 | |
393 | /// Format the subsecond into the designated output. |
394 | fn fmt_subsecond<W: io::Write>( |
395 | output: &mut W, |
396 | time: Time, |
397 | modifier::Subsecond { digits: SubsecondDigits }: modifier::Subsecond, |
398 | ) -> Result<usize, io::Error> { |
399 | use modifier::SubsecondDigits::*; |
400 | let nanos: u32 = time.nanosecond(); |
401 | |
402 | if digits == Nine || (digits == OneOrMore && nanos % 10 != 0) { |
403 | format_number_pad_zero::<9>(output, value:nanos) |
404 | } else if digits == Eight || (digits == OneOrMore && (nanos / 10) % 10 != 0) { |
405 | format_number_pad_zero::<8>(output, value:nanos / 10) |
406 | } else if digits == Seven || (digits == OneOrMore && (nanos / 100) % 10 != 0) { |
407 | format_number_pad_zero::<7>(output, value:nanos / 100) |
408 | } else if digits == Six || (digits == OneOrMore && (nanos / 1_000) % 10 != 0) { |
409 | format_number_pad_zero::<6>(output, value:nanos / 1_000) |
410 | } else if digits == Five || (digits == OneOrMore && (nanos / 10_000) % 10 != 0) { |
411 | format_number_pad_zero::<5>(output, value:nanos / 10_000) |
412 | } else if digits == Four || (digits == OneOrMore && (nanos / 100_000) % 10 != 0) { |
413 | format_number_pad_zero::<4>(output, value:nanos / 100_000) |
414 | } else if digits == Three || (digits == OneOrMore && (nanos / 1_000_000) % 10 != 0) { |
415 | format_number_pad_zero::<3>(output, value:nanos / 1_000_000) |
416 | } else if digits == Two || (digits == OneOrMore && (nanos / 10_000_000) % 10 != 0) { |
417 | format_number_pad_zero::<2>(output, value:nanos / 10_000_000) |
418 | } else { |
419 | format_number_pad_zero::<1>(output, value:nanos / 100_000_000) |
420 | } |
421 | } |
422 | // endregion time formatters |
423 | |
424 | // region: offset formatters |
425 | /// Format the offset hour into the designated output. |
426 | fn fmt_offset_hour( |
427 | output: &mut impl io::Write, |
428 | offset: UtcOffset, |
429 | modifier::OffsetHour { |
430 | padding: Padding, |
431 | sign_is_mandatory: bool, |
432 | }: modifier::OffsetHour, |
433 | ) -> Result<usize, io::Error> { |
434 | let mut bytes: usize = 0; |
435 | if offset.is_negative() { |
436 | bytes += write(output, bytes:b"-" )?; |
437 | } else if sign_is_mandatory { |
438 | bytes += write(output, bytes:b"+" )?; |
439 | } |
440 | bytes += format_number::<2>(output, value:offset.whole_hours().unsigned_abs(), padding)?; |
441 | Ok(bytes) |
442 | } |
443 | |
444 | /// Format the offset minute into the designated output. |
445 | fn fmt_offset_minute( |
446 | output: &mut impl io::Write, |
447 | offset: UtcOffset, |
448 | modifier::OffsetMinute { padding: Padding }: modifier::OffsetMinute, |
449 | ) -> Result<usize, io::Error> { |
450 | format_number::<2>(output, value:offset.minutes_past_hour().unsigned_abs(), padding) |
451 | } |
452 | |
453 | /// Format the offset second into the designated output. |
454 | fn fmt_offset_second( |
455 | output: &mut impl io::Write, |
456 | offset: UtcOffset, |
457 | modifier::OffsetSecond { padding: Padding }: modifier::OffsetSecond, |
458 | ) -> Result<usize, io::Error> { |
459 | format_number::<2>(output, value:offset.seconds_past_minute().unsigned_abs(), padding) |
460 | } |
461 | // endregion offset formatters |
462 | |
463 | /// Format the Unix timestamp into the designated output. |
464 | fn fmt_unix_timestamp( |
465 | output: &mut impl io::Write, |
466 | date: Date, |
467 | time: Time, |
468 | offset: UtcOffset, |
469 | modifier::UnixTimestamp { |
470 | precision: UnixTimestampPrecision, |
471 | sign_is_mandatory: bool, |
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 | } |
503 | |