use std::fmt;
use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DateTimeWithZone {
pub datetime: DateTime<Utc>,
pub timezone: Option<String>,
}
impl From<i32> for DateFormat {
fn from(value: i32) -> Self {
match value {
0 => DateFormat::YearMonthDaySlash,
1 => DateFormat::YearMonthDayDash,
2 => DateFormat::DayMonthYear,
3 => DateFormat::YearMonth,
4 => DateFormat::MonthDay,
5 => DateFormat::Year,
6 => DateFormat::Month,
7 => DateFormat::Day,
_ => panic!("Invalid value for DateFormat"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(into = "i32", from = "i32")]
pub enum DateFormat {
YearMonthDaySlash = 0,
YearMonthDayDash = 1,
DayMonthYear = 2,
YearMonth = 3,
MonthDay = 4,
Year = 5,
Month = 6,
Day = 7,
}
impl DateFormat {
pub fn format_with_timezone(&self, dt_with_zone: &DateTimeWithZone) -> Result<String, DateTimeError> {
if let Some(tz_str) = &dt_with_zone.timezone {
let tz: Tz = tz_str.parse()
.map_err(|_| DateTimeError::InvalidTimezone(tz_str.clone()))?;
let local_dt = dt_with_zone.datetime.with_timezone(&tz);
Ok(self.format_datetime(&local_dt.with_timezone(&Utc)))
} else {
Ok(self.format_datetime(&dt_with_zone.datetime))
}
}
pub fn parse_with_timezone(&self, date_str: &str, timezone: Option<&str>) -> Result<DateTimeWithZone, DateTimeError> {
let utc_dt = self.parse_datetime(date_str)?;
let timezone = match timezone {
Some(tz_str) => {
let _tz: Tz = tz_str.parse()
.map_err(|_| DateTimeError::InvalidTimezone(tz_str.to_string()))?;
Some(tz_str.to_string())
},
None => None
};
Ok(DateTimeWithZone {
datetime: utc_dt,
timezone,
})
}
pub fn format_timestamp(&self, timestamp: i64) -> Result<String, DateTimeError> {
let datetime = Utc
.timestamp_millis_opt(timestamp)
.single()
.ok_or_else(|| DateTimeError::InvalidTimestamp(timestamp))?;
Ok(self.format_datetime(&datetime))
}
pub fn to_timestamp(&self, date_str: &str) -> Result<i64, DateTimeError> {
let datetime = self.parse_datetime(date_str)?;
Ok(datetime.timestamp_millis())
}
pub fn get_format_string(&self) -> &'static str {
match self {
DateFormat::YearMonthDaySlash => "%Y/%m/%d",
DateFormat::YearMonthDayDash => "%Y-%m-%d",
DateFormat::DayMonthYear => "%d/%m/%Y",
DateFormat::YearMonth => "%Y-%m",
DateFormat::MonthDay => "%m-%d",
DateFormat::Year => "%Y",
DateFormat::Month => "%m",
DateFormat::Day => "%d",
}
}
pub fn format_datetime(&self, date: &DateTime<Utc>) -> String {
date.format(self.get_format_string()).to_string()
}
pub fn parse_datetime(&self, date_str: &str) -> Result<DateTime<Utc>, DateTimeError> {
let current_date = Local::now();
match self {
DateFormat::YearMonthDaySlash
| DateFormat::YearMonthDayDash
| DateFormat::DayMonthYear => {
let naive_date = NaiveDateTime::parse_from_str(
&format!("{} 00:00:00", date_str),
&format!("{} %H:%M:%S", self.get_format_string()),
)
.map_err(|_| DateTimeError::ParseError(date_str.to_string()))?;
Ok(DateTime::from_naive_utc_and_offset(naive_date, Utc))
}
DateFormat::YearMonth => {
let parts: Vec<&str> = date_str.split('-').collect();
if parts.len() != 2 {
return Err(DateTimeError::ParseError(date_str.to_string()));
}
let year = parts[0].parse::<i32>().map_err(|_| {
DateTimeError::ParseError(format!("Invalid year: {}", parts[0]))
})?;
let month = parts[1].parse::<u32>().map_err(|_| {
DateTimeError::ParseError(format!("Invalid month: {}", parts[1]))
})?;
let naive_date = NaiveDate::from_ymd_opt(year, month, 1)
.ok_or_else(|| DateTimeError::InvalidDate(date_str.to_string()))?
.and_hms_opt(0, 0, 0)
.unwrap();
Ok(DateTime::from_naive_utc_and_offset(naive_date, Utc))
}
DateFormat::MonthDay => {
let parts: Vec<&str> = date_str.split('-').collect();
if parts.len() != 2 {
return Err(DateTimeError::ParseError(date_str.to_string()));
}
let month = parts[0].parse::<u32>().map_err(|_| {
DateTimeError::ParseError(format!("Invalid month: {}", parts[0]))
})?;
let day = parts[1]
.parse::<u32>()
.map_err(|_| DateTimeError::ParseError(format!("Invalid day: {}", parts[1])))?;
let naive_date = NaiveDate::from_ymd_opt(current_date.year(), month, day)
.ok_or_else(|| DateTimeError::InvalidDate(date_str.to_string()))?
.and_hms_opt(0, 0, 0)
.unwrap();
Ok(DateTime::from_naive_utc_and_offset(naive_date, Utc))
}
DateFormat::Year => {
let year = date_str.parse::<i32>().map_err(|_| {
DateTimeError::ParseError(format!("Invalid year: {}", date_str))
})?;
let naive_date = NaiveDate::from_ymd_opt(year, 1, 1)
.ok_or_else(|| DateTimeError::InvalidDate(date_str.to_string()))?
.and_hms_opt(0, 0, 0)
.unwrap();
Ok(DateTime::from_naive_utc_and_offset(naive_date, Utc))
}
DateFormat::Month => {
let month = date_str.parse::<u32>().map_err(|_| {
DateTimeError::ParseError(format!("Invalid month: {}", date_str))
})?;
let naive_date = NaiveDate::from_ymd_opt(current_date.year(), month, 1)
.ok_or_else(|| DateTimeError::InvalidDate(date_str.to_string()))?
.and_hms_opt(0, 0, 0)
.unwrap();
Ok(DateTime::from_naive_utc_and_offset(naive_date, Utc))
}
DateFormat::Day => {
let day = date_str
.parse::<u32>()
.map_err(|_| DateTimeError::ParseError(format!("Invalid day: {}", date_str)))?;
let naive_date =
NaiveDate::from_ymd_opt(current_date.year(), current_date.month(), day)
.ok_or_else(|| DateTimeError::InvalidDate(date_str.to_string()))?
.and_hms_opt(0, 0, 0)
.unwrap();
Ok(DateTime::from_naive_utc_and_offset(naive_date, Utc))
}
}
}
}
impl From<DateFormat> for i32 {
fn from(format: DateFormat) -> i32 {
format as i32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(into = "i32", from = "i32")]
pub enum TimeFormat {
H24 = 0, H12 = 1, }
impl From<TimeFormat> for i32 {
fn from(format: TimeFormat) -> i32 {
format as i32
}
}
impl From<i32> for TimeFormat {
fn from(value: i32) -> Self {
match value {
1 => TimeFormat::H12,
_ => TimeFormat::H24,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum DateTimeError {
#[error("Failed to parse date string: {0}")]
ParseError(String),
#[error("Invalid date: {0}")]
InvalidDate(String),
#[error("Invalid timestamp: {0}")]
InvalidTimestamp(i64),
#[error("Invalid timezone: {0}")]
InvalidTimezone(String),
}
impl fmt::Display for DateFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let format_str = match self {
DateFormat::YearMonthDaySlash => "YYYY/MM/DD",
DateFormat::YearMonthDayDash => "YYYY-MM-DD",
DateFormat::DayMonthYear => "DD/MM/YYYY",
DateFormat::YearMonth => "YYYY-MM",
DateFormat::MonthDay => "MM-DD",
DateFormat::Year => "YYYY",
DateFormat::Month => "MM",
DateFormat::Day => "DD",
};
write!(f, "{}", format_str)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn test_timezone_conversion() {
let format = DateFormat::YearMonthDayDash;
let date_str = "2024-03-21";
let result = format.parse_with_timezone(date_str, Some("America/New_York")).unwrap();
let formatted = format.format_with_timezone(&result).unwrap();
assert_eq!(result.timezone, Some("America/New_York".to_string()));
assert!(formatted.starts_with("2024"));
}
#[test]
fn test_invalid_timezone() {
let format = DateFormat::YearMonthDayDash;
let result = format.parse_with_timezone("2024-03-21", Some("Invalid/Timezone"));
assert!(matches!(result.unwrap_err(), DateTimeError::InvalidTimezone(_)));
}
#[test]
fn test_date_format_conversion() {
let test_cases = vec![
(1, DateFormat::YearMonthDaySlash),
(2, DateFormat::YearMonthDayDash),
(3, DateFormat::DayMonthYear),
(4, DateFormat::YearMonth),
(5, DateFormat::MonthDay),
(6, DateFormat::Year),
(7, DateFormat::Month),
(8, DateFormat::Day),
];
for (num, expected_format) in test_cases {
let format: DateFormat = num.into();
assert_eq!(format, expected_format);
assert_eq!(i32::from(format), num);
}
}
#[test]
fn test_format_datetime() {
let test_date = Utc.with_ymd_and_hms(2024, 3, 21, 0, 0, 0).unwrap();
let test_cases = vec![
(DateFormat::YearMonthDaySlash, "2024/03/21"),
(DateFormat::YearMonthDayDash, "2024-03-21"),
(DateFormat::DayMonthYear, "21/03/2024"),
(DateFormat::YearMonth, "2024-03"),
(DateFormat::MonthDay, "03-21"),
(DateFormat::Year, "2024"),
(DateFormat::Month, "03"),
(DateFormat::Day, "21"),
];
for (format, expected) in test_cases {
let result = format.format_datetime(&test_date);
assert_eq!(result, expected, "Failed formatting with {:?}", format);
}
}
#[test]
fn test_parse_year_month() {
let format = DateFormat::YearMonth;
let result = format.parse_datetime("2024-03");
assert!(result.is_ok(), "Failed to parse year-month format");
let datetime = result.unwrap();
assert_eq!(datetime.year(), 2024);
assert_eq!(datetime.month(), 3);
assert_eq!(datetime.day(), 1); }
#[test]
fn test_parse_month_day() {
let format = DateFormat::MonthDay;
let result = format.parse_datetime("03-21");
assert!(result.is_ok(), "Failed to parse month-day format");
let datetime = result.unwrap();
let current_year = Local::now().year();
assert_eq!(datetime.year(), current_year);
assert_eq!(datetime.month(), 3);
assert_eq!(datetime.day(), 21);
}
#[test]
fn test_invalid_partial_dates() {
let test_cases = vec![
(DateFormat::YearMonth, "2024"), (DateFormat::YearMonth, "2024-13"), (DateFormat::MonthDay, "00-21"), (DateFormat::MonthDay, "12-32"), ];
for (format, invalid_date) in test_cases {
let result = format.parse_datetime(invalid_date);
assert!(
result.is_err(),
"Expected error parsing {} with format {:?}",
invalid_date,
format
);
}
}
#[test]
fn test_invalid_date_parsing() {
let test_cases = vec![
(DateFormat::YearMonthDaySlash, "invalid"),
(DateFormat::YearMonthDayDash, "2024-13-45"), (DateFormat::DayMonthYear, "32/13/2024"), (DateFormat::YearMonth, "2024-13"), (DateFormat::MonthDay, "13-32"), (DateFormat::Year, "invalid"),
(DateFormat::Month, "13"), (DateFormat::Day, "32"), ];
for (format, invalid_date) in test_cases {
let result = format.parse_datetime(invalid_date);
assert!(
result.is_err(),
"Expected error parsing {} with format {:?}",
invalid_date,
format
);
}
}
#[test]
fn test_timestamp_conversion() {
let test_cases = vec![
(DateFormat::YearMonthDayDash, "2024-03-21"),
(DateFormat::YearMonthDaySlash, "2024/03/21"),
(DateFormat::DayMonthYear, "21/03/2024"),
];
for (format, date_str) in test_cases {
let timestamp = format.to_timestamp(date_str).unwrap();
let formatted = format.format_timestamp(timestamp).unwrap();
match format {
DateFormat::YearMonthDayDash
| DateFormat::YearMonthDaySlash
| DateFormat::DayMonthYear => {
assert_eq!(formatted, date_str);
}
_ => {
let reparsed = format.parse_datetime(&formatted).unwrap();
match format {
DateFormat::YearMonth => {
assert_eq!(reparsed.format("%Y-%m").to_string(), date_str);
}
DateFormat::MonthDay => {
assert_eq!(reparsed.format("%m-%d").to_string(), date_str);
}
_ => {}
}
}
}
}
}
#[test]
fn test_display_format() {
let test_cases = vec![
(DateFormat::YearMonthDaySlash, "YYYY/MM/DD"),
(DateFormat::YearMonthDayDash, "YYYY-MM-DD"),
(DateFormat::DayMonthYear, "DD/MM/YYYY"),
(DateFormat::YearMonth, "YYYY-MM"),
(DateFormat::MonthDay, "MM-DD"),
(DateFormat::Year, "YYYY"),
(DateFormat::Month, "MM"),
(DateFormat::Day, "DD"),
];
for (format, expected) in test_cases {
assert_eq!(format.to_string(), expected);
}
}
#[test]
fn test_edge_cases() {
let result = DateFormat::YearMonthDayDash.parse_datetime("2024-02-29");
assert!(result.is_ok());
let result = DateFormat::YearMonthDayDash.parse_datetime("2023-02-29");
assert!(result.is_err());
let valid_dates = vec![
"2024-01-31",
"2024-03-31",
"2024-04-30",
"2024-05-31",
"2024-06-30",
"2024-07-31",
"2024-08-31",
"2024-09-30",
"2024-10-31",
"2024-11-30",
"2024-12-31",
];
for date in valid_dates {
let result = DateFormat::YearMonthDayDash.parse_datetime(date);
assert!(result.is_ok(), "Failed to parse valid date: {}", date);
}
}
}