progscrape_scrapers/types/
date.rs1use 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#[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 if let Some(date) = Self::parse_from_rfc3339(date) {
55 return Some(date);
56 }
57 if date.len() >= 19 {
60 if let Some(date) = Self::parse_from_rfc3339(&format!("{}Z", &date[..19])) {
61 return Some(date);
62 }
63 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 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}