use chrono::{DateTime, Duration, Utc};
use crate::errors::ValidationError;
use crate::traits::ValueObject;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TimeRangeInput {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
}
pub type TimeRangeOutput = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimeRange {
start: DateTime<Utc>,
end: DateTime<Utc>,
#[cfg_attr(feature = "serde", serde(skip))]
canonical: String,
}
impl ValueObject for TimeRange {
type Input = TimeRangeInput;
type Output = TimeRangeOutput;
type Error = ValidationError;
fn new(value: Self::Input) -> Result<Self, Self::Error> {
if value.start >= value.end {
return Err(ValidationError::invalid(
"TimeRange",
&format!("{} / {}", value.start, value.end),
));
}
let canonical = format!("{} / {}", value.start, value.end);
Ok(Self {
start: value.start,
end: value.end,
canonical,
})
}
fn value(&self) -> &Self::Output {
&self.canonical
}
fn into_inner(self) -> Self::Input {
TimeRangeInput {
start: self.start,
end: self.end,
}
}
}
impl TimeRange {
pub fn start(&self) -> &DateTime<Utc> {
&self.start
}
pub fn end(&self) -> &DateTime<Utc> {
&self.end
}
pub fn duration(&self) -> Duration {
self.end - self.start
}
}
impl std::fmt::Display for TimeRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.canonical)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn start() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap()
}
fn end() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap()
}
#[test]
fn accepts_valid_range() {
assert!(
TimeRange::new(TimeRangeInput {
start: start(),
end: end()
})
.is_ok()
);
}
#[test]
fn start_accessor() {
let r = TimeRange::new(TimeRangeInput {
start: start(),
end: end(),
})
.unwrap();
assert_eq!(r.start(), &start());
}
#[test]
fn end_accessor() {
let r = TimeRange::new(TimeRangeInput {
start: start(),
end: end(),
})
.unwrap();
assert_eq!(r.end(), &end());
}
#[test]
fn duration_is_two_hours() {
let r = TimeRange::new(TimeRangeInput {
start: start(),
end: end(),
})
.unwrap();
assert_eq!(r.duration().num_hours(), 2);
}
#[test]
fn rejects_equal_start_end() {
assert!(
TimeRange::new(TimeRangeInput {
start: start(),
end: start()
})
.is_err()
);
}
#[test]
fn rejects_start_after_end() {
assert!(
TimeRange::new(TimeRangeInput {
start: end(),
end: start()
})
.is_err()
);
}
#[test]
fn display_matches_value() {
let r = TimeRange::new(TimeRangeInput {
start: start(),
end: end(),
})
.unwrap();
assert_eq!(r.to_string(), r.value().to_owned());
}
#[test]
fn into_inner_roundtrip() {
let input = TimeRangeInput {
start: start(),
end: end(),
};
let r = TimeRange::new(input.clone()).unwrap();
assert_eq!(r.into_inner(), input);
}
}