use crate::date::Date;
use crate::date_error::{DateError, DateErrorKind};
use crate::date_time::DateTime;
use crate::time::Time;
#[derive(Debug, Clone)]
pub struct FormatString {
parts: Vec<FormatPart>,
}
#[derive(Debug, Clone)]
enum FormatPart {
Literal(String),
Specifier(FormatSpecifier),
}
#[derive(Debug, Clone)]
enum FormatSpecifier {
Year, YearShort, Month, MonthName, MonthNameShort, Day, DayOfYear, Weekday, WeekdayShort, WeekdayNum,
Hour24, Hour12, Minute, Second, Microsecond, FractionalSecond(u8), AmPm,
Timezone, TimezoneColon,
Date, Time, DateTime, Iso8601, }
impl FormatString {
pub fn new(format: &str) -> Result<Self, DateError> {
let mut parts = Vec::new();
let mut chars = format.chars().peekable();
let mut literal = String::new();
while let Some(ch) = chars.next() {
if ch == '%' {
if !literal.is_empty() {
parts.push(FormatPart::Literal(literal.clone()));
literal.clear();
}
let spec = match chars.next() {
Some('Y') => FormatSpecifier::Year,
Some('y') => FormatSpecifier::YearShort,
Some('m') => FormatSpecifier::Month,
Some('B') => FormatSpecifier::MonthName,
Some('b') => FormatSpecifier::MonthNameShort,
Some('d') => FormatSpecifier::Day,
Some('j') => FormatSpecifier::DayOfYear,
Some('A') => FormatSpecifier::Weekday,
Some('a') => FormatSpecifier::WeekdayShort,
Some('w') => FormatSpecifier::WeekdayNum,
Some('H') => FormatSpecifier::Hour24,
Some('I') => FormatSpecifier::Hour12,
Some('M') => FormatSpecifier::Minute,
Some('S') => FormatSpecifier::Second,
Some('.') => {
let mut precision = 0u8;
let mut temp_chars = chars.clone();
let mut digit_count = 0;
while let Some(&digit) = temp_chars.peek() {
if digit.is_ascii_digit() {
precision = precision * 10 + (digit as u8 - b'0');
temp_chars.next();
digit_count += 1;
} else {
break;
}
}
if temp_chars.next() == Some('f') {
for _ in 0..digit_count {
chars.next();
}
chars.next();
if precision >= 1 && precision <= 6 {
FormatSpecifier::FractionalSecond(precision)
} else {
FormatSpecifier::Microsecond
}
} else {
literal.push('.');
continue;
}
}
Some('f') => FormatSpecifier::Microsecond,
Some('p') => FormatSpecifier::AmPm,
Some('z') => FormatSpecifier::Timezone,
Some(':') if chars.peek() == Some(&'z') => {
chars.next(); FormatSpecifier::TimezoneColon
}
Some('D') => FormatSpecifier::Date,
Some('T') => FormatSpecifier::Time,
Some('F') => FormatSpecifier::DateTime,
Some('+') => FormatSpecifier::Iso8601,
Some('%') => {
literal.push('%');
continue;
}
Some(_) => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
None => return Err(DateErrorKind::WrongDateTimeStringFormat.into()),
};
parts.push(FormatPart::Specifier(spec));
} else {
literal.push(ch);
}
}
if !literal.is_empty() {
parts.push(FormatPart::Literal(literal));
}
Ok(FormatString { parts })
}
pub fn format_datetime(&self, dt: &DateTime) -> String {
let mut result = String::new();
for part in &self.parts {
match part {
FormatPart::Literal(s) => result.push_str(s),
FormatPart::Specifier(spec) => {
result.push_str(&self.format_specifier(spec, dt));
}
}
}
result
}
pub fn format_date(&self, date: &Date) -> String {
let mut result = String::new();
for part in &self.parts {
match part {
FormatPart::Literal(s) => result.push_str(s),
FormatPart::Specifier(spec) => {
result.push_str(&self.format_specifier_date(spec, date));
}
}
}
result
}
pub fn format_time(&self, time: &Time) -> String {
let mut result = String::new();
for part in &self.parts {
match part {
FormatPart::Literal(s) => result.push_str(s),
FormatPart::Specifier(spec) => {
result.push_str(&self.format_specifier_time(spec, time));
}
}
}
result
}
fn format_specifier(&self, spec: &FormatSpecifier, dt: &DateTime) -> String {
match spec {
FormatSpecifier::Year => format!("{:04}", dt.date.year),
FormatSpecifier::YearShort => format!("{:02}", dt.date.year % 100),
FormatSpecifier::Month => format!("{:02}", dt.date.month),
FormatSpecifier::MonthName => self.month_name(dt.date.month),
FormatSpecifier::MonthNameShort => self.month_name_short(dt.date.month),
FormatSpecifier::Day => format!("{:02}", dt.date.day),
FormatSpecifier::DayOfYear => format!("{:03}", dt.date.year_day()),
FormatSpecifier::Weekday => self.weekday_name(&dt.date),
FormatSpecifier::WeekdayShort => self.weekday_name_short(&dt.date),
FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(&dt.date)),
FormatSpecifier::Hour24 => format!("{:02}", dt.time.hour),
FormatSpecifier::Hour12 => {
let hour = if dt.time.hour == 0 {
12
} else if dt.time.hour > 12 {
dt.time.hour - 12
} else {
dt.time.hour
};
format!("{:02}", hour)
}
FormatSpecifier::Minute => format!("{:02}", dt.time.minute),
FormatSpecifier::Second => format!("{:02}", dt.time.second),
FormatSpecifier::Microsecond => format!(".{:06}", dt.time.microsecond),
FormatSpecifier::FractionalSecond(precision) => {
let divisor = 10u64.pow(6 - *precision as u32);
let value = dt.time.microsecond / divisor;
format!(".{:0width$}", value, width = *precision as usize)
}
FormatSpecifier::AmPm => if dt.time.hour < 12 { "AM" } else { "PM" }.to_string(),
FormatSpecifier::Timezone => self.format_timezone(dt.shift_minutes, false),
FormatSpecifier::TimezoneColon => self.format_timezone(dt.shift_minutes, true),
FormatSpecifier::Date => format!(
"{:02}/{:02}/{:02}",
dt.date.month,
dt.date.day,
dt.date.year % 100
),
FormatSpecifier::Time => format!(
"{:02}:{:02}:{:02}",
dt.time.hour, dt.time.minute, dt.time.second
),
FormatSpecifier::DateTime => format!(
"{:04}-{:02}-{:02}",
dt.date.year, dt.date.month, dt.date.day
),
FormatSpecifier::Iso8601 => dt.to_iso_8061(),
}
}
fn format_specifier_date(&self, spec: &FormatSpecifier, date: &Date) -> String {
match spec {
FormatSpecifier::Year => format!("{:04}", date.year),
FormatSpecifier::YearShort => format!("{:02}", date.year % 100),
FormatSpecifier::Month => format!("{:02}", date.month),
FormatSpecifier::MonthName => self.month_name(date.month),
FormatSpecifier::MonthNameShort => self.month_name_short(date.month),
FormatSpecifier::Day => format!("{:02}", date.day),
FormatSpecifier::DayOfYear => format!("{:03}", date.year_day()),
FormatSpecifier::Weekday => self.weekday_name(date),
FormatSpecifier::WeekdayShort => self.weekday_name_short(date),
FormatSpecifier::WeekdayNum => format!("{}", self.weekday_number(date)),
FormatSpecifier::DateTime => {
format!("{:04}-{:02}-{:02}", date.year, date.month, date.day)
}
FormatSpecifier::Date => {
format!("{:02}/{:02}/{:02}", date.month, date.day, date.year % 100)
}
_ => String::new(), }
}
fn format_specifier_time(&self, spec: &FormatSpecifier, time: &Time) -> String {
match spec {
FormatSpecifier::Hour24 => format!("{:02}", time.hour),
FormatSpecifier::Hour12 => {
let hour = if time.hour == 0 {
12
} else if time.hour > 12 {
time.hour - 12
} else {
time.hour
};
format!("{:02}", hour)
}
FormatSpecifier::Minute => format!("{:02}", time.minute),
FormatSpecifier::Second => format!("{:02}", time.second),
FormatSpecifier::Microsecond => format!(".{:06}", time.microsecond),
FormatSpecifier::FractionalSecond(precision) => {
let divisor = 10u64.pow(6 - *precision as u32);
let value = time.microsecond / divisor;
format!(".{:0width$}", value, width = *precision as usize)
}
FormatSpecifier::AmPm => if time.hour < 12 { "AM" } else { "PM" }.to_string(),
FormatSpecifier::Time => {
format!("{:02}:{:02}:{:02}", time.hour, time.minute, time.second)
}
_ => String::new(), }
}
fn month_name(&self, month: u64) -> String {
match month {
1 => "January".to_string(),
2 => "February".to_string(),
3 => "March".to_string(),
4 => "April".to_string(),
5 => "May".to_string(),
6 => "June".to_string(),
7 => "July".to_string(),
8 => "August".to_string(),
9 => "September".to_string(),
10 => "October".to_string(),
11 => "November".to_string(),
12 => "December".to_string(),
_ => "Unknown".to_string(),
}
}
fn month_name_short(&self, month: u64) -> String {
match month {
1 => "Jan".to_string(),
2 => "Feb".to_string(),
3 => "Mar".to_string(),
4 => "Apr".to_string(),
5 => "May".to_string(),
6 => "Jun".to_string(),
7 => "Jul".to_string(),
8 => "Aug".to_string(),
9 => "Sep".to_string(),
10 => "Oct".to_string(),
11 => "Nov".to_string(),
12 => "Dec".to_string(),
_ => "Unknown".to_string(),
}
}
fn weekday_name(&self, date: &Date) -> String {
if date.is_sunday() {
"Sunday".to_string()
} else if date.is_monday() {
"Monday".to_string()
} else if date.is_tuesday() {
"Tuesday".to_string()
} else if date.is_wednesday() {
"Wednesday".to_string()
} else if date.is_thursday() {
"Thursday".to_string()
} else if date.is_friday() {
"Friday".to_string()
} else if date.is_saturday() {
"Saturday".to_string()
} else {
"Unknown".to_string()
}
}
fn weekday_name_short(&self, date: &Date) -> String {
if date.is_sunday() {
"Sun".to_string()
} else if date.is_monday() {
"Mon".to_string()
} else if date.is_tuesday() {
"Tue".to_string()
} else if date.is_wednesday() {
"Wed".to_string()
} else if date.is_thursday() {
"Thu".to_string()
} else if date.is_friday() {
"Fri".to_string()
} else if date.is_saturday() {
"Sat".to_string()
} else {
"Unknown".to_string()
}
}
fn weekday_number(&self, date: &Date) -> u64 {
if date.is_sunday() {
0
} else if date.is_monday() {
1
} else if date.is_tuesday() {
2
} else if date.is_wednesday() {
3
} else if date.is_thursday() {
4
} else if date.is_friday() {
5
} else if date.is_saturday() {
6
} else {
0
}
}
fn format_timezone(&self, shift_minutes: isize, with_colon: bool) -> String {
if shift_minutes == 0 {
return "Z".to_string();
}
let abs_minutes = shift_minutes.abs() as u64;
let hours = abs_minutes / 60;
let minutes = abs_minutes % 60;
let sign = if shift_minutes > 0 { "+" } else { "-" };
if with_colon {
format!("{}{:02}:{:02}", sign, hours, minutes)
} else {
format!("{}{:02}{:02}", sign, hours, minutes)
}
}
}
pub trait Format {
fn format(&self, format: &str) -> Result<String, DateError>;
}
impl Format for DateTime {
fn format(&self, format: &str) -> Result<String, DateError> {
let format_string = FormatString::new(format)?;
Ok(format_string.format_datetime(self))
}
}
impl Format for Date {
fn format(&self, format: &str) -> Result<String, DateError> {
let format_string = FormatString::new(format)?;
Ok(format_string.format_date(self))
}
}
impl Format for Time {
fn format(&self, format: &str) -> Result<String, DateError> {
let format_string = FormatString::new(format)?;
Ok(format_string.format_time(self))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_string_parsing() {
let format = FormatString::new("%Y-%m-%d %H:%M:%S").unwrap();
assert_eq!(format.parts.len(), 11);
let format = FormatString::new("Date: %Y-%m-%d").unwrap();
assert_eq!(format.parts.len(), 6); }
#[test]
fn test_datetime_formatting() {
let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
assert_eq!(dt.format("%Y-%m-%d").unwrap(), "2023-06-15");
assert_eq!(dt.format("%H:%M:%S").unwrap(), "14:30:45");
assert_eq!(
dt.format("%Y-%m-%d %H:%M:%S").unwrap(),
"2023-06-15 14:30:45"
);
assert_eq!(dt.format("%B %d, %Y").unwrap(), "June 15, 2023");
assert_eq!(
dt.format("%A, %B %d, %Y").unwrap(),
"Thursday, June 15, 2023"
);
assert_eq!(dt.format("%I:%M %p").unwrap(), "02:30 PM");
}
#[test]
fn test_datetime_millisecond_formatting() {
let time = Time::new_with_microseconds(14, 30, 45, 123456);
let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
assert_eq!(
dt.format("%Y-%m-%d %H:%M:%S%.4f").unwrap(),
"2023-06-15 14:30:45.1234"
);
}
#[test]
fn test_fractional_second_formatting() {
let time = Time::new_with_microseconds(14, 30, 45, 123456);
let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
assert_eq!(dt.format("%H:%M:%S%.1f").unwrap(), "14:30:45.1");
assert_eq!(dt.format("%H:%M:%S%.2f").unwrap(), "14:30:45.12");
assert_eq!(dt.format("%H:%M:%S%.3f").unwrap(), "14:30:45.123");
assert_eq!(dt.format("%H:%M:%S%.4f").unwrap(), "14:30:45.1234");
assert_eq!(dt.format("%H:%M:%S%.5f").unwrap(), "14:30:45.12345");
assert_eq!(dt.format("%H:%M:%S%.6f").unwrap(), "14:30:45.123456");
assert_eq!(time.format("%H:%M:%S%.3f").unwrap(), "14:30:45.123");
assert_eq!(time.format("%H:%M:%S%.4f").unwrap(), "14:30:45.1234");
}
#[test]
fn test_fractional_second_fallback() {
let time = Time::new_with_microseconds(14, 30, 45, 123456);
let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
assert_eq!(dt.format("%H:%M:%S%.0f").unwrap(), "14:30:45.123456"); assert_eq!(dt.format("%H:%M:%S%.7f").unwrap(), "14:30:45.123456"); assert_eq!(dt.format("%H:%M:%S%.10f").unwrap(), "14:30:45.123456"); assert_eq!(dt.format("%H:%M:%S%.99f").unwrap(), "14:30:45.123456");
assert_eq!(time.format("%H:%M:%S%.0f").unwrap(), "14:30:45.123456");
assert_eq!(time.format("%H:%M:%S%.7f").unwrap(), "14:30:45.123456");
assert_eq!(time.format("%H:%M:%S%f").unwrap(), "14:30:45.123456");
}
#[test]
fn test_literal_dot_handling() {
let time = Time::new_with_microseconds(14, 30, 45, 123456);
let dt = DateTime::new(Date::new(2023, 6, 15), time, 0);
assert_eq!(dt.format("%H:%M:%S.").unwrap(), "14:30:45.");
assert_eq!(dt.format("%H:%M:%S.123").unwrap(), "14:30:45.123");
assert_eq!(dt.format("%H:%M:%S.abc").unwrap(), "14:30:45.abc");
}
#[test]
fn test_date_formatting() {
let date = Date::new(2023, 6, 15);
assert_eq!(date.format("%Y-%m-%d").unwrap(), "2023-06-15");
assert_eq!(date.format("%B %d, %Y").unwrap(), "June 15, 2023");
assert_eq!(
date.format("%A, %B %d, %Y").unwrap(),
"Thursday, June 15, 2023"
);
assert_eq!(date.format("%j").unwrap(), "166"); }
#[test]
fn test_time_formatting() {
let time = Time::new(14, 30, 45);
assert_eq!(time.format("%H:%M:%S").unwrap(), "14:30:45");
assert_eq!(time.format("%I:%M %p").unwrap(), "02:30 PM");
assert_eq!(time.format("%T").unwrap(), "14:30:45");
}
#[test]
fn test_timezone_formatting() {
let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 120);
assert_eq!(dt.format("%z").unwrap(), "+0200");
assert_eq!(dt.format("%:z").unwrap(), "+02:00");
let dt_utc = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
assert_eq!(dt_utc.format("%z").unwrap(), "Z");
}
#[test]
fn test_microsecond_formatting() {
let time = Time::new_with_microseconds(14, 30, 45, 123456);
assert_eq!(time.format("%H:%M:%S%f").unwrap(), "14:30:45.123456");
assert_eq!(time.format("%T%f").unwrap(), "14:30:45.123456");
}
#[test]
fn test_millisecond_formatting() {
let time = Time::new_with_microseconds(14, 30, 45, 123456);
assert_eq!(time.format("%H:%M:%S%.3f").unwrap(), "14:30:45.123");
assert_eq!(time.format("%T%.3f").unwrap(), "14:30:45.123");
let time2 = Time::new_with_microseconds(9, 15, 30, 50000);
assert_eq!(time2.format("%H:%M:%S%.3f").unwrap(), "09:15:30.050");
let time3 = Time::new_with_microseconds(23, 59, 59, 999000);
assert_eq!(time3.format("%H:%M:%S%.3f").unwrap(), "23:59:59.999");
}
#[test]
fn test_iso8601_formatting() {
let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
assert_eq!(dt.format("%+").unwrap(), "2023-06-15T14:30:45Z");
}
#[test]
fn test_escaped_percent() {
let format = FormatString::new("100%% complete").unwrap();
assert_eq!(format.parts.len(), 2);
let dt = DateTime::new(Date::new(2023, 6, 15), Time::new(14, 30, 45), 0);
assert_eq!(dt.format("100%% complete").unwrap(), "100% complete");
}
#[test]
fn test_invalid_format() {
assert!(FormatString::new("%").is_err());
assert!(FormatString::new("%X").is_err());
}
}