progscrape_scrapers/types/
date.rs

1use chrono::{DateTime, Datelike, Months, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3use std::{fmt::Display, ops::Sub, time::SystemTime};
4
5/// Story-specific date that wraps all of the operations we're interested in. This is a thin wrapper on top
6/// of `DateTime<Utc>` and other `chrono` utilities for now.
7#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct StoryDate {
9    internal_date: DateTime<Utc>,
10}
11
12impl StoryDate {
13    pub const MAX: StoryDate = Self::new(DateTime::<Utc>::MAX_UTC);
14    pub const MIN: StoryDate = Self::new(DateTime::<Utc>::MIN_UTC);
15
16    pub const fn new(internal_date: DateTime<Utc>) -> Self {
17        Self { internal_date }
18    }
19    pub fn year_month_day(year: i32, month: u32, day: u32) -> Option<Self> {
20        match (
21            NaiveDate::from_ymd_opt(year, month, day),
22            NaiveTime::from_hms_opt(0, 0, 0),
23        ) {
24            (Some(d), Some(t)) => {
25                let dt = d.and_time(t);
26                Some(Self::new(Utc.from_utc_datetime(&dt)))
27            }
28            _ => None,
29        }
30    }
31    pub fn now() -> Self {
32        Self::new(DateTime::<Utc>::from(SystemTime::now()))
33    }
34    pub fn from_millis(millis: i64) -> Option<Self> {
35        Utc.timestamp_millis_opt(millis).earliest().map(Self::new)
36    }
37    pub fn from_seconds(seconds: i64) -> Option<Self> {
38        Self::from_millis(seconds * 1_000)
39    }
40    pub fn from_string(date: &str, s: &str) -> Option<Self> {
41        let date = NaiveDateTime::parse_from_str(date, s).ok();
42        date.map(|x| Self::new(Utc.from_utc_datetime(&x)))
43    }
44    pub fn parse_from_rfc3339(date: &str) -> Option<Self> {
45        DateTime::parse_from_rfc3339(date)
46            .ok()
47            .map(|x| Self::new(x.into()))
48    }
49    pub fn to_rfc3339(&self) -> String {
50        self.internal_date.to_rfc3339()
51    }
52    pub fn parse_from_rfc2822(date: &str) -> Option<Self> {
53        DateTime::parse_from_rfc2822(date)
54            .ok()
55            .map(|x| Self::new(x.into()))
56    }
57    pub fn year(&self) -> i32 {
58        self.internal_date.year()
59    }
60    pub fn month(&self) -> u32 {
61        self.internal_date.month()
62    }
63    pub fn month0(&self) -> u32 {
64        self.internal_date.month0()
65    }
66    pub fn day(&self) -> u32 {
67        self.internal_date.day()
68    }
69    pub fn day0(&self) -> u32 {
70        self.internal_date.day0()
71    }
72    pub fn timestamp(&self) -> i64 {
73        self.internal_date.timestamp()
74    }
75    pub fn checked_add_months(&self, months: u32) -> Option<Self> {
76        self.internal_date
77            .checked_add_months(Months::new(months))
78            .map(StoryDate::new)
79    }
80    pub fn checked_sub_months(&self, months: u32) -> Option<Self> {
81        self.internal_date
82            .checked_sub_months(Months::new(months))
83            .map(StoryDate::new)
84    }
85}
86
87impl Sub for StoryDate {
88    type Output = StoryDuration;
89    fn sub(self, rhs: Self) -> Self::Output {
90        StoryDuration {
91            duration: self.internal_date - rhs.internal_date,
92        }
93    }
94}
95
96impl Serialize for StoryDate {
97    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
98    where
99        S: serde::Serializer,
100    {
101        chrono::serde::ts_seconds::serialize(&self.internal_date, serializer)
102    }
103}
104
105impl<'de> Deserialize<'de> for StoryDate {
106    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
107    where
108        D: serde::Deserializer<'de>,
109    {
110        chrono::serde::ts_seconds::deserialize(deserializer).map(Self::new)
111    }
112}
113
114impl Display for StoryDate {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        self.internal_date.fmt(f)
117    }
118}
119
120#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
121pub struct StoryDuration {
122    duration: chrono::Duration,
123}
124
125macro_rules! duration_unit {
126    ($unit:ident, $num_unit:ident, $num_unit_f32:ident) => {
127        #[inline(always)]
128        #[allow(dead_code)]
129        pub fn $unit($unit: i64) -> Self {
130            Self {
131                duration: chrono::Duration::$unit($unit),
132            }
133        }
134
135        #[inline(always)]
136        #[allow(dead_code)]
137        pub fn $num_unit(&self) -> i64 {
138            self.duration.$num_unit()
139        }
140
141        #[inline(always)]
142        #[allow(dead_code)]
143        pub fn $num_unit_f32(&self) -> f32 {
144            self.duration.num_milliseconds() as f32
145                / Self::$unit(1).duration.num_milliseconds() as f32
146        }
147    };
148}
149
150impl StoryDuration {
151    duration_unit!(days, num_days, num_days_f32);
152    duration_unit!(hours, num_hours, num_hours_f32);
153    duration_unit!(minutes, num_minutes, num_minutes_f32);
154    duration_unit!(seconds, num_seconds, num_seconds_f32);
155    duration_unit!(milliseconds, num_milliseconds, num_milliseconds_f32);
156}
157
158impl Sub for StoryDuration {
159    type Output = <chrono::Duration as Sub>::Output;
160
161    fn sub(self, rhs: Self) -> Self::Output {
162        self.duration - rhs.duration
163    }
164}
165
166#[cfg(test)]
167mod test {
168    use crate::StoryDate;
169
170    #[test]
171    fn test_serialize() {
172        let date = StoryDate::year_month_day(2000, 1, 1).expect("Date is valid");
173        let json = serde_json::to_string(&date).expect("Serialize");
174        let date2 = serde_json::from_str::<StoryDate>(&json).expect("Deserialize");
175        assert_eq!(date, date2);
176
177        let date_from_seconds = str::parse::<i64>(&json).expect("Parse");
178        assert_eq!(
179            date,
180            StoryDate::from_seconds(date_from_seconds).expect("From seconds")
181        );
182    }
183}