use std::str::FromStr;
use chrono::{
DateTime, FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, Offset, TimeZone, Utc,
};
use crate::utils::StringValueData;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct XsDate {
pub date: NaiveDate,
pub tz: Option<FixedOffset>,
}
impl XsDate {
pub fn new(date: NaiveDate) -> Self {
XsDate { date, tz: None }
}
pub fn from_date(year: i32, month: u32, day: u32) -> Option<Self> {
NaiveDate::from_ymd_opt(year, month, day).map(XsDate::new)
}
pub fn new_with_tz<O: Offset>(date: NaiveDate, tz: O) -> Self {
XsDate {
date,
tz: Some(tz.fix()),
}
}
}
impl From<NaiveDate> for XsDate {
fn from(value: NaiveDate) -> Self {
XsDate::new(value)
}
}
impl FromStr for XsDate {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let sep_count = s.chars().filter(|&c| c == '-' || c == '+').count();
if sep_count > 2
&& let Some(pos) = s.rfind(['+', '-'])
{
let (date_str, tz_str) = s.split_at(pos);
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
let tz = tz_str.parse::<FixedOffset>()?;
Ok(XsDate { date, tz: Some(tz) })
} else if s.ends_with('Z') || s.ends_with('z') {
let date_str = &s[..s.len() - 1];
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
Ok(XsDate {
date,
tz: Some(Utc.fix()),
})
} else {
let date = NaiveDate::parse_from_str(s, "%Y-%m-%d")?;
Ok(XsDate { date, tz: None })
}
}
}
impl StringValueData for XsDate {
type Error = chrono::ParseError;
fn parse_from_str(s: &str) -> Result<Self, Self::Error>
where
Self: Sized,
{
s.parse()
}
fn to_raw_value(&self) -> String {
match self.tz {
Some(tz) => format!("{}{}", self.date.format("%Y-%m-%d"), tz),
None => self.date.format("%Y-%m-%d").to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct XsDateTime {
pub naive_date_time: NaiveDateTime,
pub tz: Option<FixedOffset>,
}
impl XsDateTime {
pub fn new<T: TimeZone>(date_time: DateTime<T>) -> Self {
XsDateTime {
naive_date_time: date_time.naive_utc(),
tz: Some(date_time.offset().fix()),
}
}
pub fn new_without_tz(naive_date_time: NaiveDateTime) -> Self {
XsDateTime {
naive_date_time,
tz: None,
}
}
pub fn datetime_utc(&self) -> DateTime<Utc> {
match self.tz {
Some(tz) => {
DateTime::<FixedOffset>::from_naive_utc_and_offset(self.naive_date_time, tz)
.to_utc()
}
None => DateTime::<Utc>::from_naive_utc_and_offset(self.naive_date_time, Utc),
}
}
pub fn datetime_tz<Tz: TimeZone>(&self, tz: &Tz) -> MappedLocalTime<DateTime<Tz>> {
match self.tz {
Some(original_tz) => MappedLocalTime::Single(
DateTime::<FixedOffset>::from_naive_utc_and_offset(
self.naive_date_time,
original_tz,
)
.with_timezone(tz),
),
None => tz.from_local_datetime(&self.naive_date_time),
}
}
}
impl<T: TimeZone> From<DateTime<T>> for XsDateTime {
fn from(value: DateTime<T>) -> Self {
XsDateTime::new(value)
}
}
impl FromStr for XsDateTime {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
Ok(XsDateTime {
naive_date_time: dt.naive_utc(),
tz: Some(dt.offset().to_owned()),
})
} else {
let naive_dt = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f")?;
Ok(XsDateTime {
naive_date_time: naive_dt,
tz: None,
})
}
}
}
impl StringValueData for XsDateTime {
type Error = chrono::ParseError;
fn parse_from_str(s: &str) -> Result<Self, Self::Error>
where
Self: Sized,
{
s.parse()
}
fn to_raw_value(&self) -> String {
match self.tz {
Some(tz) => {
let dt_with_tz =
DateTime::<FixedOffset>::from_naive_utc_and_offset(self.naive_date_time, tz);
dt_with_tz.to_rfc3339()
}
None => self
.naive_date_time
.format("%Y-%m-%dT%H:%M:%S%.f")
.to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum XsDateOrDateTime {
Date(XsDate),
DateTime(XsDateTime),
}
impl XsDateOrDateTime {
pub fn date<Tz: TimeZone>(&self, tz: &Tz) -> MappedLocalTime<NaiveDate> {
match self {
XsDateOrDateTime::Date(d) => MappedLocalTime::Single(d.date),
XsDateOrDateTime::DateTime(dt) => {
let dt = dt.datetime_tz(tz);
match dt {
MappedLocalTime::Single(dt) => MappedLocalTime::Single(dt.date_naive()),
MappedLocalTime::None => MappedLocalTime::None,
MappedLocalTime::Ambiguous(first, second) => {
if first.date_naive() == second.date_naive() {
MappedLocalTime::Single(first.date_naive())
} else {
MappedLocalTime::Ambiguous(first.date_naive(), second.date_naive())
}
}
}
}
}
}
}
impl From<XsDate> for XsDateOrDateTime {
fn from(value: XsDate) -> Self {
XsDateOrDateTime::Date(value)
}
}
impl From<XsDateTime> for XsDateOrDateTime {
fn from(value: XsDateTime) -> Self {
XsDateOrDateTime::DateTime(value)
}
}
impl From<NaiveDate> for XsDateOrDateTime {
fn from(value: NaiveDate) -> Self {
XsDateOrDateTime::Date(XsDate::new(value))
}
}
impl<T> From<DateTime<T>> for XsDateOrDateTime
where
T: TimeZone,
{
fn from(value: DateTime<T>) -> Self {
XsDateOrDateTime::DateTime(XsDateTime::new(value))
}
}
impl FromStr for XsDateOrDateTime {
type Err = chrono::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.contains('T') {
let date_time = s.parse::<XsDateTime>()?;
Ok(XsDateOrDateTime::DateTime(date_time))
} else {
let date = s.parse::<XsDate>()?;
Ok(XsDateOrDateTime::Date(date))
}
}
}
impl StringValueData for XsDateOrDateTime {
type Error = chrono::ParseError;
fn parse_from_str(s: &str) -> Result<Self, Self::Error>
where
Self: Sized,
{
s.parse()
}
fn to_raw_value(&self) -> String {
match self {
XsDateOrDateTime::Date(d) => d.to_raw_value(),
XsDateOrDateTime::DateTime(dt) => dt.to_raw_value(),
}
}
}
#[cfg(test)]
mod tests {
use chrono::{Datelike as _, Timelike as _};
use super::*;
#[test]
fn test_xs_date_parse() {
let d1: XsDate = "2025-10-05".parse().unwrap();
assert_eq!(d1.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
assert!(d1.tz.is_none());
let d2: XsDate = "2025-10-05+02:00".parse().unwrap();
assert_eq!(d2.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
assert_eq!(d2.tz.unwrap(), FixedOffset::east_opt(2 * 3600).unwrap());
let d3: XsDate = "2025-10-05Z".parse().unwrap();
assert_eq!(d3.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
assert_eq!(d3.tz.unwrap(), Utc.fix());
let d4: XsDate = "2025-10-05-05:00".parse().unwrap();
assert_eq!(d4.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
assert_eq!(d4.tz.unwrap(), FixedOffset::west_opt(5 * 3600).unwrap());
}
#[test]
fn test_xs_date_time_parse() {
let dt1: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
assert_eq!(
dt1.naive_date_time,
NaiveDate::from_ymd_opt(2025, 10, 5)
.unwrap()
.and_hms_opt(14, 30, 0)
.unwrap()
);
assert!(dt1.tz.is_none());
let dt2: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
assert_eq!(
dt2.naive_date_time,
NaiveDate::from_ymd_opt(2025, 10, 5)
.unwrap()
.and_hms_opt(12, 30, 0)
.unwrap()
);
assert_eq!(dt2.tz.unwrap(), FixedOffset::east_opt(2 * 3600).unwrap());
let dt3: XsDateTime = "2025-10-05T14:30:00Z".parse().unwrap();
assert_eq!(
dt3.naive_date_time,
NaiveDate::from_ymd_opt(2025, 10, 5)
.unwrap()
.and_hms_opt(14, 30, 0)
.unwrap()
);
assert_eq!(dt3.tz.unwrap(), Utc.fix());
let dt4: XsDateTime = "2025-10-05T14:30:00.123456".parse().unwrap();
assert_eq!(
dt4.naive_date_time,
NaiveDate::from_ymd_opt(2025, 10, 5)
.unwrap()
.and_hms_micro_opt(14, 30, 0, 123456)
.unwrap()
);
assert!(dt4.tz.is_none());
let dt5: XsDateTime = "2025-10-05T14:30:00.123456-02:00".parse().unwrap();
assert_eq!(
dt5.naive_date_time,
NaiveDate::from_ymd_opt(2025, 10, 5)
.unwrap()
.and_hms_micro_opt(16, 30, 0, 123456)
.unwrap()
);
assert_eq!(dt5.tz.unwrap(), FixedOffset::west_opt(2 * 3600).unwrap());
}
#[test]
fn test_xs_date_time_to_datetime_utc() {
let dt1: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
let utc_dt1 = dt1.datetime_utc();
assert_eq!(utc_dt1.year(), 2025);
assert_eq!(utc_dt1.month(), 10);
assert_eq!(utc_dt1.day(), 5);
assert_eq!(utc_dt1.hour(), 12);
assert_eq!(utc_dt1.minute(), 30);
let dt2: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
let utc_dt2 = dt2.datetime_utc();
assert_eq!(utc_dt2.year(), 2025);
assert_eq!(utc_dt2.month(), 10);
assert_eq!(utc_dt2.day(), 5);
assert_eq!(utc_dt2.hour(), 14);
assert_eq!(utc_dt2.minute(), 30);
}
#[test]
fn test_xs_date_time_to_datetime_tz() {
let dt1: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
let tz = FixedOffset::east_opt(3600).unwrap();
let dt1_in_tz = dt1.datetime_tz(&tz).single().unwrap();
assert_eq!(dt1_in_tz.year(), 2025);
assert_eq!(dt1_in_tz.month(), 10);
assert_eq!(dt1_in_tz.day(), 5);
assert_eq!(dt1_in_tz.hour(), 13);
assert_eq!(dt1_in_tz.minute(), 30);
let dt2: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
let dt2_in_tz = dt2.datetime_tz(&tz).single().unwrap();
assert_eq!(dt2_in_tz.year(), 2025);
assert_eq!(dt2_in_tz.month(), 10);
assert_eq!(dt2_in_tz.day(), 5);
assert_eq!(dt2_in_tz.hour(), 14);
assert_eq!(dt2_in_tz.minute(), 30);
}
#[test]
fn test_xs_date_or_date_time_parse() {
let d: XsDateOrDateTime = "2025-10-05".parse().unwrap();
match d {
XsDateOrDateTime::Date(date) => {
assert_eq!(date.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
assert!(date.tz.is_none());
}
_ => panic!("Expected XsDate variant"),
}
let dt: XsDateOrDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
match dt {
XsDateOrDateTime::DateTime(date_time) => {
assert_eq!(
date_time.naive_date_time,
NaiveDate::from_ymd_opt(2025, 10, 5)
.unwrap()
.and_hms_opt(12, 30, 0)
.unwrap()
);
assert_eq!(
date_time.tz.unwrap(),
FixedOffset::east_opt(2 * 3600).unwrap()
);
}
_ => panic!("Expected XsDateTime variant"),
}
}
}