datetime/
format.rs

1use std::fmt::Debug;
2use std::fmt::Display;
3use std::fmt::Error;
4use std::fmt::Formatter;
5use std::fmt::Result;
6use std::fmt::Write;
7
8use crate::DateTime;
9
10/// A date with a requested format.
11pub struct FormattedDateTime<'a> {
12  pub(crate) dt: &'a DateTime,
13  pub(crate) format: &'a str,
14}
15
16impl FormattedDateTime<'_> {
17  fn tz_offset(&self) -> String {
18    #[cfg(feature = "tz")]
19    match self.dt.tz {
20      crate::tz::TimeZone::Tz(_) | crate::tz::TimeZone::FixedOffset(_) =>
21        format!("{:+03}{:02}", self.dt.tz_offset() / 3600, self.dt.tz_offset() % 3600 / 60,),
22      crate::tz::TimeZone::Unspecified => String::new(),
23    }
24    #[cfg(not(feature = "tz"))]
25    String::new()
26  }
27}
28
29impl Debug for FormattedDateTime<'_> {
30  fn fmt(&self, f: &mut Formatter<'_>) -> Result {
31    Display::fmt(self, f)
32  }
33}
34
35impl Display for FormattedDateTime<'_> {
36  fn fmt(&self, f: &mut Formatter<'_>) -> Result {
37    // Iterate over the format string and consume it.
38    let dt = self.dt;
39    let mut flag = false;
40    let mut padding = Padding::Default;
41    let mut prefix = None;
42    let mut div = 1;
43    for c in self.format.chars() {
44      if flag {
45        // Apply padding if this is a padding change.
46        #[rustfmt::skip]
47        match c {
48          '0' => { padding = Padding::Zero; continue; },
49          '-' => { padding = Padding::Suppress; continue; },
50          '_' => { padding = Padding::Space; continue; },
51          '.' => { prefix = Some('.'); continue; },
52          '3' => { div = 1_000_000; continue; },
53          '6' => { div = 1_000; continue; },
54          '9' => { div = 1; continue; },
55          _ => {},
56        };
57
58        if c != 'f' && (div != 1 || prefix.is_some()) {
59          panic!("Invalid modifier; `.`, `3`, and `6` only allowed on `f` (fractional seconds).");
60        }
61
62        // Set up a macro to process padding.
63        macro_rules! write_padded {
64          ($f:ident, $pad:ident, $level:literal, $e:expr) => {
65            match $pad {
66              Padding::Default | Padding::Zero => write!($f, concat!("{:0", $level, "}"), $e),
67              Padding::Space => write!($f, concat!("{:", $level, "}"), $e),
68              Padding::Suppress => write!($f, "{}", $e),
69            }
70          };
71        }
72
73        // Write out the formatted component.
74        flag = false;
75        match c {
76          'Y' => write_padded!(f, padding, 4, dt.year())?,
77          'C' => write_padded!(f, padding, 2, dt.year() / 100)?,
78          'y' => write_padded!(f, padding, 2, dt.year() % 100)?,
79          'm' => write_padded!(f, padding, 2, dt.month())?,
80          'b' | 'h' => write!(f, "{}", dt.month_abbv())?,
81          'B' => write!(f, "{}", dt.month_name())?,
82          'd' => write_padded!(f, padding, 2, dt.day())?,
83          'a' => write!(f, "{}", dt.weekday().to_string().chars().take(3).collect::<String>())?,
84          'A' => write!(f, "{}", dt.weekday())?,
85          'w' => write!(f, "{}", dt.weekday() as u8)?,
86          'u' => write!(f, "{}", match dt.weekday() {
87            crate::Weekday::Sunday => 7,
88            _ => self.dt.weekday() as u8,
89          })?,
90          // U, W
91          'j' => write_padded!(f, padding, 3, dt.day_of_year())?,
92          'H' => write_padded!(f, padding, 2, dt.hour())?,
93          'I' => write_padded!(f, padding, 2, match dt.hour() {
94            0 => 12,
95            1..=12 => dt.hour(),
96            13.. => dt.hour() - 12,
97          })?,
98          'M' => write_padded!(f, padding, 2, dt.minute())?,
99          'S' => write_padded!(f, padding, 2, dt.second())?,
100          'z' => write!(f, "{}", self.tz_offset())?,
101          'P' => write!(f, "{}", if dt.hour() > 12 { "PM" } else { "AM" })?,
102          'p' => write!(f, "{}", if dt.hour() > 12 { "pm" } else { "am" })?,
103          's' => write!(f, "{}", dt.seconds)?,
104          'f' => {
105            if let Some(pre) = prefix {
106              f.write_char(pre)?;
107            }
108            match div {
109              1_000 => write!(f, "{:06}", dt.nanosecond() / div)?,
110              1_000_000 => write!(f, "{:03}", dt.nanosecond() / div)?,
111              _ => write!(f, "{:09}", dt.nanosecond())?,
112            };
113            prefix = None;
114            div = 1;
115          },
116          'D' => write!(f, "{:02}/{:02}/{:02}", dt.month(), dt.day(), dt.year())?,
117          'F' => write!(f, "{:04}-{:02}-{:02}", dt.year(), dt.month(), dt.day())?,
118          'v' => write!(f, "{:2}-{}-{:04}", dt.day(), dt.month_abbv(), dt.year())?,
119          'R' => write!(f, "{:2}:{:2}", dt.hour(), dt.minute())?,
120          'T' => write!(f, "{:2}:{:2}:{:2}", dt.hour(), dt.minute(), dt.second())?,
121          't' => f.write_char('\t')?,
122          'n' => f.write_char('\n')?,
123          '%' => f.write_char('%')?,
124          _ => Err(Error)?,
125        }
126      } else if c == '%' {
127        flag = true;
128        padding = Padding::Default;
129      } else {
130        f.write_char(c)?;
131      }
132    }
133    Ok(())
134  }
135}
136
137impl PartialEq<&str> for FormattedDateTime<'_> {
138  fn eq(&self, other: &&str) -> bool {
139    &self.to_string().as_str() == other
140  }
141}
142
143macro_rules! month_str {
144  ($($num:literal => $short:ident ~ $long:ident)*) => {
145    impl DateTime {
146      /// The English name of the month.
147      const fn month_name(&self) -> &'static str {
148        match self.month() {
149          $($num => stringify!($long),)*
150          #[cfg(not(tarpaulin_include))]
151          _ => panic!("Fictitious month"),
152        }
153      }
154
155      /// The three-letter abbreviation of the month.
156      const fn month_abbv(&self) -> &'static str {
157        match self.month() {
158          $($num => stringify!($short),)*
159          #[cfg(not(tarpaulin_include))]
160          _ => panic!("Fictitious month"),
161        }
162      }
163    }
164  }
165}
166month_str! {
167   1 => Jan ~ January
168   2 => Feb ~ February
169   3 => Mar ~ March
170   4 => Apr ~ April
171   5 => May ~ May
172   6 => Jun ~ June
173   7 => Jul ~ July
174   8 => Aug ~ August
175   9 => Sep ~ September
176  10 => Oct ~ October
177  11 => Nov ~ November
178  12 => Dec ~ December
179}
180
181/// A padding modifier
182enum Padding {
183  /// Use the default padding (usually either `0` or nothing).
184  Default,
185  /// Explicitly pad with `0`
186  Zero,
187  /// Explicitly pad with ` `.
188  Space,
189  /// Explicitly prevent padding, even if the token has default padding.
190  Suppress,
191}
192
193#[cfg(test)]
194mod tests {
195  use assert2::check;
196
197  use crate::datetime;
198
199  #[test]
200  fn test_format() {
201    let date = datetime! { 2012-04-21 11:00:00 };
202    for (fmt_string, date_str) in [
203      ("%Y-%m-%d", "2012-04-21"),
204      ("%F", "2012-04-21"),
205      ("%v", "21-Apr-2012"),
206      ("%Y-%m-%d %H:%M:%S", "2012-04-21 11:00:00"),
207      ("%Y-%m-%d %H:%M:%S%.6f", "2012-04-21 11:00:00.000000"),
208      ("%Y-%m-%d %I:%M:%S %P", "2012-04-21 11:00:00 AM"),
209      ("%H:%M:%S", "11:00:00"),
210      ("%B %-d, %Y", "April 21, 2012"),
211      ("%B %-d, %C%y", "April 21, 2012"),
212      ("%A, %B %-d, %Y", "Saturday, April 21, 2012"),
213      ("%d %h %Y", "21 Apr 2012"),
214      ("%a %d %b %Y", "Sat 21 Apr 2012"),
215      ("%m/%d/%y", "04/21/12"),
216      ("year: %Y / day: %j", "year: 2012 / day: 112"),
217      ("%%", "%"),
218      ("%w %u", "6 6"),
219      ("%t %n", "\t \n"),
220    ] {
221      check!(date.format(fmt_string).to_string() == date_str);
222      check!(date.format(fmt_string) == date_str);
223      check!(format!("{:?}", date.format(fmt_string)) == date_str);
224    }
225  }
226
227  #[test]
228  fn test_padding() {
229    let date = datetime! { 2024-07-04 17:30:00 };
230    for (fmt_string, date_str) in
231      [("%Y-%m-%d", "2024-07-04"), ("%B %-d, %Y", "July 4, 2024"), ("%-d-%h-%Y", "4-Jul-2024")]
232    {
233      check!(date.format(fmt_string).to_string() == date_str);
234      check!(date.format(fmt_string) == date_str);
235    }
236  }
237}