below_common/
dateutil.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/*
16 * Copyright (c) Facebook, Inc. and its affiliates.
17 *
18 * This software may be used and distributed according to the terms of the
19 * GNU General Public License version 2.
20 */
21
22//! # dateutil
23//!
24//! See [`HgTime`] and [`HgTime::parse`] for main features.
25
26use std::ops::Add;
27use std::ops::Range;
28use std::ops::Sub;
29use std::sync::atomic::AtomicI32;
30use std::sync::atomic::Ordering;
31use std::time::SystemTime;
32
33use chrono::prelude::*;
34use chrono::Duration;
35use chrono::Local;
36use chrono::NaiveDateTime;
37use chrono::NaiveTime;
38use chrono::TimeZone;
39use regex::Regex;
40
41/// A simple time structure that matches hg's time representation.
42///
43/// Internally it's unixtime (in GMT), and offset (GMT -1 = +3600).
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct HgTime {
46    pub unixtime: u64,
47    pub offset: i32,
48}
49
50const DEFAULT_FORMATS: [&str; 44] = [
51    // mercurial/util.py defaultdateformats
52    "%Y-%m-%dT%H:%M:%S", // the 'real' ISO8601
53    "%Y-%m-%dT%H:%M",    //   without seconds
54    "%Y-%m-%dT%H%M%S",   // another awful but legal variant without :
55    "%Y-%m-%dT%H%M",     //   without seconds
56    "%Y-%m-%d %H:%M:%S", // our common legal variant
57    "%Y-%m-%d %H:%M",    //   without seconds
58    "%Y-%m-%d %H%M%S",   // without :
59    "%Y-%m-%d %H%M",     //   without seconds
60    "%Y-%m-%d %I:%M:%S%p",
61    "%Y-%m-%d %H:%M",
62    "%Y-%m-%d %I:%M%p",
63    "%a %b %d %H:%M:%S %Y",
64    "%a %b %d %I:%M:%S%p %Y",
65    "%a, %d %b %Y %H:%M:%S", //  GNU coreutils "/bin/date --rfc-2822"
66    "%b %d %H:%M:%S %Y",
67    "%b %d %I:%M:%S%p %Y",
68    "%b %d %H:%M:%S",
69    "%b %d %I:%M:%S%p",
70    "%b %d %H:%M",
71    "%b %d %I:%M%p",
72    "%m-%d",
73    "%m/%d",
74    "%Y-%m-%d",
75    "%m/%d/%y",
76    "%m/%d/%Y",
77    "%b",
78    "%b %d",
79    "%b %Y",
80    "%b %d %Y",
81    "%I:%M%p",
82    "%H:%M",
83    "%H:%M:%S",
84    "%I:%M:%S%p",
85    "%Y",
86    "%Y-%m",
87    "%m/%d/%Y %I:%M:%S %P",
88    "%m/%d/%Y %I:%M:%S%p",
89    "%m/%d/%Y %H:%M:%S",
90    "%m/%d/%Y %I:%M%p",
91    "%m/%d/%Y %H:%M",
92    "%m/%d %I:%M:%S%p",
93    "%m/%d %H:%M:%S",
94    "%m/%d %I:%M%p",
95    "%m/%d %H:%M",
96];
97
98const TIME_OF_DAY_FORMATS: [&str; 6] = [
99    "%I:%M%p",
100    "%I:%M%P",
101    "%I:%M:%S%p",
102    "%I:%M:%S%P",
103    "%H:%M",
104    "%H:%M:%S",
105];
106
107const INVALID_OFFSET: i32 = i32::max_value();
108static DEFAUL_OFFSET: AtomicI32 = AtomicI32::new(INVALID_OFFSET);
109
110fn today() -> NaiveDate {
111    Local::now().date_naive()
112}
113
114impl HgTime {
115    pub fn now() -> Self {
116        let now: HgTime = Local::now().into();
117        now.use_default_offset()
118    }
119
120    /// Parse a date string.
121    ///
122    /// Return `None` if it cannot be parsed.
123    ///
124    /// This function matches `mercurial.util.parsedate`, and can parse
125    /// some additional forms like `2 days ago`.
126    /// It can also handle future forms like "ten hours from now" and "+10h"
127    pub fn parse(date: &str) -> Option<Self> {
128        match date {
129            "now" => Some(Self::now()),
130            "today" => Some(Self::from(today().and_hms_opt(0, 0, 0).unwrap()).use_default_offset()),
131            "yesterday" => Some(
132                Self::from(today().and_hms_opt(0, 0, 0).unwrap() - Duration::days(1))
133                    .use_default_offset(),
134            ),
135            "tomorrow" => Some(
136                Self::from(today().and_hms_opt(0, 0, 0).unwrap() + Duration::days(1))
137                    .use_default_offset(),
138            ),
139            "day after tomorrow" | "the day after tomorrow" | "overmorrow" => Some(
140                Self::from(today().and_hms_opt(0, 0, 0).unwrap() + Duration::days(2))
141                    .use_default_offset(),
142            ),
143            // Match all string ends with [dhms] or ago but not ends with pm and am. Case insensitive.
144            // We have to use two regex here is because regex crate doesn't support negative lookbehind
145            // expression.
146            date if match (
147                Regex::new(r"(?i)\+?.*([dhms]|ago|from now)$"),
148                Regex::new(r"(?i)(pm|am)$"),
149            ) {
150                (Ok(relative_re), Ok(ampm_re)) => {
151                    relative_re.is_match(date) && !ampm_re.is_match(date)
152                }
153                _ => false,
154            } =>
155            {
156                let date = date.to_ascii_lowercase();
157                let plus = date.starts_with('+') && !date.ends_with("ago");
158                let from_now = date.ends_with("from now");
159                // Trim past/future markers down to just date
160                let mut duration_str = if plus { &date[1..] } else { &date };
161                duration_str = if date.ends_with("ago") {
162                    &duration_str[..duration_str.len() - 4]
163                } else {
164                    duration_str
165                };
166                duration_str = if from_now {
167                    &duration_str[..duration_str.len() - 9]
168                } else {
169                    duration_str
170                };
171
172                if plus || from_now {
173                    duration_str
174                        .parse::<humantime::Duration>()
175                        .ok()
176                        .map(|duration| Self::now() + duration.as_secs())
177                } else {
178                    duration_str
179                        .parse::<humantime::Duration>()
180                        .ok()
181                        .map(|duration| Self::now() - duration.as_secs())
182                }
183            }
184            date if match Regex::new(r"^\d{10}$") {
185                Ok(systime_re) => systime_re.is_match(date),
186                _ => false,
187            } =>
188            {
189                match date.parse::<u64>() {
190                    Ok(d) => Some(
191                        Self {
192                            unixtime: d,
193                            offset: 0,
194                        }
195                        .use_default_offset(),
196                    ),
197                    _ => None,
198                }
199            }
200            _ => Self::parse_absolute(date, default_date_lower),
201        }
202    }
203
204    /// Parse a date string as a range.
205    ///
206    /// For example, `Apr 2000` covers range `Apr 1, 2000` to `Apr 30, 2000`.
207    /// Also support more explicit ranges:
208    /// - START to END
209    /// - > START
210    /// - < END
211    #[allow(dead_code)]
212    pub fn parse_range(date: &str) -> Option<Range<Self>> {
213        match date {
214            "now" => {
215                let now = Self::now();
216                Some(now..now + 1)
217            }
218            "today" => {
219                let date = today();
220                let start = Self::from(date.and_hms_opt(0, 0, 0).unwrap()).use_default_offset();
221                let end =
222                    Self::from(date.and_hms_opt(23, 59, 59).unwrap()).use_default_offset() + 1;
223                Some(start..end)
224            }
225            "yesterday" => {
226                let date = today() - Duration::days(1);
227                let start = Self::from(date.and_hms_opt(0, 0, 0).unwrap()).use_default_offset();
228                let end =
229                    Self::from(date.and_hms_opt(23, 59, 59).unwrap()).use_default_offset() + 1;
230                Some(start..end)
231            }
232            "tomorrow" => {
233                let date = today() + Duration::days(1);
234                let start = Self::from(date.and_hms_opt(0, 0, 0).unwrap()).use_default_offset();
235                let end =
236                    Self::from(date.and_hms_opt(23, 59, 59).unwrap()).use_default_offset() + 1;
237                Some(start..end)
238            }
239            "day after tomorrow" | "the day after tomorrow" | "overmorrow" => {
240                let date = today() + Duration::days(2);
241                let start = Self::from(date.and_hms_opt(0, 0, 0).unwrap()).use_default_offset();
242                let end =
243                    Self::from(date.and_hms_opt(23, 59, 59).unwrap()).use_default_offset() + 1;
244                Some(start..end)
245            }
246            date if date.starts_with('>') => {
247                Self::parse(&date[1..]).map(|start| start..Self::max_value())
248            }
249            date if date.starts_with("since ") => {
250                Self::parse(&date[6..]).map(|start| start..Self::max_value())
251            }
252            date if date.starts_with('<') => {
253                Self::parse(&date[1..]).map(|end| Self::min_value()..end)
254            }
255            date if date.starts_with('-') => {
256                // This does not really make much sense. But is supported by hg
257                // (see 'hg help dates').
258                Self::parse_range(&format!("since {} days ago", &date[1..]))
259            }
260            date if date.starts_with("before ") => {
261                Self::parse(&date[7..]).map(|end| Self::min_value()..end)
262            }
263            date if date.contains(" to ") => {
264                let phrases: Vec<_> = date.split(" to ").collect();
265                if phrases.len() == 2 {
266                    if let (Some(start), Some(end)) =
267                        (Self::parse(phrases[0]), Self::parse(phrases[1]))
268                    {
269                        Some(start..end)
270                    } else {
271                        None
272                    }
273                } else {
274                    None
275                }
276            }
277            _ => {
278                let start = Self::parse_absolute(date, default_date_lower);
279                let end = Self::parse_absolute(date, default_date_upper::<N31>)
280                    .or_else(|| Self::parse_absolute(date, default_date_upper::<N30>))
281                    .or_else(|| Self::parse_absolute(date, default_date_upper::<N29>))
282                    .or_else(|| Self::parse_absolute(date, default_date_upper::<N28>));
283                if let (Some(start), Some(end)) = (start, end) {
284                    Some(start..end + 1)
285                } else {
286                    None
287                }
288            }
289        }
290    }
291
292    /// Parse date in an absolute form.
293    ///
294    /// Return None if it cannot be parsed.
295    ///
296    /// `default_date` takes a format char, for example, `H`, and returns a
297    /// default value of it.
298    fn parse_absolute(date: &str, default_date: fn(char) -> &'static str) -> Option<Self> {
299        let date = date.trim();
300
301        // Hg internal format. "unixtime offset"
302        let parts: Vec<_> = date.split(' ').collect();
303        if parts.len() == 2 {
304            if let Ok(unixtime) = parts[0].parse() {
305                if let Ok(offset) = parts[1].parse() {
306                    if is_valid_offset(offset) {
307                        return Some(Self { unixtime, offset });
308                    }
309                }
310            }
311        }
312
313        // Normalize UTC timezone name to +0000. The parser does not know
314        // timezone names.
315        let date = if date.ends_with("GMT") || date.ends_with("UTC") {
316            format!("{} +0000", &date[..date.len() - 3])
317        } else {
318            date.to_string()
319        };
320        let mut now = None; // cached, lazily calculated "now"
321
322        // Try all formats!
323        for naive_format in DEFAULT_FORMATS.iter() {
324            // Fill out default fields.  See mercurial.util.strdate.
325            // This makes it possible to parse partial dates like "month/day",
326            // or "hour:minute", since the missing fields will be filled.
327            let mut default_format = String::new();
328            let mut date_with_defaults = date.clone();
329            let mut use_now = false;
330            for part in ["S", "M", "HI", "d", "mb", "Yy"] {
331                if part
332                    .chars()
333                    .any(|ch| naive_format.contains(&format!("%{}", ch)))
334                {
335                    // For example, if the user specified "d" (day), but
336                    // not other things, we should use 0 for "H:M:S", and
337                    // "now" for "Y-m" (year, month).
338                    use_now = true;
339                } else {
340                    let format_char = part.chars().nth(0).unwrap();
341                    default_format += &format!(" @%{}", format_char);
342                    if use_now {
343                        // For example, if the user only specified "month/day",
344                        // then we should use the current "year", instead of
345                        // year 0.
346                        let now = now.get_or_insert_with(Local::now);
347                        date_with_defaults +=
348                            &format!(" @{}", now.format(&format!("%{}", format_char)));
349                    } else {
350                        // For example, if the user only specified
351                        // "hour:minute", then we should use "second 0", instead
352                        // of the current second.
353                        date_with_defaults += " @";
354                        date_with_defaults += default_date(format_char);
355                    }
356                }
357            }
358
359            // Try parse with timezone.
360            // See https://docs.rs/chrono/0.4.9/chrono/format/strftime/index.html#specifiers
361            let format = format!("{}%#z{}", naive_format, default_format);
362            if let Ok(parsed) = DateTime::parse_from_str(&date_with_defaults, &format) {
363                return Some(parsed.into());
364            }
365
366            // Without timezone.
367            let format = format!("{}{}", naive_format, default_format);
368            if let Ok(parsed) = NaiveDateTime::parse_from_str(&date_with_defaults, &format) {
369                return Some(parsed.into());
370            }
371        }
372
373        None
374    }
375
376    pub fn parse_time_of_day(date: &str) -> Option<NaiveTime> {
377        for naive_format in TIME_OF_DAY_FORMATS.iter() {
378            let format = naive_format.to_string();
379            if let Ok(parsed) = NaiveTime::parse_from_str(date, &format) {
380                return Some(parsed);
381            }
382        }
383
384        None
385    }
386
387    fn extract_local_date_from_system_time(system_time: SystemTime) -> Option<NaiveDate> {
388        if let Ok(system_time_as_duration) = system_time.duration_since(SystemTime::UNIX_EPOCH) {
389            return Some(
390                Local
391                    .timestamp_opt(system_time_as_duration.as_secs() as i64, 0)
392                    .unwrap()
393                    .date_naive(),
394            );
395        }
396
397        None
398    }
399
400    pub fn time_of_day_relative_to_system_time(
401        system_time: SystemTime,
402        time_of_day: NaiveTime,
403    ) -> Option<SystemTime> {
404        if let Some(date_from_system_time) = Self::extract_local_date_from_system_time(system_time)
405        {
406            let naive_date_with_time_of_day =
407                NaiveDateTime::new(date_from_system_time, time_of_day);
408            if let Some(local_time) = Local
409                .from_local_datetime(&naive_date_with_time_of_day)
410                .single()
411            {
412                let local_system_time = SystemTime::UNIX_EPOCH
413                    + std::time::Duration::from_secs(local_time.timestamp() as u64);
414                return Some(local_system_time);
415            }
416        }
417
418        None
419    }
420
421    /// Change "offset" to DEFAUL_OFFSET. Useful for tests so they won't be
422    /// affected by local timezone.
423    fn use_default_offset(mut self) -> Self {
424        let offset = DEFAUL_OFFSET.load(Ordering::SeqCst);
425        if is_valid_offset(offset) {
426            self.offset = offset
427        }
428        self
429    }
430
431    pub fn min_value() -> Self {
432        Self {
433            unixtime: 0,
434            offset: 0,
435        }
436    }
437
438    pub fn max_value() -> Self {
439        Self {
440            unixtime: u64::max_value() >> 2,
441            offset: 0,
442        }
443    }
444}
445
446impl Add<u64> for HgTime {
447    type Output = Self;
448
449    fn add(self, seconds: u64) -> Self {
450        Self {
451            unixtime: self.unixtime + seconds,
452            offset: self.offset,
453        }
454    }
455}
456
457impl Sub<u64> for HgTime {
458    type Output = Self;
459
460    fn sub(self, seconds: u64) -> Self {
461        Self {
462            // XXX: This might silently change negative time to 0.
463            unixtime: self.unixtime.max(seconds) - seconds,
464            offset: self.offset,
465        }
466    }
467}
468
469impl PartialOrd for HgTime {
470    fn partial_cmp(&self, other: &HgTime) -> Option<std::cmp::Ordering> {
471        self.unixtime.partial_cmp(&other.unixtime)
472    }
473}
474
475impl<Tz: TimeZone> From<DateTime<Tz>> for HgTime {
476    fn from(time: DateTime<Tz>) -> Self {
477        assert!(time.timestamp() >= 0);
478        Self {
479            unixtime: time.timestamp() as u64,
480            offset: time.offset().fix().utc_minus_local(),
481        }
482    }
483}
484
485impl From<NaiveDateTime> for HgTime {
486    fn from(time: NaiveDateTime) -> Self {
487        let timestamp = time.timestamp();
488        // Use local offset. (Is there a better way to do this?)
489        let offset = Self::now().offset;
490        // XXX: This might silently change negative time to 0.
491        let unixtime = (timestamp + offset as i64).max(0) as u64;
492        Self { unixtime, offset }
493    }
494}
495
496/// Change default offset (timezone).
497#[allow(dead_code)]
498pub fn set_default_offset(offset: i32) {
499    DEFAUL_OFFSET.store(offset, Ordering::SeqCst);
500}
501
502fn is_valid_offset(offset: i32) -> bool {
503    // UTC-12 to UTC+14.
504    (-50400..=43200).contains(&offset)
505}
506
507/// Lower bound for default values in dates.
508fn default_date_lower(format_char: char) -> &'static str {
509    match format_char {
510        'H' | 'M' | 'S' => "00",
511        'm' | 'd' => "1",
512        _ => unreachable!(),
513    }
514}
515
516trait ToStaticStr {
517    fn to_static_str() -> &'static str;
518}
519
520struct N31;
521struct N30;
522struct N29;
523struct N28;
524
525impl ToStaticStr for N31 {
526    fn to_static_str() -> &'static str {
527        "31"
528    }
529}
530
531impl ToStaticStr for N30 {
532    fn to_static_str() -> &'static str {
533        "30"
534    }
535}
536
537impl ToStaticStr for N29 {
538    fn to_static_str() -> &'static str {
539        "29"
540    }
541}
542
543impl ToStaticStr for N28 {
544    fn to_static_str() -> &'static str {
545        "28"
546    }
547}
548
549/// Upper bound. Assume a month has `N::to_static_str()` days.
550fn default_date_upper<N: ToStaticStr>(format_char: char) -> &'static str {
551    match format_char {
552        'H' => "23",
553        'M' | 'S' => "59",
554        'm' => "12",
555        'd' => N::to_static_str(),
556        _ => unreachable!(),
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    #[test]
565    fn test_parse_date() {
566        // Test cases are mostly from test-parse-date.t.
567        // Some variants were added.
568        set_default_offset(7200);
569
570        // t: parse date
571        // d: parse date, compare with now, within expected range
572        // The right side of assert_eq! is a string so it's autofix-able.
573
574        assert_eq!(t("2006-02-01 13:00:30"), "1138806030 7200");
575        assert_eq!(t("2006-02-01 13:00:30-0500"), "1138816830 18000");
576        assert_eq!(t("2006-02-01 13:00:30 +05:00"), "1138780830 -18000");
577        assert_eq!(t("2006-02-01 13:00:30Z"), "1138798830 0");
578        assert_eq!(t("2006-02-01 13:00:30 GMT"), "1138798830 0");
579        assert_eq!(t("2006-4-5 13:30"), "1144251000 7200");
580        assert_eq!(t("1150000000 14400"), "1150000000 14400");
581        assert_eq!(t("100000 1400000"), "fail");
582        assert_eq!(t("1000000000 -16200"), "1000000000 -16200");
583        assert_eq!(t("2006-02-01 1:00:30PM +0000"), "1138798830 0");
584
585        assert_eq!(d("1:00:30PM +0000", Duration::days(1)), "0");
586        assert_eq!(d("02/01", Duration::weeks(52)), "0");
587        assert_eq!(d("today", Duration::days(1)), "0");
588        assert_eq!(d("yesterday", Duration::days(2)), "0");
589        assert_eq!(d("tomorrow", Duration::days(1)), "0");
590        assert_eq!(d("day after tomorrow", Duration::days(2)), "0");
591        assert_eq!(d("overmorrow", Duration::days(2)), "0");
592
593        // ISO8601
594        assert_eq!(t("2016-07-27T12:10:21"), "1469628621 7200");
595        assert_eq!(t("2016-07-27T12:10:21Z"), "1469621421 0");
596        assert_eq!(t("2016-07-27T12:10:21+00:00"), "1469621421 0");
597        assert_eq!(t("2016-07-27T121021Z"), "1469621421 0");
598        assert_eq!(t("2016-07-27 12:10:21"), "1469628621 7200");
599        assert_eq!(t("2016-07-27 12:10:21Z"), "1469621421 0");
600        assert_eq!(t("2016-07-27 12:10:21+00:00"), "1469621421 0");
601        assert_eq!(t("2016-07-27 121021Z"), "1469621421 0");
602
603        // Months
604        assert_eq!(t("Jan 2018"), "1514772000 7200");
605        assert_eq!(t("Feb 2018"), "1517450400 7200");
606        assert_eq!(t("Mar 2018"), "1519869600 7200");
607        assert_eq!(t("Apr 2018"), "1522548000 7200");
608        assert_eq!(t("May 2018"), "1525140000 7200");
609        assert_eq!(t("Jun 2018"), "1527818400 7200");
610        assert_eq!(t("Jul 2018"), "1530410400 7200");
611        assert_eq!(t("Sep 2018"), "1535767200 7200");
612        assert_eq!(t("Oct 2018"), "1538359200 7200");
613        assert_eq!(t("Nov 2018"), "1541037600 7200");
614        assert_eq!(t("Dec 2018"), "1543629600 7200");
615        assert_eq!(t("Foo 2018"), "fail");
616
617        // Extra tests not in test-parse-date.t
618        assert_eq!(d("Jan", Duration::weeks(52)), "0");
619        assert_eq!(d("Jan 1", Duration::weeks(52)), "0"); // 1 is not considered as "year 1"
620        assert_eq!(d("4-26", Duration::weeks(52)), "0");
621        assert_eq!(d("4/26", Duration::weeks(52)), "0");
622        assert_eq!(t("4/26/2000"), "956714400 7200");
623        assert_eq!(t("Apr 26 2000"), "956714400 7200");
624        assert_eq!(t("2020"), "1577844000 7200"); // 2020 is considered as a "year"
625        assert_eq!(t("2020 GMT"), "1577836800 0");
626        assert_eq!(t("2020-12"), "1606788000 7200");
627        assert_eq!(t("2020-13"), "fail");
628
629        assert_eq!(t("Fri, 20 Sep 2019 12:15:13 -0700"), "1569006913 25200"); // date --rfc-2822
630        assert_eq!(t("Fri, 20 Sep 2019 12:15:13"), "1568988913 7200");
631
632        assert_eq!(t("09/20/2019 12:15:13"), "1568988913 7200");
633        assert_eq!(t("09/20/2019 12:15"), "1568988900 7200");
634        assert_eq!(t("09/20/2019 12:15:13PM"), "1568988913 7200");
635        assert_eq!(t("09/20/2019 12:15PM"), "1568988900 7200");
636        assert_eq!(t("09/20 12:15:13"), t("Sep 20 12:15:13"));
637        assert_eq!(t("09/20 12:15"), t("Sep 20 12:15:00"));
638        assert_eq!(t("09/20 12:15:13PM"), t("Sep 20 12:15:13"));
639        assert_eq!(t("09/20 12:15PM"), t("Sep 20 12:15:00"));
640    }
641
642    #[test]
643    fn test_parse_ago() {
644        set_default_offset(7200);
645        // I believe the comparisons are rounded up to the next largest duration size
646        // to absorb differences in execution time between test runs, otherwise this
647        // doesn't really make sense.
648        assert_eq!(d("10m ago", Duration::hours(1)), "0");
649        assert_eq!(d("10 min ago", Duration::hours(1)), "0");
650        assert_eq!(d("10 minutes ago", Duration::hours(1)), "0");
651        assert_eq!(d("10 hours ago", Duration::days(1)), "0");
652        assert_eq!(d("10 h ago", Duration::days(1)), "0");
653        assert_eq!(t("9999999 years ago"), "0 7200");
654    }
655
656    #[test]
657    fn test_parse_from_now() {
658        set_default_offset(7200);
659        // We'll keep the same duration comparisons for this set of tests for
660        // consistency.  Maybe we should instead round to the next minute to
661        // decrease the accepted timeframe and increase test accuracy?
662        assert_eq!(d("10m from now", Duration::hours(1)), "0");
663        assert_eq!(d("10 min from now", Duration::hours(1)), "0");
664        assert_eq!(d("10 minutes from now", Duration::hours(1)), "0");
665        assert_eq!(d("10 hours from now", Duration::days(1)), "0");
666        assert_eq!(d("10 h from now", Duration::days(1)), "0");
667    }
668
669    #[test]
670    fn test_parse_ago_short() {
671        set_default_offset(7200);
672        assert!(diff_from_now("10m", Duration::minutes(10)) < 2);
673        assert!(diff_from_now("2d", Duration::days(2)) < 2);
674        assert!(diff_from_now("10s", Duration::seconds(10)) < 2);
675        assert!(diff_from_now("10h", Duration::hours(10)) < 2);
676        assert!(diff_from_now("10h10m5s", Duration::seconds(36_605)) < 2);
677        assert!(diff_from_now("10h5s10m", Duration::seconds(36_605)) < 2);
678        assert!(diff_from_now("10H5s10M", Duration::seconds(36_605)) < 2);
679        // wrong format
680        assert_eq!(diff_from_now("10AM", Duration::minutes(10)), -1);
681        assert_eq!(diff_from_now("10hm", Duration::minutes(10)), -1);
682    }
683
684    #[test]
685    fn test_parse_from_now_short() {
686        set_default_offset(7200);
687        assert!(future_diff_from_now("+10m", Duration::minutes(10)) < 2);
688        assert!(future_diff_from_now("+2d", Duration::days(2)) < 2);
689        assert!(future_diff_from_now("+10s", Duration::seconds(10)) < 2);
690        assert!(future_diff_from_now("+10h", Duration::hours(10)) < 2);
691        assert!(future_diff_from_now("+10h10m5s", Duration::seconds(36_605)) < 2);
692        assert!(future_diff_from_now("+10h5s10m", Duration::seconds(36_605)) < 2);
693        assert!(future_diff_from_now("+10H5s10M", Duration::seconds(36_605)) < 2);
694        // wrong format
695        assert_eq!(future_diff_from_now("+10AM", Duration::minutes(10)), -1);
696        assert_eq!(future_diff_from_now("+10hm", Duration::minutes(10)), -1);
697    }
698
699    #[test]
700    fn test_parse_range() {
701        set_default_offset(7200);
702
703        assert_eq!(c("today", "tomorrow"), "does not contain");
704        assert_eq!(c("tomorrow", "overmorrow"), "does not contain");
705        assert_eq!(c("the day after tomorrow", "overmorrow"), "contains");
706        assert_eq!(c("overmorrow", "1 days from now"), "does not contain");
707        assert_eq!(c("overmorrow", "2 days from now"), "contains");
708        assert_eq!(c("overmorrow", "3 days from now"), "does not contain");
709        assert_eq!(c("since 1 month ago", "now"), "contains");
710        assert_eq!(c("since 1 month ago", "2 months ago"), "does not contain");
711        assert_eq!(c("> 1 month ago", "2 months ago"), "does not contain");
712        assert_eq!(c("< 1 month ago", "2 months ago"), "contains");
713        assert_eq!(c("< 1 month ago", "now"), "does not contain");
714
715        assert_eq!(c("-3", "now"), "contains");
716        assert_eq!(c("-3", "2 days ago"), "contains");
717        assert_eq!(c("-3", "4 days ago"), "does not contain");
718
719        assert_eq!(c("2018", "2017-12-31 23:59:59"), "does not contain");
720        assert_eq!(c("2018", "2018-1-1"), "contains");
721        assert_eq!(c("2018", "2018-12-31 23:59:59"), "contains");
722        assert_eq!(c("2018", "2019-1-1"), "does not contain");
723
724        assert_eq!(c("2018-5-1 to 2018-6-2", "2018-4-30"), "does not contain");
725        assert_eq!(c("2018-5-1 to 2018-6-2", "2018-5-30"), "contains");
726        assert_eq!(c("2018-5-1 to 2018-6-2", "2018-6-30"), "does not contain");
727    }
728
729    /// String representation of parse result.
730    fn t(date: &str) -> String {
731        match HgTime::parse(date) {
732            Some(time) => format!("{} {}", time.unixtime, time.offset),
733            None => "fail".to_string(),
734        }
735    }
736
737    /// String representation of (parse result - now) / seconds.
738    fn d(date: &str, duration: Duration) -> String {
739        match HgTime::parse(date) {
740            Some(time) => {
741                let value = (time.unixtime as i64 - HgTime::now().unixtime as i64).abs()
742                    / duration.num_seconds();
743                format!("{}", value)
744            }
745            None => "fail".to_string(),
746        }
747    }
748
749    /// String "contains" (if range contains date) or "does not contain"
750    /// or "fail" (if either range or date fails to parse).
751    fn c(range: &str, date: &str) -> &'static str {
752        if let (Some(range), Some(date)) = (HgTime::parse_range(range), HgTime::parse(date)) {
753            if range.contains(&date) {
754                "contains"
755            } else {
756                "does not contain"
757            }
758        } else {
759            "fail"
760        }
761    }
762
763    /// Comparing the date before now.
764    /// Return diff |now - date - expected|
765    /// Return negative value for parse error.
766    fn diff_from_now(date: &str, expected: Duration) -> i64 {
767        match HgTime::parse(date) {
768            Some(time) => {
769                let now = HgTime::now().unixtime as i64;
770                let before = time.unixtime as i64;
771                let expected_diff = expected.num_seconds();
772                (now - before - expected_diff).abs()
773            }
774            None => -1,
775        }
776    }
777
778    /// Comparing the date from now.
779    /// Return diff |future - now - expected|
780    /// Return negative value for parse error.
781    fn future_diff_from_now(date: &str, expected: Duration) -> i64 {
782        match HgTime::parse(date) {
783            Some(time) => {
784                let now = HgTime::now().unixtime as i64;
785                let future = time.unixtime as i64;
786                let expected_diff = expected.num_seconds();
787                (future - now - expected_diff).abs()
788            }
789            None => -1,
790        }
791    }
792}