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