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