use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Error;
use std::fmt::Formatter;
use std::fmt::Result;
use std::fmt::Write;
use crate::DateTime;
pub struct FormattedDateTime<'a> {
pub(crate) dt: &'a DateTime,
pub(crate) format: &'a str,
}
impl FormattedDateTime<'_> {
fn tz_offset(&self) -> String {
#[cfg(feature = "tz")]
match self.dt.tz {
crate::tz::TimeZone::Tz(_) | crate::tz::TimeZone::FixedOffset(_) =>
format!("{:+03}{:02}", self.dt.tz_offset() / 3600, self.dt.tz_offset() % 3600 / 60,),
crate::tz::TimeZone::Unspecified => String::new(),
}
#[cfg(not(feature = "tz"))]
String::new()
}
}
impl Debug for FormattedDateTime<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
Display::fmt(self, f)
}
}
impl Display for FormattedDateTime<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let dt = self.dt;
let mut flag = false;
let mut padding = Padding::Default;
let mut prefix = None;
let mut div = 1;
for c in self.format.chars() {
if flag {
#[rustfmt::skip]
match c {
'0' => { padding = Padding::Zero; continue; },
'-' => { padding = Padding::Suppress; continue; },
'_' => { padding = Padding::Space; continue; },
'.' => { prefix = Some('.'); continue; },
'3' => { div = 1_000_000; continue; },
'6' => { div = 1_000; continue; },
'9' => { div = 1; continue; },
_ => {},
};
if c != 'f' && (div != 1 || prefix.is_some()) {
panic!("Invalid modifier; `.`, `3`, and `6` only allowed on `f` (fractional seconds).");
}
macro_rules! write_padded {
($f:ident, $pad:ident, $level:literal, $e:expr) => {
match $pad {
Padding::Default | Padding::Zero => write!($f, concat!("{:0", $level, "}"), $e),
Padding::Space => write!($f, concat!("{:", $level, "}"), $e),
Padding::Suppress => write!($f, "{}", $e),
}
};
}
flag = false;
match c {
'Y' => write_padded!(f, padding, 4, dt.year())?,
'C' => write_padded!(f, padding, 2, dt.year() / 100)?,
'y' => write_padded!(f, padding, 2, dt.year() % 100)?,
'm' => write_padded!(f, padding, 2, dt.month())?,
'b' | 'h' => write!(f, "{}", dt.month_abbv())?,
'B' => write!(f, "{}", dt.month_name())?,
'd' => write_padded!(f, padding, 2, dt.day())?,
'a' => write!(f, "{}", dt.weekday().to_string().chars().take(3).collect::<String>())?,
'A' => write!(f, "{}", dt.weekday())?,
'w' => write!(f, "{}", dt.weekday() as u8)?,
'u' => write!(f, "{}", match dt.weekday() {
crate::Weekday::Sunday => 7,
_ => self.dt.weekday() as u8,
})?,
'j' => write_padded!(f, padding, 3, dt.day_of_year())?,
'H' => write_padded!(f, padding, 2, dt.hour())?,
'I' => write_padded!(f, padding, 2, match dt.hour() {
0 => 12,
1..=12 => dt.hour(),
13.. => dt.hour() - 12,
})?,
'M' => write_padded!(f, padding, 2, dt.minute())?,
'S' => write_padded!(f, padding, 2, dt.second())?,
'z' => write!(f, "{}", self.tz_offset())?,
'P' => write!(f, "{}", if dt.hour() > 12 { "PM" } else { "AM" })?,
'p' => write!(f, "{}", if dt.hour() > 12 { "pm" } else { "am" })?,
's' => write!(f, "{}", dt.seconds)?,
'f' => {
if let Some(pre) = prefix {
f.write_char(pre)?;
}
match div {
1_000 => write!(f, "{:06}", dt.nanosecond() / div)?,
1_000_000 => write!(f, "{:03}", dt.nanosecond() / div)?,
_ => write!(f, "{:09}", dt.nanosecond())?,
};
prefix = None;
div = 1;
},
'D' => write!(f, "{:02}/{:02}/{:02}", dt.month(), dt.day(), dt.year())?,
'F' => write!(f, "{:04}-{:02}-{:02}", dt.year(), dt.month(), dt.day())?,
'v' => write!(f, "{:2}-{}-{:04}", dt.day(), dt.month_abbv(), dt.year())?,
'R' => write!(f, "{:2}:{:2}", dt.hour(), dt.minute())?,
'T' => write!(f, "{:2}:{:2}:{:2}", dt.hour(), dt.minute(), dt.second())?,
't' => f.write_char('\t')?,
'n' => f.write_char('\n')?,
'%' => f.write_char('%')?,
_ => Err(Error)?,
}
} else if c == '%' {
flag = true;
padding = Padding::Default;
} else {
f.write_char(c)?;
}
}
Ok(())
}
}
impl PartialEq<&str> for FormattedDateTime<'_> {
fn eq(&self, other: &&str) -> bool {
&self.to_string().as_str() == other
}
}
macro_rules! month_str {
($($num:literal => $short:ident ~ $long:ident)*) => {
impl DateTime {
const fn month_name(&self) -> &'static str {
match self.month() {
$($num => stringify!($long),)*
#[cfg(not(tarpaulin_include))]
_ => panic!("Fictitious month"),
}
}
const fn month_abbv(&self) -> &'static str {
match self.month() {
$($num => stringify!($short),)*
#[cfg(not(tarpaulin_include))]
_ => panic!("Fictitious month"),
}
}
}
}
}
month_str! {
1 => Jan ~ January
2 => Feb ~ February
3 => Mar ~ March
4 => Apr ~ April
5 => May ~ May
6 => Jun ~ June
7 => Jul ~ July
8 => Aug ~ August
9 => Sep ~ September
10 => Oct ~ October
11 => Nov ~ November
12 => Dec ~ December
}
enum Padding {
Default,
Zero,
Space,
Suppress,
}
#[cfg(test)]
mod tests {
use assert2::check;
#[test]
fn test_format() {
let date = datetime! { 2012-04-21 11:00:00 };
for (fmt_string, date_str) in [
("%Y-%m-%d", "2012-04-21"),
("%F", "2012-04-21"),
("%v", "21-Apr-2012"),
("%Y-%m-%d %H:%M:%S", "2012-04-21 11:00:00"),
("%Y-%m-%d %H:%M:%S%.6f", "2012-04-21 11:00:00.000000"),
("%Y-%m-%d %I:%M:%S %P", "2012-04-21 11:00:00 AM"),
("%H:%M:%S", "11:00:00"),
("%B %-d, %Y", "April 21, 2012"),
("%B %-d, %C%y", "April 21, 2012"),
("%A, %B %-d, %Y", "Saturday, April 21, 2012"),
("%d %h %Y", "21 Apr 2012"),
("%a %d %b %Y", "Sat 21 Apr 2012"),
("%m/%d/%y", "04/21/12"),
("year: %Y / day: %j", "year: 2012 / day: 112"),
("%%", "%"),
("%w %u", "6 6"),
("%t %n", "\t \n"),
] {
check!(date.format(fmt_string).to_string() == date_str);
check!(date.format(fmt_string) == date_str);
check!(format!("{:?}", date.format(fmt_string)) == date_str);
}
}
#[test]
fn test_padding() {
let date = datetime! { 2024-07-04 17:30:00 };
for (fmt_string, date_str) in
[("%Y-%m-%d", "2024-07-04"), ("%B %-d, %Y", "July 4, 2024"), ("%-d-%h-%Y", "4-Jul-2024")]
{
check!(date.format(fmt_string).to_string() == date_str);
check!(date.format(fmt_string) == date_str);
}
}
}