progscrape_scrapers/types/
date.rs

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