use chrono::{Duration, NaiveTime, Timelike, Weekday};
use crate::errors::ValidationError;
use crate::traits::ValueObject;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BusinessHoursInput {
pub weekday: Weekday,
pub open: NaiveTime,
pub close: NaiveTime,
}
pub type BusinessHoursOutput = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BusinessHours {
weekday: Weekday,
open: NaiveTime,
close: NaiveTime,
#[cfg_attr(feature = "serde", serde(skip))]
canonical: String,
}
impl ValueObject for BusinessHours {
type Input = BusinessHoursInput;
type Output = BusinessHoursOutput;
type Error = ValidationError;
fn new(value: Self::Input) -> Result<Self, Self::Error> {
if value.open >= value.close {
return Err(ValidationError::invalid(
"BusinessHours",
&format!(
"{} {}–{}",
weekday_abbr(value.weekday),
value.open,
value.close
),
));
}
let canonical = format!(
"{} {:02}:{:02}–{:02}:{:02}",
weekday_abbr(value.weekday),
value.open.hour(),
value.open.minute(),
value.close.hour(),
value.close.minute(),
);
Ok(Self {
weekday: value.weekday,
open: value.open,
close: value.close,
canonical,
})
}
fn value(&self) -> &Self::Output {
&self.canonical
}
fn into_inner(self) -> Self::Input {
BusinessHoursInput {
weekday: self.weekday,
open: self.open,
close: self.close,
}
}
}
impl BusinessHours {
pub fn weekday(&self) -> Weekday {
self.weekday
}
pub fn open(&self) -> &NaiveTime {
&self.open
}
pub fn close(&self) -> &NaiveTime {
&self.close
}
pub fn duration(&self) -> Duration {
self.close - self.open
}
}
impl std::fmt::Display for BusinessHours {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.canonical)
}
}
fn weekday_abbr(wd: Weekday) -> &'static str {
match wd {
Weekday::Mon => "Mon",
Weekday::Tue => "Tue",
Weekday::Wed => "Wed",
Weekday::Thu => "Thu",
Weekday::Fri => "Fri",
Weekday::Sat => "Sat",
Weekday::Sun => "Sun",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn open() -> NaiveTime {
NaiveTime::from_hms_opt(9, 0, 0).unwrap()
}
fn close() -> NaiveTime {
NaiveTime::from_hms_opt(17, 0, 0).unwrap()
}
fn valid_input() -> BusinessHoursInput {
BusinessHoursInput {
weekday: Weekday::Mon,
open: open(),
close: close(),
}
}
#[test]
fn accepts_valid_hours() {
let h = BusinessHours::new(valid_input()).unwrap();
assert_eq!(h.value(), "Mon 09:00–17:00");
}
#[test]
fn weekday_accessor() {
let h = BusinessHours::new(valid_input()).unwrap();
assert_eq!(h.weekday(), Weekday::Mon);
}
#[test]
fn open_accessor() {
let h = BusinessHours::new(valid_input()).unwrap();
assert_eq!(h.open(), &open());
}
#[test]
fn close_accessor() {
let h = BusinessHours::new(valid_input()).unwrap();
assert_eq!(h.close(), &close());
}
#[test]
fn duration_is_eight_hours() {
let h = BusinessHours::new(valid_input()).unwrap();
assert_eq!(h.duration().num_hours(), 8);
}
#[test]
fn rejects_equal_open_close() {
assert!(
BusinessHours::new(BusinessHoursInput {
weekday: Weekday::Mon,
open: open(),
close: open(),
})
.is_err()
);
}
#[test]
fn rejects_open_after_close() {
assert!(
BusinessHours::new(BusinessHoursInput {
weekday: Weekday::Mon,
open: close(),
close: open(),
})
.is_err()
);
}
#[test]
fn formats_all_weekdays() {
for (wd, abbr) in [
(Weekday::Mon, "Mon"),
(Weekday::Tue, "Tue"),
(Weekday::Wed, "Wed"),
(Weekday::Thu, "Thu"),
(Weekday::Fri, "Fri"),
(Weekday::Sat, "Sat"),
(Weekday::Sun, "Sun"),
] {
let h = BusinessHours::new(BusinessHoursInput {
weekday: wd,
open: open(),
close: close(),
})
.unwrap();
assert!(h.value().starts_with(abbr));
}
}
#[test]
fn display_matches_value() {
let h = BusinessHours::new(valid_input()).unwrap();
assert_eq!(h.to_string(), h.value().to_owned());
}
#[test]
fn into_inner_roundtrip() {
let input = valid_input();
let h = BusinessHours::new(input.clone()).unwrap();
assert_eq!(h.into_inner(), input);
}
}