1// This is a part of Chrono.
2// See README.md and LICENSE.txt for details.
3
4//! Date and time formatting routines.
5
6#[cfg(all(not(feature = "std"), feature = "alloc"))]
7use alloc::string::{String, ToString};
8#[cfg(feature = "alloc")]
9use core::borrow::Borrow;
10#[cfg(feature = "alloc")]
11use core::fmt::Display;
12use core::fmt::{self, Write};
13
14#[cfg(feature = "alloc")]
15use crate::offset::Offset;
16#[cfg(any(feature = "alloc", feature = "serde", feature = "rustc-serialize"))]
17use crate::{Datelike, FixedOffset, NaiveDateTime, Timelike};
18#[cfg(feature = "alloc")]
19use crate::{NaiveDate, NaiveTime, Weekday};
20
21#[cfg(feature = "alloc")]
22use super::locales;
23#[cfg(all(feature = "unstable-locales", feature = "alloc"))]
24use super::Locale;
25#[cfg(any(feature = "alloc", feature = "serde", feature = "rustc-serialize"))]
26use super::{Colons, OffsetFormat, OffsetPrecision, Pad};
27#[cfg(feature = "alloc")]
28use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric};
29#[cfg(feature = "alloc")]
30use locales::*;
31
32/// A *temporary* object which can be used as an argument to `format!` or others.
33/// This is normally constructed via `format` methods of each date and time type.
34#[cfg(feature = "alloc")]
35#[derive(Debug)]
36pub struct DelayedFormat<I> {
37 /// The date view, if any.
38 date: Option<NaiveDate>,
39 /// The time view, if any.
40 time: Option<NaiveTime>,
41 /// The name and local-to-UTC difference for the offset (timezone), if any.
42 off: Option<(String, FixedOffset)>,
43 /// An iterator returning formatting items.
44 items: I,
45 /// Locale used for text.
46 // TODO: Only used with the locale feature. We should make this property
47 // only present when the feature is enabled.
48 #[cfg(feature = "unstable-locales")]
49 locale: Option<Locale>,
50}
51
52#[cfg(feature = "alloc")]
53impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> DelayedFormat<I> {
54 /// Makes a new `DelayedFormat` value out of local date and time.
55 #[must_use]
56 pub fn new(date: Option<NaiveDate>, time: Option<NaiveTime>, items: I) -> DelayedFormat<I> {
57 DelayedFormat {
58 date,
59 time,
60 off: None,
61 items,
62 #[cfg(feature = "unstable-locales")]
63 locale: None,
64 }
65 }
66
67 /// Makes a new `DelayedFormat` value out of local date and time and UTC offset.
68 #[must_use]
69 pub fn new_with_offset<Off>(
70 date: Option<NaiveDate>,
71 time: Option<NaiveTime>,
72 offset: &Off,
73 items: I,
74 ) -> DelayedFormat<I>
75 where
76 Off: Offset + Display,
77 {
78 let name_and_diff = (offset.to_string(), offset.fix());
79 DelayedFormat {
80 date,
81 time,
82 off: Some(name_and_diff),
83 items,
84 #[cfg(feature = "unstable-locales")]
85 locale: None,
86 }
87 }
88
89 /// Makes a new `DelayedFormat` value out of local date and time and locale.
90 #[cfg(feature = "unstable-locales")]
91 #[must_use]
92 pub fn new_with_locale(
93 date: Option<NaiveDate>,
94 time: Option<NaiveTime>,
95 items: I,
96 locale: Locale,
97 ) -> DelayedFormat<I> {
98 DelayedFormat { date, time, off: None, items, locale: Some(locale) }
99 }
100
101 /// Makes a new `DelayedFormat` value out of local date and time, UTC offset and locale.
102 #[cfg(feature = "unstable-locales")]
103 #[must_use]
104 pub fn new_with_offset_and_locale<Off>(
105 date: Option<NaiveDate>,
106 time: Option<NaiveTime>,
107 offset: &Off,
108 items: I,
109 locale: Locale,
110 ) -> DelayedFormat<I>
111 where
112 Off: Offset + Display,
113 {
114 let name_and_diff = (offset.to_string(), offset.fix());
115 DelayedFormat { date, time, off: Some(name_and_diff), items, locale: Some(locale) }
116 }
117}
118
119#[cfg(feature = "alloc")]
120impl<'a, I: Iterator<Item = B> + Clone, B: Borrow<Item<'a>>> Display for DelayedFormat<I> {
121 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122 #[cfg(feature = "unstable-locales")]
123 let locale = self.locale;
124 #[cfg(not(feature = "unstable-locales"))]
125 let locale: Option = None;
126
127 let mut result: String = String::new();
128 for item: B in self.items.clone() {
129 format_inner(
130 &mut result,
131 self.date.as_ref(),
132 self.time.as_ref(),
133 self.off.as_ref(),
134 item:item.borrow(),
135 locale,
136 )?;
137 }
138 f.pad(&result)
139 }
140}
141
142/// Tries to format given arguments with given formatting items.
143/// Internally used by `DelayedFormat`.
144#[cfg(feature = "alloc")]
145#[deprecated(since = "0.4.32", note = "Use DelayedFormat::fmt instead")]
146pub fn format<'a, I, B>(
147 w: &mut fmt::Formatter,
148 date: Option<&NaiveDate>,
149 time: Option<&NaiveTime>,
150 off: Option<&(String, FixedOffset)>,
151 items: I,
152) -> fmt::Result
153where
154 I: Iterator<Item = B> + Clone,
155 B: Borrow<Item<'a>>,
156{
157 DelayedFormat {
158 date: date.copied(),
159 time: time.copied(),
160 off: off.cloned(),
161 items,
162 #[cfg(feature = "unstable-locales")]
163 locale: None,
164 }
165 .fmt(w)
166}
167
168/// Formats single formatting item
169#[cfg(feature = "alloc")]
170#[deprecated(since = "0.4.32", note = "Use DelayedFormat::fmt instead")]
171pub fn format_item(
172 w: &mut fmt::Formatter,
173 date: Option<&NaiveDate>,
174 time: Option<&NaiveTime>,
175 off: Option<&(String, FixedOffset)>,
176 item: &Item<'_>,
177) -> fmt::Result {
178 DelayedFormat {
179 date: date.copied(),
180 time: time.copied(),
181 off: off.cloned(),
182 items: [item].into_iter(),
183 #[cfg(feature = "unstable-locales")]
184 locale: None,
185 }
186 .fmt(w)
187}
188
189#[cfg(feature = "alloc")]
190fn format_inner(
191 w: &mut impl Write,
192 date: Option<&NaiveDate>,
193 time: Option<&NaiveTime>,
194 off: Option<&(String, FixedOffset)>,
195 item: &Item<'_>,
196 locale: Option<Locale>,
197) -> fmt::Result {
198 let locale = locale.unwrap_or(default_locale());
199
200 match *item {
201 Item::Literal(s) | Item::Space(s) => w.write_str(s),
202 #[cfg(feature = "alloc")]
203 Item::OwnedLiteral(ref s) | Item::OwnedSpace(ref s) => w.write_str(s),
204
205 Item::Numeric(ref spec, ref pad) => {
206 use self::Numeric::*;
207
208 let week_from_sun = |d: &NaiveDate| d.weeks_from(Weekday::Sun);
209 let week_from_mon = |d: &NaiveDate| d.weeks_from(Weekday::Mon);
210
211 let (width, v) = match *spec {
212 Year => (4, date.map(|d| i64::from(d.year()))),
213 YearDiv100 => (2, date.map(|d| i64::from(d.year()).div_euclid(100))),
214 YearMod100 => (2, date.map(|d| i64::from(d.year()).rem_euclid(100))),
215 IsoYear => (4, date.map(|d| i64::from(d.iso_week().year()))),
216 IsoYearDiv100 => (2, date.map(|d| i64::from(d.iso_week().year()).div_euclid(100))),
217 IsoYearMod100 => (2, date.map(|d| i64::from(d.iso_week().year()).rem_euclid(100))),
218 Month => (2, date.map(|d| i64::from(d.month()))),
219 Day => (2, date.map(|d| i64::from(d.day()))),
220 WeekFromSun => (2, date.map(|d| i64::from(week_from_sun(d)))),
221 WeekFromMon => (2, date.map(|d| i64::from(week_from_mon(d)))),
222 IsoWeek => (2, date.map(|d| i64::from(d.iso_week().week()))),
223 NumDaysFromSun => (1, date.map(|d| i64::from(d.weekday().num_days_from_sunday()))),
224 WeekdayFromMon => (1, date.map(|d| i64::from(d.weekday().number_from_monday()))),
225 Ordinal => (3, date.map(|d| i64::from(d.ordinal()))),
226 Hour => (2, time.map(|t| i64::from(t.hour()))),
227 Hour12 => (2, time.map(|t| i64::from(t.hour12().1))),
228 Minute => (2, time.map(|t| i64::from(t.minute()))),
229 Second => (2, time.map(|t| i64::from(t.second() + t.nanosecond() / 1_000_000_000))),
230 Nanosecond => (9, time.map(|t| i64::from(t.nanosecond() % 1_000_000_000))),
231 Timestamp => (
232 1,
233 match (date, time, off) {
234 (Some(d), Some(t), None) => Some(d.and_time(*t).and_utc().timestamp()),
235 (Some(d), Some(t), Some(&(_, off))) => Some(
236 d.and_time(*t).and_utc().timestamp() - i64::from(off.local_minus_utc()),
237 ),
238 (_, _, _) => None,
239 },
240 ),
241
242 // for the future expansion
243 Internal(ref int) => match int._dummy {},
244 };
245
246 if let Some(v) = v {
247 if (spec == &Year || spec == &IsoYear) && !(0..10_000).contains(&v) {
248 // non-four-digit years require an explicit sign as per ISO 8601
249 match *pad {
250 Pad::None => write!(w, "{:+}", v),
251 Pad::Zero => write!(w, "{:+01$}", v, width + 1),
252 Pad::Space => write!(w, "{:+1$}", v, width + 1),
253 }
254 } else {
255 match *pad {
256 Pad::None => write!(w, "{}", v),
257 Pad::Zero => write!(w, "{:01$}", v, width),
258 Pad::Space => write!(w, "{:1$}", v, width),
259 }
260 }
261 } else {
262 Err(fmt::Error) // insufficient arguments for given format
263 }
264 }
265
266 Item::Fixed(ref spec) => {
267 use self::Fixed::*;
268
269 let ret = match *spec {
270 ShortMonthName => date.map(|d| {
271 w.write_str(short_months(locale)[d.month0() as usize])?;
272 Ok(())
273 }),
274 LongMonthName => date.map(|d| {
275 w.write_str(long_months(locale)[d.month0() as usize])?;
276 Ok(())
277 }),
278 ShortWeekdayName => date.map(|d| {
279 w.write_str(
280 short_weekdays(locale)[d.weekday().num_days_from_sunday() as usize],
281 )?;
282 Ok(())
283 }),
284 LongWeekdayName => date.map(|d| {
285 w.write_str(
286 long_weekdays(locale)[d.weekday().num_days_from_sunday() as usize],
287 )?;
288 Ok(())
289 }),
290 LowerAmPm => time.map(|t| {
291 let ampm = if t.hour12().0 { am_pm(locale)[1] } else { am_pm(locale)[0] };
292 for c in ampm.chars().flat_map(|c| c.to_lowercase()) {
293 w.write_char(c)?
294 }
295 Ok(())
296 }),
297 UpperAmPm => time.map(|t| {
298 w.write_str(if t.hour12().0 { am_pm(locale)[1] } else { am_pm(locale)[0] })?;
299 Ok(())
300 }),
301 Nanosecond => time.map(|t| {
302 let nano = t.nanosecond() % 1_000_000_000;
303 if nano == 0 {
304 Ok(())
305 } else {
306 w.write_str(decimal_point(locale))?;
307 if nano % 1_000_000 == 0 {
308 write!(w, "{:03}", nano / 1_000_000)
309 } else if nano % 1_000 == 0 {
310 write!(w, "{:06}", nano / 1_000)
311 } else {
312 write!(w, "{:09}", nano)
313 }
314 }
315 }),
316 Nanosecond3 => time.map(|t| {
317 let nano = t.nanosecond() % 1_000_000_000;
318 w.write_str(decimal_point(locale))?;
319 write!(w, "{:03}", nano / 1_000_000)
320 }),
321 Nanosecond6 => time.map(|t| {
322 let nano = t.nanosecond() % 1_000_000_000;
323 w.write_str(decimal_point(locale))?;
324 write!(w, "{:06}", nano / 1_000)
325 }),
326 Nanosecond9 => time.map(|t| {
327 let nano = t.nanosecond() % 1_000_000_000;
328 w.write_str(decimal_point(locale))?;
329 write!(w, "{:09}", nano)
330 }),
331 Internal(InternalFixed { val: InternalInternal::Nanosecond3NoDot }) => {
332 time.map(|t| {
333 let nano = t.nanosecond() % 1_000_000_000;
334 write!(w, "{:03}", nano / 1_000_000)
335 })
336 }
337 Internal(InternalFixed { val: InternalInternal::Nanosecond6NoDot }) => {
338 time.map(|t| {
339 let nano = t.nanosecond() % 1_000_000_000;
340 write!(w, "{:06}", nano / 1_000)
341 })
342 }
343 Internal(InternalFixed { val: InternalInternal::Nanosecond9NoDot }) => {
344 time.map(|t| {
345 let nano = t.nanosecond() % 1_000_000_000;
346 write!(w, "{:09}", nano)
347 })
348 }
349 TimezoneName => off.map(|(name, _)| {
350 w.write_str(name)?;
351 Ok(())
352 }),
353 TimezoneOffset | TimezoneOffsetZ => off.map(|&(_, off)| {
354 OffsetFormat {
355 precision: OffsetPrecision::Minutes,
356 colons: Colons::Maybe,
357 allow_zulu: *spec == TimezoneOffsetZ,
358 padding: Pad::Zero,
359 }
360 .format(w, off)
361 }),
362 TimezoneOffsetColon | TimezoneOffsetColonZ => off.map(|&(_, off)| {
363 OffsetFormat {
364 precision: OffsetPrecision::Minutes,
365 colons: Colons::Colon,
366 allow_zulu: *spec == TimezoneOffsetColonZ,
367 padding: Pad::Zero,
368 }
369 .format(w, off)
370 }),
371 TimezoneOffsetDoubleColon => off.map(|&(_, off)| {
372 OffsetFormat {
373 precision: OffsetPrecision::Seconds,
374 colons: Colons::Colon,
375 allow_zulu: false,
376 padding: Pad::Zero,
377 }
378 .format(w, off)
379 }),
380 TimezoneOffsetTripleColon => off.map(|&(_, off)| {
381 OffsetFormat {
382 precision: OffsetPrecision::Hours,
383 colons: Colons::None,
384 allow_zulu: false,
385 padding: Pad::Zero,
386 }
387 .format(w, off)
388 }),
389 Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive }) => {
390 return Err(fmt::Error);
391 }
392 RFC2822 =>
393 // same as `%a, %d %b %Y %H:%M:%S %z`
394 {
395 if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) {
396 Some(write_rfc2822(w, crate::NaiveDateTime::new(*d, *t), off))
397 } else {
398 None
399 }
400 }
401 RFC3339 =>
402 // same as `%Y-%m-%dT%H:%M:%S%.f%:z`
403 {
404 if let (Some(d), Some(t), Some(&(_, off))) = (date, time, off) {
405 Some(write_rfc3339(
406 w,
407 crate::NaiveDateTime::new(*d, *t),
408 off.fix(),
409 SecondsFormat::AutoSi,
410 false,
411 ))
412 } else {
413 None
414 }
415 }
416 };
417
418 ret.unwrap_or(Err(fmt::Error)) // insufficient arguments for given format
419 }
420
421 Item::Error => Err(fmt::Error),
422 }
423}
424
425#[cfg(any(feature = "alloc", feature = "serde", feature = "rustc-serialize"))]
426impl OffsetFormat {
427 /// Writes an offset from UTC with the format defined by `self`.
428 fn format(&self, w: &mut impl Write, off: FixedOffset) -> fmt::Result {
429 let off = off.local_minus_utc();
430 if self.allow_zulu && off == 0 {
431 w.write_char('Z')?;
432 return Ok(());
433 }
434 let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) };
435
436 let hours;
437 let mut mins = 0;
438 let mut secs = 0;
439 let precision = match self.precision {
440 OffsetPrecision::Hours => {
441 // Minutes and seconds are simply truncated
442 hours = (off / 3600) as u8;
443 OffsetPrecision::Hours
444 }
445 OffsetPrecision::Minutes | OffsetPrecision::OptionalMinutes => {
446 // Round seconds to the nearest minute.
447 let minutes = (off + 30) / 60;
448 mins = (minutes % 60) as u8;
449 hours = (minutes / 60) as u8;
450 if self.precision == OffsetPrecision::OptionalMinutes && mins == 0 {
451 OffsetPrecision::Hours
452 } else {
453 OffsetPrecision::Minutes
454 }
455 }
456 OffsetPrecision::Seconds
457 | OffsetPrecision::OptionalSeconds
458 | OffsetPrecision::OptionalMinutesAndSeconds => {
459 let minutes = off / 60;
460 secs = (off % 60) as u8;
461 mins = (minutes % 60) as u8;
462 hours = (minutes / 60) as u8;
463 if self.precision != OffsetPrecision::Seconds && secs == 0 {
464 if self.precision == OffsetPrecision::OptionalMinutesAndSeconds && mins == 0 {
465 OffsetPrecision::Hours
466 } else {
467 OffsetPrecision::Minutes
468 }
469 } else {
470 OffsetPrecision::Seconds
471 }
472 }
473 };
474 let colons = self.colons == Colons::Colon;
475
476 if hours < 10 {
477 if self.padding == Pad::Space {
478 w.write_char(' ')?;
479 }
480 w.write_char(sign)?;
481 if self.padding == Pad::Zero {
482 w.write_char('0')?;
483 }
484 w.write_char((b'0' + hours) as char)?;
485 } else {
486 w.write_char(sign)?;
487 write_hundreds(w, hours)?;
488 }
489 if let OffsetPrecision::Minutes | OffsetPrecision::Seconds = precision {
490 if colons {
491 w.write_char(':')?;
492 }
493 write_hundreds(w, mins)?;
494 }
495 if let OffsetPrecision::Seconds = precision {
496 if colons {
497 w.write_char(':')?;
498 }
499 write_hundreds(w, secs)?;
500 }
501 Ok(())
502 }
503}
504
505/// Specific formatting options for seconds. This may be extended in the
506/// future, so exhaustive matching in external code is not recommended.
507///
508/// See the `TimeZone::to_rfc3339_opts` function for usage.
509#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
510#[allow(clippy::manual_non_exhaustive)]
511pub enum SecondsFormat {
512 /// Format whole seconds only, with no decimal point nor subseconds.
513 Secs,
514
515 /// Use fixed 3 subsecond digits. This corresponds to [Fixed::Nanosecond3].
516 Millis,
517
518 /// Use fixed 6 subsecond digits. This corresponds to [Fixed::Nanosecond6].
519 Micros,
520
521 /// Use fixed 9 subsecond digits. This corresponds to [Fixed::Nanosecond9].
522 Nanos,
523
524 /// Automatically select one of `Secs`, `Millis`, `Micros`, or `Nanos` to display all available
525 /// non-zero sub-second digits. This corresponds to [Fixed::Nanosecond].
526 AutoSi,
527
528 // Do not match against this.
529 #[doc(hidden)]
530 __NonExhaustive,
531}
532
533/// Writes the date, time and offset to the string. same as `%Y-%m-%dT%H:%M:%S%.f%:z`
534#[inline]
535#[cfg(any(feature = "alloc", feature = "serde", feature = "rustc-serialize"))]
536pub(crate) fn write_rfc3339(
537 w: &mut impl Write,
538 dt: NaiveDateTime,
539 off: FixedOffset,
540 secform: SecondsFormat,
541 use_z: bool,
542) -> fmt::Result {
543 let year = dt.date().year();
544 if (0..=9999).contains(&year) {
545 write_hundreds(w, (year / 100) as u8)?;
546 write_hundreds(w, (year % 100) as u8)?;
547 } else {
548 // ISO 8601 requires the explicit sign for out-of-range years
549 write!(w, "{:+05}", year)?;
550 }
551 w.write_char('-')?;
552 write_hundreds(w, dt.date().month() as u8)?;
553 w.write_char('-')?;
554 write_hundreds(w, dt.date().day() as u8)?;
555
556 w.write_char('T')?;
557
558 let (hour, min, mut sec) = dt.time().hms();
559 let mut nano = dt.nanosecond();
560 if nano >= 1_000_000_000 {
561 sec += 1;
562 nano -= 1_000_000_000;
563 }
564 write_hundreds(w, hour as u8)?;
565 w.write_char(':')?;
566 write_hundreds(w, min as u8)?;
567 w.write_char(':')?;
568 let sec = sec;
569 write_hundreds(w, sec as u8)?;
570
571 match secform {
572 SecondsFormat::Secs => {}
573 SecondsFormat::Millis => write!(w, ".{:03}", nano / 1_000_000)?,
574 SecondsFormat::Micros => write!(w, ".{:06}", nano / 1000)?,
575 SecondsFormat::Nanos => write!(w, ".{:09}", nano)?,
576 SecondsFormat::AutoSi => {
577 if nano == 0 {
578 } else if nano % 1_000_000 == 0 {
579 write!(w, ".{:03}", nano / 1_000_000)?
580 } else if nano % 1_000 == 0 {
581 write!(w, ".{:06}", nano / 1_000)?
582 } else {
583 write!(w, ".{:09}", nano)?
584 }
585 }
586 SecondsFormat::__NonExhaustive => unreachable!(),
587 };
588
589 OffsetFormat {
590 precision: OffsetPrecision::Minutes,
591 colons: Colons::Colon,
592 allow_zulu: use_z,
593 padding: Pad::Zero,
594 }
595 .format(w, off)
596}
597
598#[cfg(feature = "alloc")]
599/// write datetimes like `Tue, 1 Jul 2003 10:52:37 +0200`, same as `%a, %d %b %Y %H:%M:%S %z`
600pub(crate) fn write_rfc2822(
601 w: &mut impl Write,
602 dt: NaiveDateTime,
603 off: FixedOffset,
604) -> fmt::Result {
605 let year = dt.year();
606 // RFC2822 is only defined on years 0 through 9999
607 if !(0..=9999).contains(&year) {
608 return Err(fmt::Error);
609 }
610
611 let english = default_locale();
612
613 w.write_str(short_weekdays(english)[dt.weekday().num_days_from_sunday() as usize])?;
614 w.write_str(", ")?;
615 let day = dt.day();
616 if day < 10 {
617 w.write_char((b'0' + day as u8) as char)?;
618 } else {
619 write_hundreds(w, day as u8)?;
620 }
621 w.write_char(' ')?;
622 w.write_str(short_months(english)[dt.month0() as usize])?;
623 w.write_char(' ')?;
624 write_hundreds(w, (year / 100) as u8)?;
625 write_hundreds(w, (year % 100) as u8)?;
626 w.write_char(' ')?;
627
628 let (hour, min, sec) = dt.time().hms();
629 write_hundreds(w, hour as u8)?;
630 w.write_char(':')?;
631 write_hundreds(w, min as u8)?;
632 w.write_char(':')?;
633 let sec = sec + dt.nanosecond() / 1_000_000_000;
634 write_hundreds(w, sec as u8)?;
635 w.write_char(' ')?;
636 OffsetFormat {
637 precision: OffsetPrecision::Minutes,
638 colons: Colons::None,
639 allow_zulu: false,
640 padding: Pad::Zero,
641 }
642 .format(w, off)
643}
644
645/// Equivalent to `{:02}` formatting for n < 100.
646pub(crate) fn write_hundreds(w: &mut impl Write, n: u8) -> fmt::Result {
647 if n >= 100 {
648 return Err(fmt::Error);
649 }
650
651 let tens: u8 = b'0' + n / 10;
652 let ones: u8 = b'0' + n % 10;
653 w.write_char(tens as char)?;
654 w.write_char(ones as char)
655}
656
657#[cfg(test)]
658#[cfg(feature = "alloc")]
659mod tests {
660 use super::{Colons, OffsetFormat, OffsetPrecision, Pad};
661 use crate::FixedOffset;
662 #[cfg(feature = "alloc")]
663 use crate::{NaiveDate, NaiveTime, TimeZone, Timelike, Utc};
664
665 #[test]
666 #[cfg(feature = "alloc")]
667 fn test_date_format() {
668 let d = NaiveDate::from_ymd_opt(2012, 3, 4).unwrap();
669 assert_eq!(d.format("%Y,%C,%y,%G,%g").to_string(), "2012,20,12,2012,12");
670 assert_eq!(d.format("%m,%b,%h,%B").to_string(), "03,Mar,Mar,March");
671 assert_eq!(d.format("%d,%e").to_string(), "04, 4");
672 assert_eq!(d.format("%U,%W,%V").to_string(), "10,09,09");
673 assert_eq!(d.format("%a,%A,%w,%u").to_string(), "Sun,Sunday,0,7");
674 assert_eq!(d.format("%j").to_string(), "064"); // since 2012 is a leap year
675 assert_eq!(d.format("%D,%x").to_string(), "03/04/12,03/04/12");
676 assert_eq!(d.format("%F").to_string(), "2012-03-04");
677 assert_eq!(d.format("%v").to_string(), " 4-Mar-2012");
678 assert_eq!(d.format("%t%n%%%n%t").to_string(), "\t\n%\n\t");
679
680 // non-four-digit years
681 assert_eq!(
682 NaiveDate::from_ymd_opt(12345, 1, 1).unwrap().format("%Y").to_string(),
683 "+12345"
684 );
685 assert_eq!(NaiveDate::from_ymd_opt(1234, 1, 1).unwrap().format("%Y").to_string(), "1234");
686 assert_eq!(NaiveDate::from_ymd_opt(123, 1, 1).unwrap().format("%Y").to_string(), "0123");
687 assert_eq!(NaiveDate::from_ymd_opt(12, 1, 1).unwrap().format("%Y").to_string(), "0012");
688 assert_eq!(NaiveDate::from_ymd_opt(1, 1, 1).unwrap().format("%Y").to_string(), "0001");
689 assert_eq!(NaiveDate::from_ymd_opt(0, 1, 1).unwrap().format("%Y").to_string(), "0000");
690 assert_eq!(NaiveDate::from_ymd_opt(-1, 1, 1).unwrap().format("%Y").to_string(), "-0001");
691 assert_eq!(NaiveDate::from_ymd_opt(-12, 1, 1).unwrap().format("%Y").to_string(), "-0012");
692 assert_eq!(NaiveDate::from_ymd_opt(-123, 1, 1).unwrap().format("%Y").to_string(), "-0123");
693 assert_eq!(NaiveDate::from_ymd_opt(-1234, 1, 1).unwrap().format("%Y").to_string(), "-1234");
694 assert_eq!(
695 NaiveDate::from_ymd_opt(-12345, 1, 1).unwrap().format("%Y").to_string(),
696 "-12345"
697 );
698
699 // corner cases
700 assert_eq!(
701 NaiveDate::from_ymd_opt(2007, 12, 31).unwrap().format("%G,%g,%U,%W,%V").to_string(),
702 "2008,08,52,53,01"
703 );
704 assert_eq!(
705 NaiveDate::from_ymd_opt(2010, 1, 3).unwrap().format("%G,%g,%U,%W,%V").to_string(),
706 "2009,09,01,00,53"
707 );
708 }
709
710 #[test]
711 #[cfg(feature = "alloc")]
712 fn test_time_format() {
713 let t = NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap();
714 assert_eq!(t.format("%H,%k,%I,%l,%P,%p").to_string(), "03, 3,03, 3,am,AM");
715 assert_eq!(t.format("%M").to_string(), "05");
716 assert_eq!(t.format("%S,%f,%.f").to_string(), "07,098765432,.098765432");
717 assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".098,.098765,.098765432");
718 assert_eq!(t.format("%R").to_string(), "03:05");
719 assert_eq!(t.format("%T,%X").to_string(), "03:05:07,03:05:07");
720 assert_eq!(t.format("%r").to_string(), "03:05:07 AM");
721 assert_eq!(t.format("%t%n%%%n%t").to_string(), "\t\n%\n\t");
722
723 let t = NaiveTime::from_hms_micro_opt(3, 5, 7, 432100).unwrap();
724 assert_eq!(t.format("%S,%f,%.f").to_string(), "07,432100000,.432100");
725 assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".432,.432100,.432100000");
726
727 let t = NaiveTime::from_hms_milli_opt(3, 5, 7, 210).unwrap();
728 assert_eq!(t.format("%S,%f,%.f").to_string(), "07,210000000,.210");
729 assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".210,.210000,.210000000");
730
731 let t = NaiveTime::from_hms_opt(3, 5, 7).unwrap();
732 assert_eq!(t.format("%S,%f,%.f").to_string(), "07,000000000,");
733 assert_eq!(t.format("%.3f,%.6f,%.9f").to_string(), ".000,.000000,.000000000");
734
735 // corner cases
736 assert_eq!(
737 NaiveTime::from_hms_opt(13, 57, 9).unwrap().format("%r").to_string(),
738 "01:57:09 PM"
739 );
740 assert_eq!(
741 NaiveTime::from_hms_milli_opt(23, 59, 59, 1_000).unwrap().format("%X").to_string(),
742 "23:59:60"
743 );
744 }
745
746 #[test]
747 #[cfg(feature = "alloc")]
748 fn test_datetime_format() {
749 let dt =
750 NaiveDate::from_ymd_opt(2010, 9, 8).unwrap().and_hms_milli_opt(7, 6, 54, 321).unwrap();
751 assert_eq!(dt.format("%c").to_string(), "Wed Sep 8 07:06:54 2010");
752 assert_eq!(dt.format("%s").to_string(), "1283929614");
753 assert_eq!(dt.format("%t%n%%%n%t").to_string(), "\t\n%\n\t");
754
755 // a horror of leap second: coming near to you.
756 let dt = NaiveDate::from_ymd_opt(2012, 6, 30)
757 .unwrap()
758 .and_hms_milli_opt(23, 59, 59, 1_000)
759 .unwrap();
760 assert_eq!(dt.format("%c").to_string(), "Sat Jun 30 23:59:60 2012");
761 assert_eq!(dt.format("%s").to_string(), "1341100799"); // not 1341100800, it's intentional.
762 }
763
764 #[test]
765 #[cfg(feature = "alloc")]
766 fn test_datetime_format_alignment() {
767 let datetime = Utc
768 .with_ymd_and_hms(2007, 1, 2, 12, 34, 56)
769 .unwrap()
770 .with_nanosecond(123456789)
771 .unwrap();
772
773 // Item::Literal, odd number of padding bytes.
774 let percent = datetime.format("%%");
775 assert_eq!(" %", format!("{:>4}", percent));
776 assert_eq!("% ", format!("{:<4}", percent));
777 assert_eq!(" % ", format!("{:^4}", percent));
778
779 // Item::Numeric, custom non-ASCII padding character
780 let year = datetime.format("%Y");
781 assert_eq!("——2007", format!("{:—>6}", year));
782 assert_eq!("2007——", format!("{:—<6}", year));
783 assert_eq!("—2007—", format!("{:—^6}", year));
784
785 // Item::Fixed
786 let tz = datetime.format("%Z");
787 assert_eq!(" UTC", format!("{:>5}", tz));
788 assert_eq!("UTC ", format!("{:<5}", tz));
789 assert_eq!(" UTC ", format!("{:^5}", tz));
790
791 // [Item::Numeric, Item::Space, Item::Literal, Item::Space, Item::Numeric]
792 let ymd = datetime.format("%Y %B %d");
793 assert_eq!(" 2007 January 02", format!("{:>17}", ymd));
794 assert_eq!("2007 January 02 ", format!("{:<17}", ymd));
795 assert_eq!(" 2007 January 02 ", format!("{:^17}", ymd));
796
797 // Truncated
798 let time = datetime.format("%T%.6f");
799 assert_eq!("12:34:56.1234", format!("{:.13}", time));
800 }
801
802 #[test]
803 fn test_offset_formatting() {
804 fn check_all(precision: OffsetPrecision, expected: [[&str; 7]; 12]) {
805 fn check(
806 precision: OffsetPrecision,
807 colons: Colons,
808 padding: Pad,
809 allow_zulu: bool,
810 offsets: [FixedOffset; 7],
811 expected: [&str; 7],
812 ) {
813 let offset_format = OffsetFormat { precision, colons, allow_zulu, padding };
814 for (offset, expected) in offsets.iter().zip(expected.iter()) {
815 let mut output = String::new();
816 offset_format.format(&mut output, *offset).unwrap();
817 assert_eq!(&output, expected);
818 }
819 }
820 // +03:45, -03:30, +11:00, -11:00:22, +02:34:26, -12:34:30, +00:00
821 let offsets = [
822 FixedOffset::east_opt(13_500).unwrap(),
823 FixedOffset::east_opt(-12_600).unwrap(),
824 FixedOffset::east_opt(39_600).unwrap(),
825 FixedOffset::east_opt(-39_622).unwrap(),
826 FixedOffset::east_opt(9266).unwrap(),
827 FixedOffset::east_opt(-45270).unwrap(),
828 FixedOffset::east_opt(0).unwrap(),
829 ];
830 check(precision, Colons::Colon, Pad::Zero, false, offsets, expected[0]);
831 check(precision, Colons::Colon, Pad::Zero, true, offsets, expected[1]);
832 check(precision, Colons::Colon, Pad::Space, false, offsets, expected[2]);
833 check(precision, Colons::Colon, Pad::Space, true, offsets, expected[3]);
834 check(precision, Colons::Colon, Pad::None, false, offsets, expected[4]);
835 check(precision, Colons::Colon, Pad::None, true, offsets, expected[5]);
836 check(precision, Colons::None, Pad::Zero, false, offsets, expected[6]);
837 check(precision, Colons::None, Pad::Zero, true, offsets, expected[7]);
838 check(precision, Colons::None, Pad::Space, false, offsets, expected[8]);
839 check(precision, Colons::None, Pad::Space, true, offsets, expected[9]);
840 check(precision, Colons::None, Pad::None, false, offsets, expected[10]);
841 check(precision, Colons::None, Pad::None, true, offsets, expected[11]);
842 // `Colons::Maybe` should format the same as `Colons::None`
843 check(precision, Colons::Maybe, Pad::Zero, false, offsets, expected[6]);
844 check(precision, Colons::Maybe, Pad::Zero, true, offsets, expected[7]);
845 check(precision, Colons::Maybe, Pad::Space, false, offsets, expected[8]);
846 check(precision, Colons::Maybe, Pad::Space, true, offsets, expected[9]);
847 check(precision, Colons::Maybe, Pad::None, false, offsets, expected[10]);
848 check(precision, Colons::Maybe, Pad::None, true, offsets, expected[11]);
849 }
850 check_all(
851 OffsetPrecision::Hours,
852 [
853 ["+03", "-03", "+11", "-11", "+02", "-12", "+00"],
854 ["+03", "-03", "+11", "-11", "+02", "-12", "Z"],
855 [" +3", " -3", "+11", "-11", " +2", "-12", " +0"],
856 [" +3", " -3", "+11", "-11", " +2", "-12", "Z"],
857 ["+3", "-3", "+11", "-11", "+2", "-12", "+0"],
858 ["+3", "-3", "+11", "-11", "+2", "-12", "Z"],
859 ["+03", "-03", "+11", "-11", "+02", "-12", "+00"],
860 ["+03", "-03", "+11", "-11", "+02", "-12", "Z"],
861 [" +3", " -3", "+11", "-11", " +2", "-12", " +0"],
862 [" +3", " -3", "+11", "-11", " +2", "-12", "Z"],
863 ["+3", "-3", "+11", "-11", "+2", "-12", "+0"],
864 ["+3", "-3", "+11", "-11", "+2", "-12", "Z"],
865 ],
866 );
867 check_all(
868 OffsetPrecision::Minutes,
869 [
870 ["+03:45", "-03:30", "+11:00", "-11:00", "+02:34", "-12:35", "+00:00"],
871 ["+03:45", "-03:30", "+11:00", "-11:00", "+02:34", "-12:35", "Z"],
872 [" +3:45", " -3:30", "+11:00", "-11:00", " +2:34", "-12:35", " +0:00"],
873 [" +3:45", " -3:30", "+11:00", "-11:00", " +2:34", "-12:35", "Z"],
874 ["+3:45", "-3:30", "+11:00", "-11:00", "+2:34", "-12:35", "+0:00"],
875 ["+3:45", "-3:30", "+11:00", "-11:00", "+2:34", "-12:35", "Z"],
876 ["+0345", "-0330", "+1100", "-1100", "+0234", "-1235", "+0000"],
877 ["+0345", "-0330", "+1100", "-1100", "+0234", "-1235", "Z"],
878 [" +345", " -330", "+1100", "-1100", " +234", "-1235", " +000"],
879 [" +345", " -330", "+1100", "-1100", " +234", "-1235", "Z"],
880 ["+345", "-330", "+1100", "-1100", "+234", "-1235", "+000"],
881 ["+345", "-330", "+1100", "-1100", "+234", "-1235", "Z"],
882 ],
883 );
884 #[rustfmt::skip]
885 check_all(
886 OffsetPrecision::Seconds,
887 [
888 ["+03:45:00", "-03:30:00", "+11:00:00", "-11:00:22", "+02:34:26", "-12:34:30", "+00:00:00"],
889 ["+03:45:00", "-03:30:00", "+11:00:00", "-11:00:22", "+02:34:26", "-12:34:30", "Z"],
890 [" +3:45:00", " -3:30:00", "+11:00:00", "-11:00:22", " +2:34:26", "-12:34:30", " +0:00:00"],
891 [" +3:45:00", " -3:30:00", "+11:00:00", "-11:00:22", " +2:34:26", "-12:34:30", "Z"],
892 ["+3:45:00", "-3:30:00", "+11:00:00", "-11:00:22", "+2:34:26", "-12:34:30", "+0:00:00"],
893 ["+3:45:00", "-3:30:00", "+11:00:00", "-11:00:22", "+2:34:26", "-12:34:30", "Z"],
894 ["+034500", "-033000", "+110000", "-110022", "+023426", "-123430", "+000000"],
895 ["+034500", "-033000", "+110000", "-110022", "+023426", "-123430", "Z"],
896 [" +34500", " -33000", "+110000", "-110022", " +23426", "-123430", " +00000"],
897 [" +34500", " -33000", "+110000", "-110022", " +23426", "-123430", "Z"],
898 ["+34500", "-33000", "+110000", "-110022", "+23426", "-123430", "+00000"],
899 ["+34500", "-33000", "+110000", "-110022", "+23426", "-123430", "Z"],
900 ],
901 );
902 check_all(
903 OffsetPrecision::OptionalMinutes,
904 [
905 ["+03:45", "-03:30", "+11", "-11", "+02:34", "-12:35", "+00"],
906 ["+03:45", "-03:30", "+11", "-11", "+02:34", "-12:35", "Z"],
907 [" +3:45", " -3:30", "+11", "-11", " +2:34", "-12:35", " +0"],
908 [" +3:45", " -3:30", "+11", "-11", " +2:34", "-12:35", "Z"],
909 ["+3:45", "-3:30", "+11", "-11", "+2:34", "-12:35", "+0"],
910 ["+3:45", "-3:30", "+11", "-11", "+2:34", "-12:35", "Z"],
911 ["+0345", "-0330", "+11", "-11", "+0234", "-1235", "+00"],
912 ["+0345", "-0330", "+11", "-11", "+0234", "-1235", "Z"],
913 [" +345", " -330", "+11", "-11", " +234", "-1235", " +0"],
914 [" +345", " -330", "+11", "-11", " +234", "-1235", "Z"],
915 ["+345", "-330", "+11", "-11", "+234", "-1235", "+0"],
916 ["+345", "-330", "+11", "-11", "+234", "-1235", "Z"],
917 ],
918 );
919 check_all(
920 OffsetPrecision::OptionalSeconds,
921 [
922 ["+03:45", "-03:30", "+11:00", "-11:00:22", "+02:34:26", "-12:34:30", "+00:00"],
923 ["+03:45", "-03:30", "+11:00", "-11:00:22", "+02:34:26", "-12:34:30", "Z"],
924 [" +3:45", " -3:30", "+11:00", "-11:00:22", " +2:34:26", "-12:34:30", " +0:00"],
925 [" +3:45", " -3:30", "+11:00", "-11:00:22", " +2:34:26", "-12:34:30", "Z"],
926 ["+3:45", "-3:30", "+11:00", "-11:00:22", "+2:34:26", "-12:34:30", "+0:00"],
927 ["+3:45", "-3:30", "+11:00", "-11:00:22", "+2:34:26", "-12:34:30", "Z"],
928 ["+0345", "-0330", "+1100", "-110022", "+023426", "-123430", "+0000"],
929 ["+0345", "-0330", "+1100", "-110022", "+023426", "-123430", "Z"],
930 [" +345", " -330", "+1100", "-110022", " +23426", "-123430", " +000"],
931 [" +345", " -330", "+1100", "-110022", " +23426", "-123430", "Z"],
932 ["+345", "-330", "+1100", "-110022", "+23426", "-123430", "+000"],
933 ["+345", "-330", "+1100", "-110022", "+23426", "-123430", "Z"],
934 ],
935 );
936 check_all(
937 OffsetPrecision::OptionalMinutesAndSeconds,
938 [
939 ["+03:45", "-03:30", "+11", "-11:00:22", "+02:34:26", "-12:34:30", "+00"],
940 ["+03:45", "-03:30", "+11", "-11:00:22", "+02:34:26", "-12:34:30", "Z"],
941 [" +3:45", " -3:30", "+11", "-11:00:22", " +2:34:26", "-12:34:30", " +0"],
942 [" +3:45", " -3:30", "+11", "-11:00:22", " +2:34:26", "-12:34:30", "Z"],
943 ["+3:45", "-3:30", "+11", "-11:00:22", "+2:34:26", "-12:34:30", "+0"],
944 ["+3:45", "-3:30", "+11", "-11:00:22", "+2:34:26", "-12:34:30", "Z"],
945 ["+0345", "-0330", "+11", "-110022", "+023426", "-123430", "+00"],
946 ["+0345", "-0330", "+11", "-110022", "+023426", "-123430", "Z"],
947 [" +345", " -330", "+11", "-110022", " +23426", "-123430", " +0"],
948 [" +345", " -330", "+11", "-110022", " +23426", "-123430", "Z"],
949 ["+345", "-330", "+11", "-110022", "+23426", "-123430", "+0"],
950 ["+345", "-330", "+11", "-110022", "+23426", "-123430", "Z"],
951 ],
952 );
953 }
954}
955