use crate::Error;
use crate::model::xs::{DateTime, offset};
use chrono::{Datelike, FixedOffset, NaiveDate, NaiveTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Date {
start: DateTime,
}
impl Date {
pub fn new(date: NaiveDate, offset: FixedOffset) -> Result<Self, Error> {
let start = DateTime::from_local(date.and_time(NaiveTime::MIN), offset)?;
Ok(Self { start })
}
pub fn from_parts(year: i32, month: u32, day: u32, offset_seconds: i32) -> Result<Self, Error> {
let date = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| {
Error::InvalidDateTimeFields {
value: format!("year={year}, month={month}, day={day}"),
}
})?;
let offset = offset::offset_from_seconds(offset_seconds)?;
Self::new(date, offset)
}
pub fn date(&self) -> NaiveDate {
self.start.local().date()
}
pub fn offset(&self) -> FixedOffset {
self.start.offset()
}
pub fn start_of_day(&self) -> DateTime {
self.start
}
pub fn year(&self) -> i32 {
self.date().year()
}
pub fn month(&self) -> u32 {
self.date().month()
}
pub fn day(&self) -> u32 {
self.date().day()
}
pub fn offset_seconds(&self) -> i32 {
self.offset().local_minus_utc()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
fn hash_of(value: &Date) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
fn hours_east(hours: i32) -> FixedOffset {
FixedOffset::east_opt(hours * 3600).unwrap()
}
fn date(y: i32, m: u32, d: u32, hours: i32) -> Date {
Date::new(NaiveDate::from_ymd_opt(y, m, d).unwrap(), hours_east(hours))
.expect("should be valid")
}
#[test]
fn test_new() {
let value = date(2020, 1, 2, 2);
assert_eq!(value.date(), NaiveDate::from_ymd_opt(2020, 1, 2).unwrap());
assert_eq!(value.offset(), hours_east(2));
}
#[test]
fn test_new_rejects_invalid_offset() {
let day = NaiveDate::from_ymd_opt(2020, 1, 2).unwrap();
assert_eq!(
Date::new(day, hours_east(-15)),
Err(Error::InvalidTimezoneOffset {
seconds: -15 * 3600
})
);
}
#[test]
fn test_start_of_day() {
let start = date(2020, 1, 2, 2).start_of_day();
assert_eq!(
start.local(),
NaiveDate::from_ymd_opt(2020, 1, 2)
.unwrap()
.and_time(NaiveTime::MIN)
);
assert_eq!(start.offset(), hours_east(2));
}
#[test]
fn test_equality_compares_starting_instants() {
let a = date(2020, 1, 2, 12);
let b = date(2020, 1, 1, -12);
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
}
#[test]
fn test_ordering() {
assert!(date(2020, 1, 2, 0) < date(2020, 1, 3, 0));
assert!(date(2020, 1, 2, 2) < date(2020, 1, 2, 0));
}
#[test]
fn test_getters() {
let value = Date::from_parts(2020, 1, 2, -5 * 3600).expect("should be valid");
assert_eq!((value.year(), value.month(), value.day()), (2020, 1, 2));
assert_eq!(value.offset_seconds(), -5 * 3600);
}
#[test]
fn test_getters_before_1970() {
let late_1969 = Date::from_parts(1969, 12, 31, 3600).expect("should be valid");
let ancient = Date::from_parts(-44, 3, 15, 0).expect("should be valid");
assert_eq!(
(late_1969.year(), late_1969.month(), late_1969.day()),
(1969, 12, 31)
);
assert_eq!(
(ancient.year(), ancient.month(), ancient.day()),
(-44, 3, 15)
);
}
#[test]
fn test_from_parts_matches_new() {
assert_eq!(
Date::from_parts(2020, 1, 2, 2 * 3600),
Ok(date(2020, 1, 2, 2))
);
}
#[test]
fn test_from_parts_round_trips_through_getters() {
let value = Date::from_parts(2020, 2, 29, -(9 * 3600 + 30 * 60)).expect("should be valid");
let rebuilt = Date::from_parts(
value.year(),
value.month(),
value.day(),
value.offset_seconds(),
)
.expect("should be valid");
assert_eq!(rebuilt, value);
assert_eq!(rebuilt.offset_seconds(), value.offset_seconds());
}
#[test]
fn test_from_parts_rejects_invalid_fields() {
for (year, month, day) in [(2020, 2, 30), (2021, 2, 29), (2020, 13, 1), (2020, 1, 0)] {
assert!(
matches!(
Date::from_parts(year, month, day, 0),
Err(Error::InvalidDateTimeFields { .. })
),
"{year}-{month}-{day} should be rejected"
);
}
}
#[test]
fn test_from_parts_rejects_invalid_offsets() {
for offset_seconds in [15 * 3600, 30, 100_000] {
assert_eq!(
Date::from_parts(2020, 1, 2, offset_seconds),
Err(Error::InvalidTimezoneOffset {
seconds: offset_seconds
})
);
}
}
#[test]
fn test_from_parts_compares_starting_instants() {
let a = Date::from_parts(2020, 1, 2, 12 * 3600).expect("should be valid");
let b = Date::from_parts(2020, 1, 1, -12 * 3600).expect("should be valid");
let later = Date::from_parts(2020, 1, 2, 0).expect("should be valid");
assert_eq!(a, b);
assert_eq!(hash_of(&a), hash_of(&b));
assert!(a < later);
}
}