1use std::time::{SystemTime, UNIX_EPOCH};
4
5use chrono::{DateTime, Local, TimeZone, Utc};
6use facet::Facet;
7use serde::{Deserialize, Serialize};
8
9use crate::date::Date;
10
11#[allow(clippy::unsafe_derive_deserialize)]
13#[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14pub struct Instant {
15 pub seconds: u64,
17 pub nanos: u32,
18}
19
20impl Instant {
21 #[must_use]
22 pub fn duration_until_midnight(self) -> Duration {
23 let secs = self.seconds.try_into().unwrap_or_default();
24 let local_datetime = Local
26 .timestamp_opt(secs, self.nanos)
27 .earliest()
28 .or_else(|| Local.timestamp_opt(secs, self.nanos).latest())
29 .unwrap_or_else(|| {
30 Utc.timestamp_opt(secs, self.nanos)
32 .earliest()
33 .unwrap_or_default()
34 .with_timezone(&Local)
35 });
36
37 let next_day = local_datetime.date_naive() + chrono::Duration::days(1);
39 let next_midnight_naive = next_day.and_hms_opt(0, 0, 0).unwrap_or_default();
40
41 let next_midnight = Local
43 .from_local_datetime(&next_midnight_naive)
44 .earliest()
45 .or_else(|| Local.from_local_datetime(&next_midnight_naive).latest())
46 .unwrap_or_else(|| {
47 Utc.from_utc_datetime(&next_midnight_naive)
48 .with_timezone(&Local)
49 });
50
51 let next_midnight_timestamp = next_midnight.timestamp();
52 let seconds_until_midnight = next_midnight_timestamp.saturating_sub(secs);
53
54 Duration::from_secs(seconds_until_midnight.try_into().unwrap_or_default())
55 }
56
57 #[must_use]
58 pub fn into_date(self) -> Date {
59 self.into()
60 }
61
62 #[must_use]
63 pub fn from_utc_datetime(value: DateTime<Utc>) -> Self {
64 let timestamp = value.timestamp();
65 let seconds = timestamp.max(0).try_into().unwrap_or_default();
66 let nanos = value.timestamp_subsec_nanos();
67
68 Self { seconds, nanos }
69 }
70
71 #[must_use]
72 pub fn from_timestamp(seconds: i64) -> Self {
73 let seconds = seconds.max(0).try_into().unwrap_or_default();
74 Self { seconds, nanos: 0 }
75 }
76
77 #[must_use]
78 pub fn into_utc_datetime(self) -> DateTime<Utc> {
79 let seconds = i64::try_from(self.seconds).unwrap_or(i64::MAX);
80 Utc.timestamp_opt(seconds, self.nanos)
82 .earliest()
83 .unwrap_or_default()
84 }
85
86 #[must_use]
87 pub fn subtract_minutes(self, value: u64) -> Self {
88 let seconds = self.seconds.saturating_sub(value);
89 Self {
90 seconds,
91 nanos: self.nanos,
92 }
93 }
94
95 #[must_use]
96 pub fn add_minutes(self, value: u64) -> Self {
97 let seconds = self.seconds.saturating_add(value.saturating_mul(60));
98 Self {
99 seconds,
100 nanos: self.nanos,
101 }
102 }
103}
104
105impl std::fmt::Display for Instant {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 self.into_utc_datetime().fmt(f)
108 }
109}
110
111impl From<Date> for Instant {
112 fn from(value: Date) -> Self {
113 let naive_date: chrono::NaiveDate = value.into();
115
116 let midnight = naive_date.and_hms_opt(0, 0, 0).unwrap_or_else(|| {
118 naive_date
119 .and_hms_opt(0, 0, 1)
120 .unwrap_or(naive_date.and_hms_opt(1, 0, 0).unwrap_or_default())
121 });
122
123 let seconds = if let Some(local_dt) = Local.from_local_datetime(&midnight).earliest() {
125 local_dt.timestamp().max(0)
126 } else if let Some(local_dt) = Local.from_local_datetime(&midnight).latest() {
127 local_dt.timestamp().max(0)
128 } else {
129 Utc.from_utc_datetime(&midnight).timestamp().max(0)
131 };
132 Instant {
133 seconds: seconds.max(0).try_into().unwrap_or_default(),
134 nanos: 0,
135 }
136 }
137}
138
139impl From<Instant> for Date {
140 fn from(value: Instant) -> Self {
141 let seconds = i64::try_from(value.seconds).unwrap_or(i64::MAX);
142 let local_datetime = Local
144 .timestamp_opt(seconds, value.nanos)
145 .earliest()
146 .or_else(|| Local.timestamp_opt(seconds, value.nanos).latest())
147 .unwrap_or_else(|| {
148 Utc.timestamp_opt(seconds, value.nanos)
150 .earliest()
151 .unwrap_or_default()
152 .with_timezone(&Local)
153 });
154 let naive_date = local_datetime.date_naive();
155
156 Date::from(naive_date)
157 }
158}
159
160impl From<SystemTime> for Instant {
161 fn from(value: SystemTime) -> Self {
162 let duration = value.duration_since(UNIX_EPOCH).unwrap_or_default();
163 let seconds = duration.as_secs();
164 Instant {
165 seconds,
166 nanos: duration.subsec_nanos(),
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, derive_more::Display)]
172#[display("{} seconds", self.as_secs())]
173pub struct Duration {
174 pub nanos: u64,
175}
176
177impl Duration {
178 #[must_use]
179 pub fn from_secs(secs: u64) -> Self {
180 Self {
181 nanos: secs * 1_000_000_000,
182 }
183 }
184
185 #[must_use]
186 pub fn as_secs(&self) -> u64 {
187 self.nanos / 1_000_000_000
188 }
189
190 #[must_use]
191 pub fn from_std(duration: std::time::Duration) -> Self {
192 Self {
193 nanos: (duration.as_secs() * 1_000_000_000)
194 .saturating_add(duration.subsec_nanos().into()),
195 }
196 }
197
198 #[must_use]
199 pub fn into_std(&self) -> std::time::Duration {
200 std::time::Duration::from_nanos(self.nanos)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use chrono::NaiveDate;
207 use claims::{assert_ok, assert_some};
208 use pretty_assertions::assert_eq;
209 use rstest::rstest;
210
211 use super::*;
212
213 #[rstest]
214 #[case::summer_date(2025, 7, 24)]
215 #[case::new_years_day(2024, 1, 1)]
216 #[case::independence_day(2024, 7, 4)]
217 #[case::new_years_eve(2024, 12, 31)]
218 #[case::leap_year_feb_29(2024, 2, 29)]
219 #[case::regular_feb_28(2023, 2, 28)]
220 #[case::end_of_month_31(2024, 1, 31)]
221 #[case::end_of_month_30(2024, 4, 30)]
222 fn from_date_should_convert_to_local_midnight(
223 #[case] year: i32,
224 #[case] month: u32,
225 #[case] day: u32,
226 ) {
227 let naive_date = assert_some!(
228 NaiveDate::from_ymd_opt(year, month, day),
229 "precondition: date is constructed"
230 );
231 let date = crate::date::Date::from(naive_date);
232
233 let instant = Instant::from(date);
235
236 let expected_datetime = assert_some!(naive_date.and_hms_opt(0, 0, 0));
237 let expected_seconds = assert_some!(
238 Local.from_local_datetime(&expected_datetime).earliest(),
239 "expecting local time to exist"
240 )
241 .timestamp();
242
243 assert_eq!(
244 instant.seconds,
245 expected_seconds.max(0).try_into().unwrap_or_default()
246 );
247 assert_eq!(instant.nanos, 0);
248
249 let converted_date = Date::from(instant);
251 assert_eq!(
252 converted_date, date,
253 "expecting instant to convert back into date"
254 );
255 }
256
257 #[rstest]
258 #[case::spring_dst_transition(2024, 3, 10)]
259 #[case::fall_dst_transition(2024, 11, 3)]
260 fn from_date_should_handle_dst_transitions(
261 #[case] year: i32,
262 #[case] month: u32,
263 #[case] day: u32,
264 ) {
265 let naive_date = assert_some!(
266 NaiveDate::from_ymd_opt(year, month, day),
267 "precondition: DST transition date is constructed"
268 );
269 let date = crate::date::Date::from(naive_date);
270
271 let instant = Instant::from(date);
273
274 assert!(
275 instant.seconds > 0,
276 "Should produce valid timestamp for DST transition"
277 );
278 assert_eq!(instant.nanos, 0);
279 }
280
281 #[test]
282 fn now_should_convert_to_instant() {
283 let instant: Instant = SystemTime::now().into();
284 assert!(instant.seconds > 0, "expecting instant to have seconds");
285 }
286
287 #[rstest]
288 #[case::early_morning("2024-07-15 02:00:00", 22 * 3600)] #[case::afternoon("2024-07-15 14:30:00", 9 * 3600 + 30 * 60)] #[case::late_evening("2024-07-15 23:30:00", 30 * 60)] #[case::very_close_to_midnight("2024-07-15 23:59:59", 1)] #[case::exactly_midnight("2024-07-15 00:00:00", 24 * 3600)] fn duration_until_midnight_should_calculate_correctly(
294 #[case] datetime_str: &str,
295 #[case] expected_seconds: u64,
296 ) {
297 let naive_datetime = assert_ok!(
299 chrono::NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S"),
300 "parsing test datetime"
301 );
302
303 let local_datetime = assert_some!(
304 Local.from_local_datetime(&naive_datetime).earliest(),
305 "converting to local time"
306 );
307
308 let instant = Instant {
309 seconds: local_datetime.timestamp().try_into().unwrap_or_default(),
310 nanos: 0,
311 };
312
313 let duration = instant.duration_until_midnight();
314
315 let diff = if duration.as_secs() > expected_seconds {
317 duration.as_secs() - expected_seconds
318 } else {
319 expected_seconds - duration.as_secs()
320 };
321
322 assert!(
323 diff <= 1,
324 "Expected ~{} seconds, got {} seconds (diff: {})",
325 expected_seconds,
326 duration.as_secs(),
327 diff
328 );
329 }
330
331 #[test]
332 fn duration_until_midnight_should_handle_nanoseconds() {
333 let instant = Instant {
335 seconds: 1_721_030_400, nanos: 500_000_000, };
338
339 let duration = instant.duration_until_midnight();
340
341 assert!(
344 duration.as_secs() < 24 * 3600,
345 "Duration should be less than 24 hours"
346 );
347 assert!(duration.as_secs() > 0, "Duration should be positive");
348 }
349
350 #[rstest]
351 #[case::seconds(4, 0, 5, 0)]
352 #[case::nanos(5, 2, 5, 3)]
353 #[case::nanos_larger_but_seconds_smaller(5, 10, 6, 0)]
354 fn instant_comparison(
355 #[case] earlier_seconds: u64,
356 #[case] earlier_nanos: u32,
357 #[case] later_seconds: u64,
358 #[case] later_nanos: u32,
359 ) {
360 let earlier = Instant {
361 seconds: earlier_seconds,
362 nanos: earlier_nanos,
363 };
364 let later = Instant {
365 seconds: later_seconds,
366 nanos: later_nanos,
367 };
368
369 assert!(earlier < later, "expecting earlier to be less than later");
370 assert!(
371 earlier <= later,
372 "expecting earlier to be less than or equal to later"
373 );
374 assert!(
375 later > earlier,
376 "expecting later to be greater than earlier"
377 );
378 assert!(
379 later >= earlier,
380 "expecting later to be greater than or equal to earlier"
381 );
382 }
383}