1use std::{fmt, str::FromStr, sync::OnceLock};
6
7use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, TimeZone, Timelike};
8use regex::Regex;
9use serde::de;
10
11use crate::LooseDateTime;
12use crate::datetime::util::{end_of_day, from_local_datetime, start_of_day};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DateTimeAnchor {
17 InDays(i64),
19
20 Relative(i64),
22
23 DateTime(LooseDateTime),
25
26 Time(NaiveTime),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum DayBoundary {
32 Start,
33 End,
34}
35
36impl DateTimeAnchor {
37 pub fn now() -> Self {
39 DateTimeAnchor::Relative(0)
40 }
41
42 pub fn today() -> Self {
44 DateTimeAnchor::InDays(0)
45 }
46
47 pub fn tomorrow() -> Self {
49 DateTimeAnchor::InDays(1)
50 }
51
52 pub fn yesterday() -> Self {
54 DateTimeAnchor::InDays(-1)
55 }
56
57 pub fn resolve_at_start_of_day<Tz: TimeZone>(&self, now: &DateTime<Tz>) -> DateTime<Tz> {
59 self.resolve_at_day_boundary(now, DayBoundary::Start)
60 }
61
62 pub fn resolve_at_end_of_day<Tz: TimeZone>(&self, now: &DateTime<Tz>) -> DateTime<Tz> {
64 self.resolve_at_day_boundary(now, DayBoundary::End)
65 }
66
67 fn resolve_at_day_boundary<Tz: TimeZone>(
69 &self,
70 now: &DateTime<Tz>,
71 boundary: DayBoundary,
72 ) -> DateTime<Tz> {
73 match self {
74 DateTimeAnchor::InDays(n) => {
75 let dt = match boundary {
76 DayBoundary::Start => start_of_day(now),
77 DayBoundary::End => end_of_day(now),
78 };
79 dt + TimeDelta::days(*n)
80 }
81 DateTimeAnchor::Relative(n) => now.clone() + TimeDelta::seconds(*n),
82 DateTimeAnchor::DateTime(dt) => {
83 let naive = match boundary {
84 DayBoundary::Start => dt.with_start_of_day(),
85 DayBoundary::End => dt.with_end_of_day(),
86 };
87 from_local_datetime(&now.timezone(), naive)
88 }
89 DateTimeAnchor::Time(t) => {
90 let naive = NaiveDateTime::new(now.date_naive(), *t);
91 from_local_datetime(&now.timezone(), naive)
92 }
93 }
94 }
95
96 pub fn resolve_at(self, now: &LooseDateTime) -> LooseDateTime {
98 match self {
99 DateTimeAnchor::InDays(n) => *now + TimeDelta::days(n),
100 DateTimeAnchor::Relative(n) => *now + TimeDelta::seconds(n),
101 DateTimeAnchor::DateTime(dt) => dt,
102 DateTimeAnchor::Time(t) => match now {
103 LooseDateTime::Local(dt) => {
104 let dt = NaiveDateTime::new(dt.date_naive(), t);
105 from_local_datetime(&Local, dt).into()
106 }
107 LooseDateTime::Floating(dt) => {
108 let dt = NaiveDateTime::new(dt.date(), t);
109 LooseDateTime::Floating(dt)
110 }
111 LooseDateTime::DateOnly(d) => {
112 let dt = NaiveDateTime::new(*d, t);
113 LooseDateTime::from_local_datetime(dt)
114 }
115 },
116 }
117 }
118
119 pub fn resolve_since(self, start: &LooseDateTime) -> LooseDateTime {
121 match self {
122 DateTimeAnchor::InDays(n) => match n {
123 0 => match start {
124 LooseDateTime::Local(dt) => next_suggested_time(&dt.naive_local()),
125 LooseDateTime::Floating(dt) => next_suggested_time(dt),
126 LooseDateTime::DateOnly(d) => first_suggested_time(d),
127 },
128 _ => {
129 let date = start.date() + TimeDelta::days(n);
130 let dt = NaiveDateTime::new(date, NaiveTime::from_hms_opt(9, 0, 0).unwrap());
131 LooseDateTime::from_local_datetime(dt)
132 }
133 },
134 DateTimeAnchor::Relative(n) => *start + TimeDelta::seconds(n),
135 DateTimeAnchor::DateTime(dt) => dt,
136 DateTimeAnchor::Time(t) => {
137 let date = start.date();
138 let delta = if let Some(s) = start.time()
140 && s >= t
141 {
142 TimeDelta::days(1)
143 } else {
144 TimeDelta::zero()
145 };
146 let dt = NaiveDateTime::new(date, t) + delta;
147 LooseDateTime::from_local_datetime(dt)
148 }
149 }
150 }
151
152 pub fn resolve_since_datetime<Tz: TimeZone>(self, start: &DateTime<Tz>) -> LooseDateTime {
154 match self {
155 DateTimeAnchor::InDays(n) => match n {
156 0 => next_suggested_time(&start.naive_local()),
157 _ => {
158 let date = start.date_naive() + TimeDelta::days(n);
159 let dt = NaiveDateTime::new(date, NaiveTime::from_hms_opt(9, 0, 0).unwrap());
160 LooseDateTime::from_local_datetime(dt)
161 }
162 },
163 DateTimeAnchor::Relative(n) => {
164 let dt = start.clone() + TimeDelta::seconds(n);
165 LooseDateTime::Local(dt.with_timezone(&Local))
166 }
167 DateTimeAnchor::DateTime(dt) => dt,
168 DateTimeAnchor::Time(t) => {
169 let date = start.date_naive();
170 let delta = if start.time() >= t {
172 TimeDelta::days(1)
173 } else {
174 TimeDelta::zero()
175 };
176 let dt = NaiveDateTime::new(date, t) + delta;
177 LooseDateTime::from_local_datetime(dt)
178 }
179 }
180 }
181
182 #[deprecated(
185 since = "0.9.0",
186 note = "use `resolve_at_start_of_day` method instead, will be removed in 0.12.0"
187 )]
188 pub fn parse_as_start_of_day<Tz: TimeZone>(&self, now: &DateTime<Tz>) -> DateTime<Tz> {
189 self.resolve_at_start_of_day(now)
190 }
191
192 #[deprecated(
195 since = "0.9.0",
196 note = "use `resolve_at_end_of_day` method instead, will be removed in 0.12.0"
197 )]
198 pub fn parse_as_end_of_day<Tz: TimeZone>(&self, now: &DateTime<Tz>) -> DateTime<Tz> {
199 self.resolve_at_end_of_day(now)
200 }
201
202 #[deprecated(
204 since = "0.9.0",
205 note = "use `resolve_at` method instead, will be removed in 0.12.0"
206 )]
207 pub fn parse_from_loose(self, now: &LooseDateTime) -> LooseDateTime {
209 self.resolve_at(now)
210 }
211
212 #[deprecated(
214 since = "0.9.0",
215 note = "use `resolve_since_datetime` method instead, will be removed in 0.12.0"
216 )]
217 pub fn parse_from_dt<Tz: TimeZone>(self, now: &DateTime<Tz>) -> LooseDateTime {
219 self.resolve_since_datetime(now)
220 }
221}
222
223impl FromStr for DateTimeAnchor {
224 type Err = String;
225
226 fn from_str(t: &str) -> Result<Self, Self::Err> {
227 match t {
229 "yesterday" => return Ok(Self::yesterday()),
230 "tomorrow" => return Ok(Self::tomorrow()),
231 "today" => return Ok(Self::today()),
232 "now" => return Ok(Self::now()),
233 _ => {}
234 }
235
236 if let Ok(dt) = NaiveDateTime::parse_from_str(t, "%Y-%m-%d %H:%M") {
237 Ok(Self::DateTime(LooseDateTime::from_local_datetime(dt)))
239 } else if let Ok(date) = NaiveDate::parse_from_str(t, "%Y-%m-%d") {
240 Ok(Self::DateTime(LooseDateTime::DateOnly(date)))
242 } else if let Ok(time) = NaiveTime::parse_from_str(t, "%H:%M") {
243 Ok(Self::Time(time))
245 } else if let Some(hours) = parse_seconds(t) {
246 Ok(Self::Relative(hours))
248 } else if let Some(minutes) = parse_minutes(t) {
249 Ok(Self::Relative(60 * minutes))
251 } else if let Some(hours) = parse_hours(t) {
252 Ok(Self::Relative(60 * 60 * hours))
254 } else if let Some(days) = parse_days(t) {
255 Ok(Self::InDays(days))
257 } else {
258 Err(format!("Invalid datetime anchor: {t}"))
259 }
260 }
261}
262
263impl<'de> serde::Deserialize<'de> for DateTimeAnchor {
264 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
265 where
266 D: serde::Deserializer<'de>,
267 {
268 struct Visitor;
269
270 impl<'de> de::Visitor<'de> for Visitor {
271 type Value = DateTimeAnchor;
272
273 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
274 formatter.write_str("a string representing a datetime")
275 }
276
277 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
278 where
279 E: de::Error,
280 {
281 value.parse().map_err(de::Error::custom)
282 }
283 }
284
285 deserializer.deserialize_str(Visitor)
286 }
287}
288
289macro_rules! parse_with_regex {
290 ($fn: ident, $re:expr) => {
291 fn $fn(s: &str) -> Option<i64> {
292 static REGEX: OnceLock<Regex> = OnceLock::new();
293 let re = REGEX.get_or_init(|| Regex::new($re).unwrap());
294 if let Some(captures) = re.captures(s)
295 && let Ok(num) = captures[1].parse::<i64>()
296 {
297 return Some(num);
298 }
299 None
300 }
301 };
302}
303
304parse_with_regex!(parse_seconds, r"^\s*(\d+)\s*s(?:ec|econds)?\s*$"); parse_with_regex!(parse_minutes, r"^\s*(\d+)\s*m(?:in|inutes)?\s*$"); parse_with_regex!(parse_hours, r"(?i)^\s*(?:in\s*)?(\d+)\s*h(?:ours)?\s*$"); parse_with_regex!(parse_days, r"(?i)^\s*(?:in\s*)?(\d+)\s*d(?:ays)?\s*$"); const HOURS: [u32; 3] = [9, 13, 18];
312
313fn next_suggested_time(now: &NaiveDateTime) -> LooseDateTime {
314 let date = now.date();
315 let current_hour = now.hour();
316 for hour in HOURS {
317 if current_hour < hour {
318 let dt = NaiveDateTime::new(date, NaiveTime::from_hms_opt(hour, 0, 0).unwrap());
319 return LooseDateTime::from_local_datetime(dt);
320 }
321 }
322
323 LooseDateTime::DateOnly(date)
324}
325
326fn first_suggested_time(date: &NaiveDate) -> LooseDateTime {
327 let dt = NaiveDateTime::new(*date, NaiveTime::from_hms_opt(HOURS[0], 0, 0).unwrap());
328 LooseDateTime::from_local_datetime(dt)
329}
330
331#[cfg(test)]
332mod tests {
333 use chrono::{NaiveDate, Utc};
334
335 use super::*;
336
337 #[test]
338 fn test_anchor_now() {
339 let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 30, 45).unwrap();
340 assert_eq!(DateTimeAnchor::now().resolve_at_start_of_day(&now), now);
341 assert_eq!(DateTimeAnchor::now().resolve_at_end_of_day(&now), now);
342 #[allow(deprecated)]
343 {
344 assert_eq!(DateTimeAnchor::now().parse_as_start_of_day(&now), now);
345 assert_eq!(DateTimeAnchor::now().parse_as_end_of_day(&now), now);
346 }
347 }
348
349 #[test]
350 fn test_anchor_in_days() {
351 let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 30, 45).unwrap();
352 let anchor = DateTimeAnchor::InDays(1);
353
354 let expected = Utc.with_ymd_and_hms(2025, 1, 2, 0, 0, 0).unwrap();
355
356 let parsed = anchor.resolve_at_start_of_day(&now);
357 assert_eq!(parsed, expected);
358
359 let parsed = anchor.resolve_at_end_of_day(&now);
360 assert!(parsed > Utc.with_ymd_and_hms(2025, 1, 2, 23, 59, 59).unwrap());
361 assert!(parsed < Utc.with_ymd_and_hms(2025, 1, 3, 0, 0, 0).unwrap());
362
363 #[allow(deprecated)]
365 {
366 let parsed = anchor.parse_as_start_of_day(&now);
367 assert_eq!(parsed, expected);
368
369 let parsed = anchor.parse_as_end_of_day(&now);
370 assert!(parsed > Utc.with_ymd_and_hms(2025, 1, 2, 23, 59, 59).unwrap());
371 assert!(parsed < Utc.with_ymd_and_hms(2025, 1, 3, 0, 0, 0).unwrap());
372 }
373 }
374
375 #[test]
376 fn test_anchor_relative() {
377 let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap();
378 let anchor = DateTimeAnchor::Relative(2 * 60 * 60 + 30 * 60 + 45);
379
380 let parsed = anchor.resolve_at_start_of_day(&now);
381 let expected = Utc.with_ymd_and_hms(2025, 1, 1, 17, 30, 45).unwrap();
382 assert_eq!(parsed, expected);
383
384 let parsed = anchor.resolve_at_end_of_day(&now);
385 assert_eq!(parsed, expected);
386
387 #[allow(deprecated)]
389 {
390 let parsed = anchor.parse_as_start_of_day(&now);
391 assert_eq!(parsed, expected);
392
393 let parsed = anchor.parse_as_end_of_day(&now);
394 assert_eq!(parsed, expected);
395 }
396 }
397
398 #[test]
399 fn test_anchor_time_dateonly() {
400 let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 30, 45).unwrap();
401 let date = NaiveDate::from_ymd_opt(2025, 1, 5).unwrap();
402 let loose_date = LooseDateTime::DateOnly(date);
403 let anchor = DateTimeAnchor::DateTime(loose_date);
404
405 let parsed = anchor.resolve_at_start_of_day(&now);
406 let expected = Utc.with_ymd_and_hms(2025, 1, 5, 0, 0, 0).unwrap();
407 assert_eq!(parsed, expected);
408
409 let parsed = anchor.resolve_at_end_of_day(&now);
410 assert!(parsed > Utc.with_ymd_and_hms(2025, 1, 5, 23, 59, 59).unwrap());
411 assert!(parsed < Utc.with_ymd_and_hms(2025, 1, 6, 0, 0, 0).unwrap());
412
413 #[allow(deprecated)]
415 {
416 let parsed = anchor.parse_as_start_of_day(&now);
417 assert_eq!(parsed, expected);
418
419 let parsed = anchor.parse_as_end_of_day(&now);
420 assert!(parsed > Utc.with_ymd_and_hms(2025, 1, 5, 23, 59, 59).unwrap());
421 assert!(parsed < Utc.with_ymd_and_hms(2025, 1, 6, 0, 0, 0).unwrap());
422 }
423 }
424
425 #[test]
426 fn test_anchor_time_floating() {
427 let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 30, 45).unwrap();
428 let date = NaiveDate::from_ymd_opt(2025, 1, 5).unwrap();
429 let time = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
430 let datetime = NaiveDateTime::new(date, time);
431 let loose_datetime = LooseDateTime::Floating(datetime);
432 let anchor = DateTimeAnchor::DateTime(loose_datetime);
433
434 let parsed = anchor.resolve_at_start_of_day(&now);
435 let expected = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
436 assert_eq!(parsed, expected);
437
438 let parsed = anchor.resolve_at_end_of_day(&now);
439 let expected = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
440 assert_eq!(parsed, expected);
441
442 #[allow(deprecated)]
444 {
445 let parsed = anchor.parse_as_start_of_day(&now);
446 assert_eq!(parsed, expected);
447
448 let parsed = anchor.parse_as_end_of_day(&now);
449 assert_eq!(parsed, expected);
450 }
451 }
452
453 #[test]
454 fn test_anchor_time_local() {
455 let now = Utc.with_ymd_and_hms(2025, 1, 1, 15, 30, 45).unwrap();
456 let local_dt = Local.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
457 let loose_local = LooseDateTime::Local(local_dt);
458 let anchor = DateTimeAnchor::DateTime(loose_local);
459
460 let parsed = anchor.resolve_at_start_of_day(&now);
461 let expected = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
462 assert_eq!(parsed, expected);
463
464 let parsed = anchor.resolve_at_end_of_day(&now);
465 let expected = Utc.with_ymd_and_hms(2025, 1, 5, 14, 30, 0).unwrap();
466 assert_eq!(parsed, expected);
467
468 #[allow(deprecated)]
470 {
471 let parsed = anchor.parse_as_start_of_day(&now);
472 assert_eq!(parsed, expected);
473
474 let parsed = anchor.parse_as_end_of_day(&now);
475 assert_eq!(parsed, expected);
476 }
477 }
478
479 #[test]
480 fn test_start_of_day() {
481 let now = Utc.with_ymd_and_hms(2025, 1, 1, 10, 30, 59).unwrap();
482 let parsed = start_of_day(&now);
483 let expected = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
484 assert_eq!(parsed, expected);
485 }
486
487 #[test]
488 fn test_end_of_day() {
489 let now = Utc.with_ymd_and_hms(2025, 1, 1, 10, 30, 0).unwrap();
490 let parsed = end_of_day(&now);
491 let last_sec = Utc.with_ymd_and_hms(2025, 1, 1, 23, 59, 59).unwrap();
492 let next_day = Utc.with_ymd_and_hms(2025, 1, 2, 0, 0, 0).unwrap();
493 assert!(parsed > last_sec);
494 assert!(parsed < next_day);
495 }
496
497 #[test]
498 fn test_from_local_datetime_dst_ambiguity_pick_earliest() {
499 let tz = chrono_tz::America::New_York; let now = NaiveDateTime::new(
501 NaiveDate::from_ymd_opt(2025, 11, 2).unwrap(),
502 NaiveTime::from_hms_opt(1, 30, 0).unwrap(),
503 );
504
505 let parsed = from_local_datetime(&tz, now).with_timezone(&Utc);
506 let expected = Utc.with_ymd_and_hms(2025, 11, 2, 5, 30, 0).unwrap();
507 assert_eq!(parsed, expected);
508 }
509
510 #[test]
511 fn test_time_parsing() {
512 let time = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
514 let anchor = DateTimeAnchor::Time(time);
515
516 let now = Utc.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap();
518 let parsed_start = anchor.resolve_at_start_of_day(&now);
519 let parsed_end = anchor.resolve_at_end_of_day(&now);
520
521 assert_eq!(parsed_start.date_naive(), now.date_naive());
523 assert_eq!(parsed_start.time(), time);
524 assert_eq!(parsed_end.date_naive(), now.date_naive());
525 assert_eq!(parsed_end.time(), time);
526
527 #[allow(deprecated)]
529 {
530 let parsed_start = anchor.parse_as_start_of_day(&now);
531 let parsed_end = anchor.parse_as_end_of_day(&now);
532
533 assert_eq!(parsed_start.date_naive(), now.date_naive());
535 assert_eq!(parsed_start.time(), time);
536 assert_eq!(parsed_end.date_naive(), now.date_naive());
537 assert_eq!(parsed_end.time(), time);
538 }
539 }
540
541 #[test]
542 fn test_resolve_at_in_days() {
543 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap());
544 let anchor = DateTimeAnchor::DateTime(expected);
545 let result = anchor.resolve_at(&expected);
546 assert_eq!(result, expected);
547
548 #[allow(deprecated)]
550 {
551 let result = anchor.parse_from_loose(&expected);
552 assert_eq!(result, expected);
553 }
554 }
555
556 #[test]
557 fn test_resolve_at_relative() {
558 let anchor = DateTimeAnchor::Relative(3 * 60 * 60);
559 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
560 let result = anchor.resolve_at(&now.into());
561 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 15, 0, 0).unwrap());
562 assert_eq!(result, expected);
563
564 #[allow(deprecated)]
566 {
567 let result = anchor.parse_from_loose(&now.into());
568 assert_eq!(result, expected);
569 }
570 }
571
572 #[test]
573 fn test_resolve_at_datetime() {
574 let anchor = DateTimeAnchor::DateTime(LooseDateTime::Local(
575 Local.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
576 ));
577 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
578 let result = anchor.resolve_at(&now.into());
579 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap());
580 assert_eq!(result, expected);
581
582 #[allow(deprecated)]
584 {
585 let result = anchor.parse_from_loose(&now.into());
586 assert_eq!(result, expected);
587 }
588 }
589
590 #[test]
591 fn test_resolve_at_time() {
592 let anchor = DateTimeAnchor::Time(NaiveTime::from_hms_opt(10, 0, 0).unwrap());
593 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
594 let result = anchor.resolve_at(&now.into());
595 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap());
596 assert_eq!(result, expected);
597
598 #[allow(deprecated)]
600 {
601 let result = anchor.parse_from_loose(&now.into());
602 assert_eq!(result, expected);
603 }
604 }
605
606 #[test]
607 fn test_resolve_since_in_days() {
608 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap().into();
609 let expected = now;
610 let anchor = DateTimeAnchor::DateTime(expected);
611 let result = anchor.resolve_since(&now);
612 assert_eq!(result, expected);
613 }
614
615 #[test]
616 fn test_resolve_since_relative() {
617 let anchor = DateTimeAnchor::Relative(3 * 60 * 60 + 25 * 60 + 45);
618 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap().into();
619 let result = anchor.resolve_since(&now);
620 let expected =
621 LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 15, 25, 45).unwrap());
622 assert_eq!(result, expected);
623 }
624
625 #[test]
626 fn test_resolve_since_datetime() {
627 let anchor = DateTimeAnchor::DateTime(LooseDateTime::Local(
628 Local.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
629 ));
630 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap().into();
631 let result = anchor.resolve_since(&now);
632 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap());
633 assert_eq!(result, expected);
634 }
635
636 #[test]
637 fn test_resolve_since_time_before_now() {
638 let anchor = DateTimeAnchor::Time(NaiveTime::from_hms_opt(10, 0, 0).unwrap());
640 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap().into();
641 let result = anchor.resolve_since(&now);
642 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap());
643 assert_eq!(result, expected);
644 }
645
646 #[test]
647 fn test_resolve_since_time_after_now() {
648 let anchor = DateTimeAnchor::Time(NaiveTime::from_hms_opt(14, 0, 0).unwrap());
650 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap().into();
651 let result = anchor.resolve_since(&now);
652 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap());
653 assert_eq!(result, expected);
654 }
655
656 #[test]
657 fn test_resolve_since_datetime_in_days() {
658 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
659 let expected = now.into();
660 let anchor = DateTimeAnchor::DateTime(expected);
661 let result = anchor.resolve_since_datetime(&now);
662 assert_eq!(result, expected);
663
664 #[allow(deprecated)]
666 {
667 let result = anchor.parse_from_dt(&now);
668 assert_eq!(result, expected);
669 }
670 }
671
672 #[test]
673 fn test_resolve_since_datetime_relative() {
674 let anchor = DateTimeAnchor::Relative(3 * 60 * 60 + 25 * 60 + 45);
675 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
676 let result = anchor.resolve_since_datetime(&now);
677 let expected =
678 LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 15, 25, 45).unwrap());
679 assert_eq!(result, expected);
680
681 #[allow(deprecated)]
683 {
684 let result = anchor.parse_from_dt(&now);
685 assert_eq!(result, expected);
686 }
687 }
688
689 #[test]
690 fn test_resolve_since_datetime_datetime() {
691 let anchor = DateTimeAnchor::DateTime(LooseDateTime::Local(
692 Local.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap(),
693 ));
694 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
695 let result = anchor.resolve_since_datetime(&now);
696 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 10, 0, 0).unwrap());
697 assert_eq!(result, expected);
698
699 #[allow(deprecated)]
701 {
702 let result = anchor.parse_from_dt(&now);
703 assert_eq!(result, expected);
704 }
705 }
706
707 #[test]
708 fn test_resolve_since_datetime_time_before_now() {
709 let anchor = DateTimeAnchor::Time(NaiveTime::from_hms_opt(10, 0, 0).unwrap());
711 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
712 let result = anchor.resolve_since_datetime(&now);
713 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 2, 10, 0, 0).unwrap());
714 assert_eq!(result, expected);
715
716 #[allow(deprecated)]
718 {
719 let result = anchor.parse_from_dt(&now);
720 assert_eq!(result, expected);
721 }
722 }
723
724 #[test]
725 fn test_resolve_since_datetime_time_after_now() {
726 let anchor = DateTimeAnchor::Time(NaiveTime::from_hms_opt(14, 0, 0).unwrap());
728 let now = Local.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
729 let result = anchor.resolve_since_datetime(&now);
730 let expected = LooseDateTime::Local(Local.with_ymd_and_hms(2025, 1, 1, 14, 0, 0).unwrap());
731 assert_eq!(result, expected);
732
733 #[allow(deprecated)]
735 {
736 let result = anchor.parse_from_dt(&now);
737 assert_eq!(result, expected);
738 }
739 }
740
741 #[test]
742 fn test_from_str_keywords() {
743 for (s, expected) in [
744 ("now", DateTimeAnchor::now()),
745 ("today", DateTimeAnchor::today()),
746 ("yesterday", DateTimeAnchor::yesterday()),
747 ("tomorrow", DateTimeAnchor::tomorrow()),
748 ] {
749 let anchor: DateTimeAnchor = s.parse().unwrap();
750 assert_eq!(anchor, expected);
751 }
752 }
753
754 #[test]
755 fn test_from_str_datetime() {
756 let anchor: DateTimeAnchor = "2025-01-15 14:30".parse().unwrap();
757 let expected = DateTimeAnchor::DateTime(LooseDateTime::Local(
758 Local.with_ymd_and_hms(2025, 1, 15, 14, 30, 0).unwrap(),
759 ));
760 assert_eq!(anchor, expected);
761 }
762
763 #[test]
764 fn test_from_str_time() {
765 let anchor: DateTimeAnchor = "14:30".parse().unwrap();
766 let expected = DateTimeAnchor::Time(NaiveTime::from_hms_opt(14, 30, 0).unwrap());
767 assert_eq!(anchor, expected);
768 }
769
770 #[test]
771 fn test_from_str_invalid() {
772 let result = DateTimeAnchor::from_str("invalid");
773 assert!(result.is_err());
774 assert!(result.unwrap_err().contains("Invalid datetime anchor"));
775 }
776
777 #[test]
778 fn test_from_str_seconds() {
779 for s in [
780 "10s",
781 "10sec",
782 "10seconds",
783 " 10 s ",
784 " 10 sec ",
785 " 10 seconds ",
786 ] {
787 let anchor: DateTimeAnchor = s.parse().unwrap();
788 let expected = DateTimeAnchor::Relative(10);
789 assert_eq!(anchor, expected, "Failed to parse '{}'", s);
790
791 let prefix_in = DateTimeAnchor::from_str(&format!("in {s}"));
793 assert!(prefix_in.is_err());
794
795 let uppercase = DateTimeAnchor::from_str(&s.to_uppercase());
797 assert!(uppercase.is_err());
798 }
799 }
800
801 #[test]
802 fn test_from_str_minutes() {
803 for s in [
804 "10m",
805 "10min",
806 "10minutes",
807 " 10 m ",
808 " 10 min ",
809 " 10 minutes ",
810 ] {
811 let anchor: DateTimeAnchor = s.parse().unwrap();
812 let expected = DateTimeAnchor::Relative(10 * 60);
813 assert_eq!(anchor, expected, "Failed to parse '{}'", s);
814
815 let prefix_in = DateTimeAnchor::from_str(&format!("in {s}"));
817 assert!(prefix_in.is_err());
818
819 let uppercase = DateTimeAnchor::from_str(&s.to_uppercase());
821 assert!(uppercase.is_err());
822 }
823 }
824
825 #[test]
826 fn test_from_str_hours() {
827 for s in [
828 "in 10hours",
829 "in 10H",
830 " IN 10 hours ",
831 "10hours",
832 "10 HOURS",
833 " 10 hours ",
834 "10h",
835 "10 H",
836 " 10 h ",
837 ] {
838 let anchor: DateTimeAnchor = s.parse().unwrap();
839 let expected = DateTimeAnchor::Relative(10 * 60 * 60);
840 assert_eq!(anchor, expected, "Failed to parse '{}'", s);
841 }
842 }
843
844 #[test]
845 fn test_from_str_days() {
846 for s in [
847 "in 10days",
848 "in 10D",
849 " IN 10 days ",
850 "10days",
851 "10 DAYS",
852 " 10 days ",
853 "10d",
854 "10 D",
855 " 10 d ",
856 ] {
857 let anchor: DateTimeAnchor = s.parse().unwrap();
858 let expected = DateTimeAnchor::InDays(10);
859 assert_eq!(anchor, expected, "Failed to parse '{}'", s);
860 }
861 }
862
863 #[test]
864 fn test_next_suggested_time() {
865 let test_cases = vec![
866 (8, 30, 9, "Before 9 AM, should suggest 9 AM"),
868 (
869 10,
870 30,
871 13,
872 "After 9 AM but before 1 PM, should suggest 1 PM",
873 ),
874 (
875 14,
876 30,
877 18,
878 "After 1 PM but before 6 PM, should suggest 6 PM",
879 ),
880 (9, 0, 13, "Exactly at 9 AM, should suggest 1 PM"),
881 (13, 0, 18, "Exactly at 1 PM, should suggest 6 PM"),
882 ];
883
884 for (hour, min, expected_hour, description) in test_cases {
885 let now = Local.with_ymd_and_hms(2025, 1, 1, hour, min, 0).unwrap();
886 let result = next_suggested_time(&now.naive_local());
887 let expected = LooseDateTime::Local(
888 Local
889 .with_ymd_and_hms(2025, 1, 1, expected_hour, 0, 0)
890 .unwrap(),
891 );
892 assert_eq!(result, expected, "{}", description);
893 }
894 }
895
896 #[test]
897 fn test_next_suggested_time_after_6pm() {
898 let now = Local.with_ymd_and_hms(2025, 1, 1, 19, 30, 0).unwrap();
900 let result = next_suggested_time(&now.naive_local());
901 let expected = LooseDateTime::DateOnly(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap());
902 assert_eq!(result, expected, "After 6 PM, should suggest DateOnly");
903
904 let now = Local.with_ymd_and_hms(2025, 1, 1, 18, 0, 0).unwrap();
906 let result = next_suggested_time(&now.naive_local());
907 let expected = LooseDateTime::DateOnly(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap());
908 assert_eq!(result, expected, "Exactly at 6 PM, should suggest DateOnly");
909 }
910}