1//! Formatting for various types.
2
3pub(crate) mod formattable;
4mod iso8601;
5
6use core::num::NonZeroU8;
7use std::io;
8
9pub use self::formattable::Formattable;
10use crate::convert::*;
11use crate::format_description::{modifier, Component};
12use crate::{error, Date, OffsetDateTime, Time, UtcOffset};
13
14#[allow(clippy::missing_docs_in_private_items)]
15const 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)]
31const 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.
46pub(crate) trait DigitCount {
47 /// The number of digits in the stringified value.
48 fn num_digits(self) -> u8;
49}
50impl 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}
62impl 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
79impl 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.
122pub(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.
128pub(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`.
133pub(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.
146pub(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.
171pub(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.
186pub(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.
201pub(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.
216pub(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.
226pub(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.
259fn 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.
268fn 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.
285fn 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.
294fn 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.
326fn 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.
343fn 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.
385fn 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.
403fn 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.
412fn 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.
429fn 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.
438fn 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.
470fn 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.
489fn 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.
498fn 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.
508fn 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