use chrono::{NaiveDate, NaiveDateTime};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DateTimeValue {
Date(NaiveDate),
DateTime(NaiveDateTime),
}
impl DateTimeValue {
#[must_use]
pub fn date(&self) -> NaiveDate {
match self {
Self::Date(d) => *d,
Self::DateTime(dt) => dt.date(),
}
}
#[must_use]
pub fn datetime(&self) -> Option<NaiveDateTime> {
match self {
Self::Date(_) => None,
Self::DateTime(dt) => Some(*dt),
}
}
#[must_use]
pub fn is_date_only(&self) -> bool {
matches!(self, Self::Date(_))
}
#[must_use]
pub fn from_date(date: NaiveDate) -> Self {
Self::Date(date)
}
#[must_use]
pub fn from_datetime(datetime: NaiveDateTime) -> Self {
Self::DateTime(datetime)
}
#[must_use]
pub fn now() -> Self {
Self::DateTime(chrono::Utc::now().naive_utc())
}
#[must_use]
pub fn today() -> Self {
Self::Date(chrono::Utc::now().naive_utc().date())
}
}
impl fmt::Display for DateTimeValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Date(d) => write!(f, "{}", d.format("%Y-%m-%d")),
Self::DateTime(dt) => write!(f, "{}", dt.format("%Y-%m-%dT%H:%M:%S")),
}
}
}
impl FromStr for DateTimeValue {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
return Ok(Self::DateTime(dt));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M") {
return Ok(Self::DateTime(dt));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
return Ok(Self::DateTime(dt));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M") {
return Ok(Self::DateTime(dt));
}
if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
return Ok(Self::Date(d));
}
Err(format!("invalid date/datetime format: {s}"))
}
}
impl From<NaiveDate> for DateTimeValue {
fn from(date: NaiveDate) -> Self {
Self::Date(date)
}
}
impl From<NaiveDateTime> for DateTimeValue {
fn from(datetime: NaiveDateTime) -> Self {
Self::DateTime(datetime)
}
}
impl PartialOrd for DateTimeValue {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DateTimeValue {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let self_dt = match self {
Self::Date(d) => d.and_hms_opt(0, 0, 0).unwrap_or_default(),
Self::DateTime(dt) => *dt,
};
let other_dt = match other {
Self::Date(d) => d.and_hms_opt(0, 0, 0).unwrap_or_default(),
Self::DateTime(dt) => *dt,
};
self_dt.cmp(&other_dt)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_date_only() {
let value: DateTimeValue = "2025-01-15".parse().unwrap();
assert!(value.is_date_only());
assert_eq!(value.date(), NaiveDate::from_ymd_opt(2025, 1, 15).unwrap());
assert!(value.datetime().is_none());
}
#[test]
fn parse_datetime() {
let value: DateTimeValue = "2025-01-15T14:30:00".parse().unwrap();
assert!(!value.is_date_only());
assert_eq!(value.date(), NaiveDate::from_ymd_opt(2025, 1, 15).unwrap());
assert!(value.datetime().is_some());
}
#[test]
fn parse_datetime_without_seconds() {
let value: DateTimeValue = "2025-01-15T14:30".parse().unwrap();
assert!(!value.is_date_only());
assert_eq!(value.date(), NaiveDate::from_ymd_opt(2025, 1, 15).unwrap());
let dt = value.datetime().unwrap();
assert_eq!(dt.format("%H:%M").to_string(), "14:30");
}
#[test]
fn parse_datetime_space_separated() {
let value: DateTimeValue = "2025-01-15 14:30:00".parse().unwrap();
assert!(!value.is_date_only());
assert_eq!(value.date(), NaiveDate::from_ymd_opt(2025, 1, 15).unwrap());
assert!(value.datetime().is_some());
}
#[test]
fn parse_datetime_space_separated_without_seconds() {
let value: DateTimeValue = "2025-01-15 14:30".parse().unwrap();
assert!(!value.is_date_only());
assert_eq!(value.date(), NaiveDate::from_ymd_opt(2025, 1, 15).unwrap());
let dt = value.datetime().unwrap();
assert_eq!(dt.format("%H:%M").to_string(), "14:30");
}
#[test]
fn display_preserves_format() {
let date_value: DateTimeValue = "2025-01-15".parse().unwrap();
assert_eq!(date_value.to_string(), "2025-01-15");
let datetime_value: DateTimeValue = "2025-01-15T14:30:00".parse().unwrap();
assert_eq!(datetime_value.to_string(), "2025-01-15T14:30:00");
}
#[test]
fn from_naive_date() {
let date = NaiveDate::from_ymd_opt(2025, 6, 1).unwrap();
let value = DateTimeValue::from(date);
assert!(value.is_date_only());
assert_eq!(value.date(), date);
}
#[test]
fn from_naive_datetime() {
let dt = NaiveDate::from_ymd_opt(2025, 6, 1)
.unwrap()
.and_hms_opt(10, 30, 0)
.unwrap();
let value = DateTimeValue::from(dt);
assert!(!value.is_date_only());
assert_eq!(value.datetime(), Some(dt));
}
#[test]
fn ordering_works() {
let earlier: DateTimeValue = "2025-01-01".parse().unwrap();
let later: DateTimeValue = "2025-01-02".parse().unwrap();
assert!(earlier < later);
let date_value: DateTimeValue = "2025-01-01".parse().unwrap();
let datetime_value: DateTimeValue = "2025-01-01T12:00:00".parse().unwrap();
assert!(date_value < datetime_value); }
#[test]
fn invalid_format_returns_error() {
let result: Result<DateTimeValue, _> = "not-a-date".parse();
assert!(result.is_err());
}
}