1use std::ops::Add;
13use std::ops::Range;
14use std::ops::RangeInclusive;
15use std::ops::Sub;
16use std::sync::atomic::AtomicI32;
17use std::sync::atomic::AtomicU64;
18use std::sync::atomic::Ordering;
19
20use chrono::prelude::*;
21use chrono::Duration;
22use chrono::LocalResult;
23
24#[cfg(feature = "serde")]
25mod serde_impl;
26
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
31pub struct HgTime {
32 pub unixtime: i64,
33 pub offset: i32,
34}
35
36const DEFAULT_FORMATS: [&str; 35] = [
37 "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M", "%Y-%m-%dT%H%M%S", "%Y-%m-%dT%H%M", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d %H%M%S", "%Y-%m-%d %H%M", "%Y-%m-%d %I:%M:%S%p",
47 "%Y-%m-%d %H:%M",
48 "%Y-%m-%d %I:%M%p",
49 "%a %b %d %H:%M:%S %Y",
50 "%a %b %d %I:%M:%S%p %Y",
51 "%a, %d %b %Y %H:%M:%S", "%b %d %H:%M:%S %Y",
53 "%b %d %I:%M:%S%p %Y",
54 "%b %d %H:%M:%S",
55 "%b %d %I:%M:%S%p",
56 "%b %d %H:%M",
57 "%b %d %I:%M%p",
58 "%m-%d",
59 "%m/%d",
60 "%Y-%m-%d",
61 "%m/%d/%y",
62 "%m/%d/%Y",
63 "%b",
64 "%b %d",
65 "%b %Y",
66 "%b %d %Y",
67 "%I:%M%p",
68 "%H:%M",
69 "%H:%M:%S",
70 "%I:%M:%S%p",
71 "%Y",
72 "%Y-%m",
73];
74
75const INVALID_OFFSET: i32 = i32::MAX;
76static DEFAULT_OFFSET: AtomicI32 = AtomicI32::new(INVALID_OFFSET);
77static FORCED_NOW: AtomicU64 = AtomicU64::new(0); macro_rules! with_local_timezone {
86 (|$tz:ident| { $($expr: tt)* }) => {
87 {
88 let offset = DEFAULT_OFFSET.load(Ordering::Acquire);
89 match FixedOffset::west_opt(offset) {
90 Some($tz) => {
91 $($expr)*
92 },
93 None => {
94 let $tz = Local;
95 $($expr)*
96 },
97 }
98 }
99 };
100}
101
102impl HgTime {
103 pub const RANGE: RangeInclusive<HgTime> = Self::min_value()..=Self::max_value();
109
110 pub fn now() -> Option<Self> {
115 let forced_now = FORCED_NOW.load(Ordering::Acquire);
116 if forced_now == 0 {
117 Self::try_from(Local::now()).ok().and_then(|mut t: HgTime| {
118 let offset = DEFAULT_OFFSET.load(Ordering::Acquire);
119 if is_valid_offset(offset) {
120 t.offset = offset;
121 }
122 t.bounded()
123 })
124 } else {
125 Some(Self::from_compact_u64(forced_now))
126 }
127 }
128
129 pub fn to_utc(self) -> DateTime<Utc> {
130 let naive = DateTime::from_timestamp(self.unixtime, 0).unwrap();
131 naive.to_utc()
132 }
133
134 fn to_naive(self) -> NaiveDateTime {
136 DateTime::from_timestamp(self.unixtime - self.offset as i64, 0)
137 .unwrap()
138 .naive_local()
139 }
140
141 pub fn set_as_now_for_testing(self) {
145 FORCED_NOW.store(self.to_lossy_compact_u64(), Ordering::SeqCst);
146 }
147
148 pub fn clear_now_for_testing() {
150 FORCED_NOW.store(0, Ordering::Release);
151 }
152
153 pub fn parse(date: &str) -> Option<Self> {
160 match date {
161 "now" => Self::now(),
162 "today" => Self::now().and_then(|now| {
163 Self::try_from(now.to_naive().date().and_hms_opt(0, 0, 0).unwrap()).ok()
164 }),
165 "yesterday" => Self::now().and_then(|now| {
166 Self::try_from(
167 (now.to_naive().date() - Duration::days(1))
168 .and_hms_opt(0, 0, 0)
169 .unwrap(),
170 )
171 .ok()
172 }),
173 date if date.ends_with(" ago") => {
174 let duration_str = &date[..date.len() - 4];
175 duration_str
176 .parse::<humantime::Duration>()
177 .ok()
178 .and_then(|duration| Self::now().and_then(|n| n - duration.as_secs()))
179 }
180 _ => Self::parse_absolute(date, &default_date_lower),
181 }
182 }
183
184 pub fn parse_range(date: &str) -> Option<Range<Self>> {
192 Self::parse_range_internal(date, true)
193 }
194
195 fn parse_range_internal(date: &str, support_to: bool) -> Option<Range<Self>> {
196 match date {
197 "now" => Self::now().and_then(|n| (n + 1).map(|m| n..m)),
198 "today" => Self::now().and_then(|now| {
199 let date = now.to_naive().date();
200 let start = Self::try_from(date.and_hms_opt(0, 0, 0).unwrap());
201 let end = Self::try_from(date.and_hms_opt(23, 59, 59).unwrap()).map(|t| t + 1);
202 if let (Ok(start), Ok(Some(end))) = (start, end) {
203 Some(start..end)
204 } else {
205 None
206 }
207 }),
208 "yesterday" => Self::now().and_then(|now| {
209 let date = now.to_naive().date() - Duration::days(1);
210 let start = Self::try_from(date.and_hms_opt(0, 0, 0).unwrap());
211 let end = Self::try_from(date.and_hms_opt(23, 59, 59).unwrap()).map(|t| t + 1);
212 if let (Ok(start), Ok(Some(end))) = (start, end) {
213 Some(start..end)
214 } else {
215 None
216 }
217 }),
218 date if date.starts_with('>') => {
219 Self::parse(&date[1..]).map(|start| start..Self::max_value())
220 }
221 date if date.starts_with("since ") => {
222 Self::parse(&date[6..]).map(|start| start..Self::max_value())
223 }
224 date if date.starts_with('<') => Self::parse(&date[1..])
225 .and_then(|end| end + 1)
226 .map(|end| Self::min_value()..end),
227 date if date.starts_with('-') => {
228 Self::parse_range(&format!("since {} days ago", &date[1..]))
231 }
232 date if date.starts_with("before ") => {
233 Self::parse(&date[7..]).map(|end| Self::min_value()..end)
234 }
235 date if support_to && date.contains(" to ") => {
236 let phrases: Vec<_> = date.split(" to ").collect();
237 if phrases.len() == 2 {
238 if let (Some(start), Some(end)) = (
239 Self::parse_range_internal(phrases[0], false),
240 Self::parse_range_internal(phrases[1], false),
241 ) {
242 Some(start.start..end.end)
243 } else {
244 None
245 }
246 } else {
247 None
248 }
249 }
250 _ => {
251 let start = Self::parse_absolute(date, &default_date_lower);
252 let end = Self::parse_absolute(date, &|c| default_date_upper(c, "31"))
253 .or_else(|| Self::parse_absolute(date, &|c| default_date_upper(c, "30")))
254 .or_else(|| Self::parse_absolute(date, &|c| default_date_upper(c, "29")))
255 .or_else(|| Self::parse_absolute(date, &|c| default_date_upper(c, "28")))
256 .and_then(|end| end + 1);
257 if let (Some(start), Some(end)) = (start, end) {
258 Some(start..end)
259 } else {
260 None
261 }
262 }
263 }
264 }
265
266 pub fn parse_hg_internal_format(date: &str) -> Option<Option<Self>> {
270 let parts: Vec<_> = date.split(' ').collect();
271 if parts.len() == 2 {
272 let unixtime = parts[0].parse::<i64>().ok();
273 let unixtime = if unixtime.is_none() {
274 parts[0].parse::<f64>().map(|x| x as i64).ok()
275 } else {
276 unixtime
277 };
278 if let Some(unixtime) = unixtime {
279 if let Ok(offset) = parts[1].parse() {
280 if is_valid_offset(offset) {
281 return Some(Self { unixtime, offset }.bounded());
282 }
283 }
284 }
285 }
286 None
287 }
288
289 fn parse_absolute(date: &str, default_date: &dyn Fn(char) -> &'static str) -> Option<Self> {
296 let date = date.trim();
297
298 if let Some(hg_internal) = Self::parse_hg_internal_format(date) {
299 return hg_internal;
300 }
301
302 let date = if date.ends_with("GMT") || date.ends_with("UTC") {
305 format!("{} +0000", &date[..date.len() - 3])
306 } else {
307 date.to_string()
308 };
309 let mut now = None; for naive_format in DEFAULT_FORMATS.iter() {
313 let mut default_format = String::new();
317 let mut date_with_defaults = date.clone();
318 let mut use_now = false;
319 for part in ["S", "M", "HI", "d", "mb", "Yy"] {
320 if part
321 .chars()
322 .any(|ch| naive_format.contains(&format!("%{}", ch)))
323 {
324 use_now = true;
328 } else {
329 let format_char = part.chars().next().unwrap();
330 default_format += &format!(" @%{}", format_char);
331 if use_now {
332 now = now.or_else(|| Self::now().map(|n| n.to_naive()));
336 match now {
337 Some(now) => {
338 date_with_defaults +=
339 &format!(" @{}", now.format(&format!("%{}", format_char)))
340 }
341 None => return None,
342 }
343 } else {
344 date_with_defaults += " @";
348 date_with_defaults += default_date(format_char);
349 }
350 }
351 }
352
353 let format = format!("{}%#z{}", naive_format, default_format);
356 if let Ok(parsed) = DateTime::parse_from_str(&date_with_defaults, &format) {
357 if let Ok(parsed) = parsed.try_into() {
358 return Some(parsed);
359 }
360 }
361
362 let format = format!("{}{}", naive_format, default_format);
364 if let Ok(parsed) = NaiveDateTime::parse_from_str(&date_with_defaults, &format) {
365 if let Ok(parsed) = parsed.try_into() {
366 return Some(parsed);
367 }
368 }
369 }
370
371 None
372 }
373
374 pub const fn min_value() -> Self {
376 Self {
377 unixtime: -2208988800, offset: 0,
379 }
380 }
381
382 pub const fn max_value() -> Self {
384 Self {
385 unixtime: 253402300799, offset: 0,
387 }
388 }
389
390 pub fn bounded(self) -> Option<Self> {
392 if self < Self::min_value() || self > Self::max_value() {
393 None
394 } else {
395 Some(self)
396 }
397 }
398}
399
400impl HgTime {
403 fn to_lossy_compact_u64(self) -> u64 {
404 ((self.unixtime as u64) << 17) + (self.offset + 50401) as u64
405 }
406
407 fn from_compact_u64(value: u64) -> Self {
408 let unixtime = (value as i64) >> 17;
409 let offset = (((value & 0x1ffff) as i64) - 50401) as i32;
410 Self { unixtime, offset }
411 }
412}
413
414impl From<HgTime> for NaiveDateTime {
415 fn from(time: HgTime) -> Self {
416 time.to_naive()
417 }
418}
419
420impl From<HgTime> for DateTime<Utc> {
421 fn from(time: HgTime) -> Self {
422 time.to_utc()
423 }
424}
425
426impl Add<u64> for HgTime {
427 type Output = Option<Self>;
428
429 fn add(self, seconds: u64) -> Option<Self> {
430 seconds.try_into().ok().and_then(|seconds| {
431 self.unixtime.checked_add(seconds).and_then(|unixtime| {
432 Self {
433 unixtime,
434 offset: self.offset,
435 }
436 .bounded()
437 })
438 })
439 }
440}
441
442impl Sub<u64> for HgTime {
443 type Output = Option<Self>;
444
445 fn sub(self, seconds: u64) -> Option<Self> {
446 seconds.try_into().ok().and_then(|seconds| {
447 self.unixtime.checked_sub(seconds).and_then(|unixtime| {
448 Self {
449 unixtime,
450 offset: self.offset,
451 }
452 .bounded()
453 })
454 })
455 }
456}
457
458impl PartialOrd for HgTime {
459 fn partial_cmp(&self, other: &HgTime) -> Option<std::cmp::Ordering> {
460 self.unixtime.partial_cmp(&other.unixtime)
461 }
462}
463
464impl<Tz: TimeZone> TryFrom<DateTime<Tz>> for HgTime {
465 type Error = ();
466 fn try_from(time: DateTime<Tz>) -> Result<Self, ()> {
467 Self {
468 unixtime: time.timestamp(),
469 offset: time.offset().fix().utc_minus_local(),
470 }
471 .bounded()
472 .ok_or(())
473 }
474}
475
476impl<Tz: TimeZone> TryFrom<LocalResult<DateTime<Tz>>> for HgTime {
477 type Error = ();
478 fn try_from(time: LocalResult<DateTime<Tz>>) -> Result<Self, ()> {
479 match time {
480 LocalResult::Single(datetime) => HgTime::try_from(datetime),
481 _ => Err(()),
482 }
483 }
484}
485
486impl TryFrom<NaiveDateTime> for HgTime {
487 type Error = ();
488 fn try_from(time: NaiveDateTime) -> Result<Self, ()> {
489 with_local_timezone!(|tz| { tz.from_local_datetime(&time).try_into() })
490 }
491}
492
493pub fn set_default_offset(offset: i32) {
495 DEFAULT_OFFSET.store(offset, Ordering::SeqCst);
496}
497
498fn is_valid_offset(offset: i32) -> bool {
499 (-50400..=43200).contains(&offset)
501}
502
503fn default_date_lower(format_char: char) -> &'static str {
505 match format_char {
506 'H' | 'M' | 'S' => "00",
507 'm' | 'd' => "1",
508 _ => unreachable!(),
509 }
510}
511
512fn default_date_upper(format_char: char, max_day: &'static str) -> &'static str {
514 match format_char {
515 'H' => "23",
516 'M' | 'S' => "59",
517 'm' => "12",
518 'd' => max_day,
519 _ => unreachable!(),
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn test_naive_local_roundtrip() {
529 let now = Local::now().with_nanosecond(0).unwrap().naive_local();
530 let hgtime: HgTime = now.try_into().unwrap();
531 let now_again = hgtime.to_naive();
532 assert_eq!(now, now_again);
533 }
534
535 #[test]
536 fn test_parse_date() {
537 set_default_offset(7200);
540
541 assert_eq!(t("2006-02-01 13:00:30"), t("2006-02-01 13:00:30-0200"));
546 assert_eq!(t("2006-02-01 13:00:30-0500"), "1138816830 18000");
547 assert_eq!(t("2006-02-01 13:00:30 +05:00"), "1138780830 -18000");
548 assert_eq!(t("2006-02-01 13:00:30Z"), "1138798830 0");
549 assert_eq!(t("2006-02-01 13:00:30 GMT"), "1138798830 0");
550 assert_eq!(t("2006-4-5 13:30"), "1144251000 7200");
551 assert_eq!(t("1150000000 14400"), "1150000000 14400");
552 assert_eq!(t("100000 1400000"), "fail");
553 assert_eq!(t("1000000000 -16200"), "1000000000 -16200");
554 assert_eq!(t("2006-02-01 1:00:30PM +0000"), "1138798830 0");
555
556 assert_eq!(d("1:00:30PM +0000", Duration::days(1)), "0");
557 assert_eq!(d("02/01", Duration::weeks(52)), "0");
558 assert_eq!(d("today", Duration::days(1)), "0");
559 assert_eq!(d("yesterday", Duration::days(2)), "0");
560
561 assert_eq!(t("2016-07-27T12:10:21"), "1469628621 7200");
563 assert_eq!(t("2016-07-27T12:10:21Z"), "1469621421 0");
564 assert_eq!(t("2016-07-27T12:10:21+00:00"), "1469621421 0");
565 assert_eq!(t("2016-07-27T121021Z"), "1469621421 0");
566 assert_eq!(t("2016-07-27 12:10:21"), "1469628621 7200");
567 assert_eq!(t("2016-07-27 12:10:21Z"), "1469621421 0");
568 assert_eq!(t("2016-07-27 12:10:21+00:00"), "1469621421 0");
569 assert_eq!(t("2016-07-27 121021Z"), "1469621421 0");
570
571 assert_eq!(t("Jan 2018"), "1514772000 7200");
573 assert_eq!(t("Feb 2018"), "1517450400 7200");
574 assert_eq!(t("Mar 2018"), "1519869600 7200");
575 assert_eq!(t("Apr 2018"), "1522548000 7200");
576 assert_eq!(t("May 2018"), "1525140000 7200");
577 assert_eq!(t("Jun 2018"), "1527818400 7200");
578 assert_eq!(t("Jul 2018"), "1530410400 7200");
579 assert_eq!(t("Sep 2018"), "1535767200 7200");
580 assert_eq!(t("Oct 2018"), "1538359200 7200");
581 assert_eq!(t("Nov 2018"), "1541037600 7200");
582 assert_eq!(t("Dec 2018"), "1543629600 7200");
583 assert_eq!(t("Foo 2018"), "fail");
584
585 assert_eq!(d("Jan", Duration::weeks(52)), "0");
587 assert_eq!(d("Jan 1", Duration::weeks(52)), "0"); assert_eq!(d("4-26", Duration::weeks(52)), "0");
589 assert_eq!(d("4/26", Duration::weeks(52)), "0");
590 assert_eq!(t("4/26/2000"), "956714400 7200");
591 assert_eq!(t("Apr 26 2000"), "956714400 7200");
592 assert_eq!(t("2020"), "1577844000 7200"); assert_eq!(t("2020 GMT"), "1577836800 0");
594 assert_eq!(t("2020-12"), "1606788000 7200");
595 assert_eq!(t("2020-13"), "fail");
596 assert_eq!(t("1000"), "fail"); assert_eq!(t("1"), "fail");
598 assert_eq!(t("0"), "fail");
599 assert_eq!(t("100000000000000000 1400"), "fail");
600
601 assert_eq!(t("Fri, 20 Sep 2019 12:15:13 -0700"), "1569006913 25200"); assert_eq!(t("Fri, 20 Sep 2019 12:15:13"), "1568988913 7200");
603 }
604
605 #[test]
606 fn test_parse_ago() {
607 set_default_offset(7200);
608 assert_eq!(d("10m ago", Duration::hours(1)), "0");
609 assert_eq!(d("10 min ago", Duration::hours(1)), "0");
610 assert_eq!(d("10 minutes ago", Duration::hours(1)), "0");
611 assert_eq!(d("10 hours ago", Duration::days(1)), "0");
612 assert_eq!(d("10 h ago", Duration::days(1)), "0");
613 assert_eq!(t("9999999 years ago"), "fail");
614 }
615
616 #[test]
617 fn test_parse_range() {
618 set_default_offset(7200);
619
620 assert_eq!(c("since 1 month ago", "now"), "contains");
621 assert_eq!(c("since 1 month ago", "2 months ago"), "does not contain");
622 assert_eq!(c("> 1 month ago", "2 months ago"), "does not contain");
623 assert_eq!(c("< 1 month ago", "2 months ago"), "contains");
624 assert_eq!(c("< 1 month ago", "now"), "does not contain");
625
626 assert_eq!(c("-3", "now"), "contains");
627 assert_eq!(c("-3", "2 days ago"), "contains");
628 assert_eq!(c("-3", "4 days ago"), "does not contain");
629
630 assert_eq!(c("2018", "2017-12-31 23:59:59"), "does not contain");
631 assert_eq!(c("2018", "2018-1-1"), "contains");
632 assert_eq!(c("2018", "2018-12-31 23:59:59"), "contains");
633 assert_eq!(c("2018", "2019-1-1"), "does not contain");
634
635 assert_eq!(c("2018-5-1 to 2018-6-2", "2018-4-30"), "does not contain");
636 assert_eq!(c("2018-5-1 to 2018-6-2", "2018-5-30"), "contains");
637 assert_eq!(c("2018-5-1 to 2018-6-2", "2018-6-30"), "does not contain");
638 assert_eq!(c("2018-5 to 2018-6", "2018-5-1 0:0:0"), "contains");
639 assert_eq!(c("2018-5 to 2018-6", "2018-6-30 23:59:59"), "contains");
640 assert_eq!(c("2018-5 to 2018-6 to 2018-7", "2018-6-30"), "fail");
641
642 let range = HgTime::parse_range("yesterday to today").unwrap();
645 assert!(range.end.unixtime - range.start.unixtime >= (24 + 20) * 3600);
646 }
647
648 #[test]
649 fn test_parse_internal_format() {
650 assert_eq!(
651 HgTime::parse_hg_internal_format("123 456")
652 .unwrap()
653 .unwrap(),
654 HgTime {
655 unixtime: 123,
656 offset: 456
657 }
658 );
659 assert_eq!(
660 HgTime::parse_hg_internal_format("123.7 456")
661 .unwrap()
662 .unwrap(),
663 HgTime {
664 unixtime: 123,
665 offset: 456
666 }
667 );
668 assert_eq!(HgTime::parse_hg_internal_format("foobar"), None);
669 assert_eq!(
670 HgTime::parse_hg_internal_format("100000000000000000 1400"),
671 Some(None)
672 );
673 }
674
675 fn t(date: &str) -> String {
677 match HgTime::parse(date) {
678 Some(time) => format!("{} {}", time.unixtime, time.offset),
679 None => "fail".to_string(),
680 }
681 }
682
683 fn d(date: &str, duration: Duration) -> String {
685 match HgTime::parse(date) {
686 Some(time) => {
687 let value = (time.unixtime - HgTime::now().unwrap().unixtime).abs()
688 / duration.num_seconds();
689 format!("{}", value)
690 }
691 None => "fail".to_string(),
692 }
693 }
694
695 fn c(range: &str, date: &str) -> &'static str {
698 if let (Some(range), Some(date)) = (HgTime::parse_range(range), HgTime::parse(date)) {
699 if range.contains(&date) {
700 "contains"
701 } else {
702 "does not contain"
703 }
704 } else {
705 "fail"
706 }
707 }
708}