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
10pub 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 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 #[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 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 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 '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 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 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
181enum Padding {
183 Default,
185 Zero,
187 Space,
189 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}