use crate::entities::{Error, Result, WorkingInterval};
use chrono::{Datelike, TimeZone, Timelike};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
pub(crate) type Duration = chrono::Duration;
pub(crate) type DelayedFormat<'a> =
chrono::format::DelayedFormat<chrono::format::StrftimeItems<'a>>;
pub(crate) type NaiveDate = chrono::NaiveDate;
pub(crate) type NaiveTime = chrono::NaiveTime;
pub(crate) type NaiveDateTime = chrono::NaiveDateTime;
#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
pub(crate) struct DateTime(chrono::DateTime<chrono::Utc>);
impl std::ops::Sub<DateTime> for DateTime {
type Output = Duration;
fn sub(self, rhs: Self) -> Self::Output {
self.0 - rhs.0
}
}
impl std::ops::Sub<&DateTime> for DateTime {
type Output = Duration;
fn sub(self, rhs: &Self) -> Self::Output {
self.0 - rhs.0
}
}
impl std::ops::Add<Duration> for DateTime {
type Output = DateTime;
fn add(self, rhs: Duration) -> Self::Output {
Self(self.0 + rhs)
}
}
impl std::ops::Sub<Duration> for DateTime {
type Output = DateTime;
fn sub(self, rhs: Duration) -> Self::Output {
Self(self.0 - rhs)
}
}
impl<T: chrono::TimeZone> From<chrono::DateTime<T>> for DateTime {
fn from(value: chrono::DateTime<T>) -> Self {
Self(value.to_utc())
}
}
impl FromStr for DateTime {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if let Some(local) = chrono::Local
.from_local_datetime(&chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M")?)
.single()
{
Ok(Self(local.with_timezone(&chrono::Utc)))
} else {
Err(Error::CouldNotParseDateTime(s.to_string()))
}
}
}
impl std::fmt::Display for DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let local = self.0.with_timezone(&chrono::Local).naive_local();
write!(f, "{local}")
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum RoundMode {
Future,
Past,
}
impl DateTime {
pub(crate) fn now() -> Self {
Self(chrono::Utc::now())
}
pub(crate) const fn min() -> Self {
Self(chrono::DateTime::<chrono::Utc>::MIN_UTC)
}
pub(crate) const fn max() -> Self {
Self(chrono::DateTime::<chrono::Utc>::MAX_UTC)
}
pub(crate) fn from_naive(datetime: chrono::NaiveDateTime) -> Self {
let local = chrono::Local
.from_local_datetime(&datetime)
.single()
.expect("Ungültiges Datum für lokale Zeitzone");
Self(local.with_timezone(&chrono::Utc))
}
pub(crate) fn from_naive_date(date: NaiveDate) -> Self {
let datetime = date.and_hms_opt(0, 0, 0).expect("invalid date");
Self::from_naive(datetime)
}
pub(crate) fn from_ymd(year: i32, month: u32, day: u32) -> Result<Self> {
match chrono::NaiveDate::from_ymd_opt(year, month, day) {
Some(date) => Ok(Self::from_naive_date(date)),
None => Err(Error::InvalidDateValues(year, month, day)),
}
}
pub(crate) fn with_time(&self, time: NaiveTime) -> Self {
let local_datetime = self
.0
.with_timezone(&chrono::Local)
.date_naive()
.and_time(time);
Self::from_naive(local_datetime)
}
pub(crate) fn with_delta(&self, delta: Duration) -> Self {
Self(self.0 + delta)
}
pub(crate) fn start_of_day(&self) -> Self {
self.with_time(chrono::NaiveTime::MIN)
}
pub(crate) fn end_of_day(&self) -> Self {
self.start_of_day() + Duration::days(1)
}
pub(crate) fn start_of_month(&self) -> DateTime {
DateTime(
self.0
.with_timezone(&chrono::Local)
.with_day(1)
.unwrap()
.with_time(chrono::NaiveTime::default())
.unwrap()
.to_utc(),
)
}
pub(crate) fn format<'a>(&self, fmt: &'a str) -> DelayedFormat<'a> {
self.format_tz(fmt, &chrono::Local)
}
pub(crate) fn format_tz<'a>(
&self,
fmt: &'a str,
tz: &impl chrono::TimeZone,
) -> DelayedFormat<'a> {
self.0.with_timezone(tz).naive_local().format(fmt)
}
#[cfg(test)]
pub(crate) fn as_chrono_utc(&self) -> chrono::DateTime<chrono::Utc> {
self.0
}
pub(crate) fn round_to_interval(&self, interval: WorkingInterval, mode: RoundMode) -> DateTime {
let interval = interval.num_minutes();
let local = self.0.with_timezone(&chrono::Local);
let total_minutes = local.hour() * 60 + local.minute();
let rounded = match mode {
RoundMode::Future => total_minutes.div_ceil(interval) * interval % 1440,
RoundMode::Past => total_minutes / interval * interval,
};
local
.with_hour(rounded / 60)
.and_then(|dt| dt.with_minute(rounded % 60))
.and_then(|dt| dt.with_second(0))
.and_then(|dt| dt.with_nanosecond(0))
.map(|dt| dt.with_timezone(&chrono::Utc))
.expect("invalid time")
.into()
}
pub(crate) fn day(&self) -> u32 {
self.0.with_timezone(&chrono::Local).day()
}
pub(crate) fn month(&self) -> u32 {
self.0.with_timezone(&chrono::Local).month()
}
pub(crate) fn year(&self) -> i32 {
self.0.with_timezone(&chrono::Local).year()
}
pub(crate) fn day_of_week(&self) -> u32 {
self.0
.with_timezone(&chrono::Local)
.weekday()
.num_days_from_sunday()
}
pub(crate) fn next_month(&self) -> Self {
Self(
self.0
.with_timezone(&chrono::Local)
.checked_add_months(chrono::Months::new(1))
.expect("internal date/time error")
.to_utc(),
)
}
pub(crate) fn next_day(&self) -> Self {
Self(
self.0
.with_timezone(&chrono::Local)
.checked_add_days(chrono::Days::new(1))
.expect("internal date/time error")
.to_utc(),
)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::entities::DateTime;
fn naive_time(s: &str) -> chrono::NaiveTime {
chrono::NaiveTime::parse_from_str(s, "%H:%M").unwrap()
}
#[test]
fn with_time() {
assert_eq!(
DateTime::from_str("2026-1-1 12:00")
.unwrap()
.with_time(naive_time("14:00")),
DateTime::from_str("2026-1-1 14:00").unwrap()
);
}
#[test]
fn naive_date() {
assert_eq!(
chrono::NaiveDateTime::parse_from_str("2026-01-01 00:00", "%Y-%m-%d %H:%M")
.unwrap()
.and_local_timezone(chrono::Local)
.unwrap(),
DateTime::from_str("2026-1-1 00:00").unwrap().0
)
}
#[test]
fn from_naive() {
assert_eq!(
DateTime::from_naive(chrono::NaiveDateTime::new(
chrono::NaiveDate::from_str("2026-01-01").unwrap(),
chrono::NaiveTime::from_str("00:00").unwrap(),
))
.format("%Y-%m-%d %H:%M")
.to_string(),
"2026-01-01 00:00"
);
}
}