rust_queries_core/
datetime.rs

1//! DateTime operations for query builder.
2//!
3//! This module provides datetime comparison and manipulation operations that can be used
4//! with the query builder. It supports both `std::time::SystemTime` and optionally
5//! `chrono` types when the `datetime` feature is enabled.
6//!
7//! # Features
8//!
9//! - Date comparisons (before, after, between)
10//! - Time range queries
11//! - Date arithmetic (add/subtract days, hours, etc.)
12//! - Date extraction (year, month, day, hour, etc.)
13//! - Timezone-aware operations (with chrono)
14//!
15//! # Example
16//!
17//! ```ignore
18//! use rust_queries_core::{Query, datetime::*};
19//! use chrono::{DateTime, Utc};
20//!
21//! #[derive(Keypath)]
22//! struct Event {
23//!     name: String,
24//!     timestamp: DateTime<Utc>,
25//! }
26//!
27//! let events = vec![/* ... */];
28//! let recent = Query::new(&events)
29//!     .where_(Event::timestamp(), |ts| {
30//!         is_after(ts, &Utc::now() - chrono::Duration::days(7))
31//!     });
32//! ```
33
34use std::time::{SystemTime, Duration, UNIX_EPOCH};
35
36#[cfg(feature = "datetime")]
37pub use chrono;
38
39/// Check if a SystemTime is after another SystemTime
40pub fn is_after_systemtime(time: &SystemTime, reference: &SystemTime) -> bool {
41    time > reference
42}
43
44/// Check if a SystemTime is before another SystemTime
45pub fn is_before_systemtime(time: &SystemTime, reference: &SystemTime) -> bool {
46    time < reference
47}
48
49/// Check if a SystemTime is between two SystemTimes (inclusive)
50pub fn is_between_systemtime(time: &SystemTime, start: &SystemTime, end: &SystemTime) -> bool {
51    time >= start && time <= end
52}
53
54/// Check if a SystemTime is within a duration from now
55pub fn is_within_duration_systemtime(time: &SystemTime, duration: Duration) -> bool {
56    match SystemTime::now().duration_since(UNIX_EPOCH) {
57        Ok(now) => {
58            if let Ok(time_dur) = time.duration_since(UNIX_EPOCH) {
59                let diff = if now > time_dur {
60                    now - time_dur
61                } else {
62                    time_dur - now
63                };
64                diff <= duration
65            } else {
66                false
67            }
68        }
69        Err(_) => false,
70    }
71}
72
73/// Add duration to SystemTime
74pub fn add_duration_systemtime(time: &SystemTime, duration: Duration) -> SystemTime {
75    *time + duration
76}
77
78/// Subtract duration from SystemTime
79pub fn subtract_duration_systemtime(time: &SystemTime, duration: Duration) -> SystemTime {
80    *time - duration
81}
82
83// Chrono-specific operations (only available with datetime feature)
84#[cfg(feature = "datetime")]
85pub mod chrono_ops {
86    use chrono::{DateTime, TimeZone, Datelike, Timelike, Duration};
87
88    /// Check if a DateTime is after another DateTime
89    pub fn is_after<Tz: TimeZone>(time: &DateTime<Tz>, reference: &DateTime<Tz>) -> bool
90    where
91        Tz::Offset: std::fmt::Display,
92    {
93        time > reference
94    }
95
96    /// Check if a DateTime is before another DateTime
97    pub fn is_before<Tz: TimeZone>(time: &DateTime<Tz>, reference: &DateTime<Tz>) -> bool
98    where
99        Tz::Offset: std::fmt::Display,
100    {
101        time < reference
102    }
103
104    /// Check if a DateTime is between two DateTimes (inclusive)
105    pub fn is_between<Tz: TimeZone>(
106        time: &DateTime<Tz>,
107        start: &DateTime<Tz>,
108        end: &DateTime<Tz>,
109    ) -> bool
110    where
111        Tz::Offset: std::fmt::Display,
112    {
113        time >= start && time <= end
114    }
115
116    /// Check if a DateTime is today
117    pub fn is_today<Tz: TimeZone>(time: &DateTime<Tz>, now: &DateTime<Tz>) -> bool
118    where
119        Tz::Offset: std::fmt::Display,
120    {
121        time.date_naive() == now.date_naive()
122    }
123
124    /// Check if a DateTime is within a duration from now
125    pub fn is_within_duration<Tz: TimeZone>(
126        time: &DateTime<Tz>,
127        now: &DateTime<Tz>,
128        duration: Duration,
129    ) -> bool
130    where
131        Tz::Offset: std::fmt::Display,
132    {
133        let diff = if time > now {
134            time.clone() - now.clone()
135        } else {
136            now.clone() - time.clone()
137        };
138        diff <= duration
139    }
140
141    /// Check if two DateTimes are on the same day
142    pub fn is_same_day<Tz: TimeZone>(time1: &DateTime<Tz>, time2: &DateTime<Tz>) -> bool
143    where
144        Tz::Offset: std::fmt::Display,
145    {
146        time1.date_naive() == time2.date_naive()
147    }
148
149    /// Check if a DateTime is in the past
150    pub fn is_past<Tz: TimeZone>(time: &DateTime<Tz>, now: &DateTime<Tz>) -> bool
151    where
152        Tz::Offset: std::fmt::Display,
153    {
154        time < now
155    }
156
157    /// Check if a DateTime is in the future
158    pub fn is_future<Tz: TimeZone>(time: &DateTime<Tz>, now: &DateTime<Tz>) -> bool
159    where
160        Tz::Offset: std::fmt::Display,
161    {
162        time > now
163    }
164
165    /// Extract year from DateTime
166    pub fn extract_year<Tz: TimeZone>(time: &DateTime<Tz>) -> i32
167    where
168        Tz::Offset: std::fmt::Display,
169    {
170        time.year()
171    }
172
173    /// Extract month from DateTime (1-12)
174    pub fn extract_month<Tz: TimeZone>(time: &DateTime<Tz>) -> u32
175    where
176        Tz::Offset: std::fmt::Display,
177    {
178        time.month()
179    }
180
181    /// Extract day from DateTime (1-31)
182    pub fn extract_day<Tz: TimeZone>(time: &DateTime<Tz>) -> u32
183    where
184        Tz::Offset: std::fmt::Display,
185    {
186        time.day()
187    }
188
189    /// Extract hour from DateTime (0-23)
190    pub fn extract_hour<Tz: TimeZone>(time: &DateTime<Tz>) -> u32
191    where
192        Tz::Offset: std::fmt::Display,
193    {
194        time.hour()
195    }
196
197    /// Extract minute from DateTime (0-59)
198    pub fn extract_minute<Tz: TimeZone>(time: &DateTime<Tz>) -> u32
199    where
200        Tz::Offset: std::fmt::Display,
201    {
202        time.minute()
203    }
204
205    /// Extract second from DateTime (0-59)
206    pub fn extract_second<Tz: TimeZone>(time: &DateTime<Tz>) -> u32
207    where
208        Tz::Offset: std::fmt::Display,
209    {
210        time.second()
211    }
212
213    /// Get day of week (Monday = 0, Sunday = 6)
214    pub fn day_of_week<Tz: TimeZone>(time: &DateTime<Tz>) -> u32
215    where
216        Tz::Offset: std::fmt::Display,
217    {
218        time.weekday().num_days_from_monday()
219    }
220
221    /// Check if a DateTime is on a weekend (Saturday or Sunday)
222    pub fn is_weekend<Tz: TimeZone>(time: &DateTime<Tz>) -> bool
223    where
224        Tz::Offset: std::fmt::Display,
225    {
226        let weekday = time.weekday().num_days_from_monday();
227        weekday >= 5 // Saturday = 5, Sunday = 6
228    }
229
230    /// Check if a DateTime is on a weekday (Monday-Friday)
231    pub fn is_weekday<Tz: TimeZone>(time: &DateTime<Tz>) -> bool
232    where
233        Tz::Offset: std::fmt::Display,
234    {
235        !is_weekend(time)
236    }
237
238    /// Add days to a DateTime
239    pub fn add_days<Tz: TimeZone>(time: &DateTime<Tz>, days: i64) -> DateTime<Tz>
240    where
241        Tz::Offset: std::fmt::Display,
242    {
243        time.clone() + Duration::days(days)
244    }
245
246    /// Add hours to a DateTime
247    pub fn add_hours<Tz: TimeZone>(time: &DateTime<Tz>, hours: i64) -> DateTime<Tz>
248    where
249        Tz::Offset: std::fmt::Display,
250    {
251        time.clone() + Duration::hours(hours)
252    }
253
254    /// Add minutes to a DateTime
255    pub fn add_minutes<Tz: TimeZone>(time: &DateTime<Tz>, minutes: i64) -> DateTime<Tz>
256    where
257        Tz::Offset: std::fmt::Display,
258    {
259        time.clone() + Duration::minutes(minutes)
260    }
261
262    /// Subtract days from a DateTime
263    pub fn subtract_days<Tz: TimeZone>(time: &DateTime<Tz>, days: i64) -> DateTime<Tz>
264    where
265        Tz::Offset: std::fmt::Display,
266    {
267        time.clone() - Duration::days(days)
268    }
269
270    /// Subtract hours from a DateTime
271    pub fn subtract_hours<Tz: TimeZone>(time: &DateTime<Tz>, hours: i64) -> DateTime<Tz>
272    where
273        Tz::Offset: std::fmt::Display,
274    {
275        time.clone() - Duration::hours(hours)
276    }
277
278    /// Subtract minutes from a DateTime
279    pub fn subtract_minutes<Tz: TimeZone>(time: &DateTime<Tz>, minutes: i64) -> DateTime<Tz>
280    where
281        Tz::Offset: std::fmt::Display,
282    {
283        time.clone() - Duration::minutes(minutes)
284    }
285
286    /// Get the start of day (midnight) for a DateTime
287    pub fn start_of_day<Tz: TimeZone + Clone>(time: &DateTime<Tz>) -> Option<DateTime<Tz>>
288    where
289        Tz::Offset: std::fmt::Display,
290    {
291        time.date_naive()
292            .and_hms_opt(0, 0, 0)
293            .and_then(|naive| time.timezone().from_local_datetime(&naive).single())
294    }
295
296    /// Get the end of day (23:59:59) for a DateTime
297    pub fn end_of_day<Tz: TimeZone + Clone>(time: &DateTime<Tz>) -> Option<DateTime<Tz>>
298    where
299        Tz::Offset: std::fmt::Display,
300    {
301        time.date_naive()
302            .and_hms_opt(23, 59, 59)
303            .and_then(|naive| time.timezone().from_local_datetime(&naive).single())
304    }
305
306    /// Check if a DateTime falls within business hours (9 AM - 5 PM)
307    pub fn is_business_hours<Tz: TimeZone>(time: &DateTime<Tz>) -> bool
308    where
309        Tz::Offset: std::fmt::Display,
310    {
311        let hour = time.hour();
312        hour >= 9 && hour < 17
313    }
314
315    /// Calculate the number of days between two DateTimes
316    pub fn days_between<Tz: TimeZone>(time1: &DateTime<Tz>, time2: &DateTime<Tz>) -> i64
317    where
318        Tz::Offset: std::fmt::Display,
319    {
320        let diff = if time1 > time2 {
321            time1.clone() - time2.clone()
322        } else {
323            time2.clone() - time1.clone()
324        };
325        diff.num_days()
326    }
327
328    /// Calculate the number of hours between two DateTimes
329    pub fn hours_between<Tz: TimeZone>(time1: &DateTime<Tz>, time2: &DateTime<Tz>) -> i64
330    where
331        Tz::Offset: std::fmt::Display,
332    {
333        let diff = if time1 > time2 {
334            time1.clone() - time2.clone()
335        } else {
336            time2.clone() - time1.clone()
337        };
338        diff.num_hours()
339    }
340}
341
342#[cfg(test)]
343#[cfg(feature = "datetime")]
344mod tests {
345    use super::*;
346    use chrono::{Utc, Duration, TimeZone};
347
348    #[test]
349    fn test_is_after() {
350        let now = Utc::now();
351        let future = now + Duration::hours(1);
352        let past = now - Duration::hours(1);
353
354        assert!(chrono_ops::is_after(&future, &now));
355        assert!(!chrono_ops::is_after(&past, &now));
356    }
357
358    #[test]
359    fn test_is_between() {
360        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
361        let end = Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap();
362        let middle = Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
363        let before = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap();
364
365        assert!(chrono_ops::is_between(&middle, &start, &end));
366        assert!(!chrono_ops::is_between(&before, &start, &end));
367    }
368
369    #[test]
370    fn test_date_extraction() {
371        let dt = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
372
373        assert_eq!(chrono_ops::extract_year(&dt), 2024);
374        assert_eq!(chrono_ops::extract_month(&dt), 3);
375        assert_eq!(chrono_ops::extract_day(&dt), 15);
376        assert_eq!(chrono_ops::extract_hour(&dt), 14);
377        assert_eq!(chrono_ops::extract_minute(&dt), 30);
378        assert_eq!(chrono_ops::extract_second(&dt), 45);
379    }
380
381    #[test]
382    fn test_date_arithmetic() {
383        let dt = Utc.with_ymd_and_hms(2024, 3, 15, 12, 0, 0).unwrap();
384        let future = chrono_ops::add_days(&dt, 10);
385        let past = chrono_ops::subtract_days(&dt, 5);
386
387        assert_eq!(chrono_ops::extract_day(&future), 25);
388        assert_eq!(chrono_ops::extract_day(&past), 10);
389    }
390
391    #[test]
392    fn test_is_weekend() {
393        // Saturday, March 16, 2024
394        let saturday = Utc.with_ymd_and_hms(2024, 3, 16, 12, 0, 0).unwrap();
395        // Monday, March 18, 2024
396        let monday = Utc.with_ymd_and_hms(2024, 3, 18, 12, 0, 0).unwrap();
397
398        assert!(chrono_ops::is_weekend(&saturday));
399        assert!(!chrono_ops::is_weekend(&monday));
400        assert!(chrono_ops::is_weekday(&monday));
401    }
402
403    #[test]
404    fn test_is_business_hours() {
405        let morning = Utc.with_ymd_and_hms(2024, 3, 15, 10, 0, 0).unwrap();
406        let evening = Utc.with_ymd_and_hms(2024, 3, 15, 18, 0, 0).unwrap();
407
408        assert!(chrono_ops::is_business_hours(&morning));
409        assert!(!chrono_ops::is_business_hours(&evening));
410    }
411}
412