rink_core/parsing/
datetime.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use crate::ast::{DatePattern, DateToken};
6use crate::loader::Context;
7use crate::types::{BaseUnit, BigInt, BigRat, Dimensionality, GenericDateTime, Number, Numeric};
8use chrono::format::Parsed;
9use chrono::{DateTime, Duration, FixedOffset, Local, TimeZone, Weekday};
10use chrono_tz::Tz;
11use std::iter::Peekable;
12use std::str::FromStr;
13
14fn parse_fixed(value: &str, digits: usize) -> Option<i32> {
15    if digits != 0 && value.len() != digits {
16        return None;
17    }
18    i32::from_str_radix(value, 10).ok()
19}
20
21fn parse_range(value: &str, digits: usize, range: std::ops::RangeInclusive<i32>) -> Option<i32> {
22    parse_fixed(value, digits).filter(|v| range.contains(v))
23}
24
25fn numeric_match(
26    tok: Option<&DateToken>,
27    name: &str,
28    digits: usize,
29    range: std::ops::RangeInclusive<i32>,
30) -> Result<i32, String> {
31    let tok = tok.ok_or_else(|| format!("Expected {}-digit {}, got eof", digits, name))?;
32
33    if let DateToken::Number(ref s, None) = tok {
34        parse_range(s, digits, range.clone())
35            .ok_or(format!("Expected {} in range {:?}, got {}", name, range, s))
36    } else {
37        Err(format!("Expected {}-digit {}, got {}", digits, name, tok))
38    }
39}
40
41pub fn parse_date<I>(
42    out: &mut Parsed,
43    out_tz: &mut Option<Tz>,
44    date: &mut Peekable<I>,
45    pat: &[DatePattern],
46) -> Result<(), String>
47where
48    I: Iterator<Item = DateToken> + Clone,
49{
50    use std::borrow::Borrow;
51
52    let tok = date.peek().cloned();
53
54    fn ts<T>(x: Option<T>) -> String
55    where
56        T: Borrow<DateToken>,
57    {
58        match x {
59            Some(ref x) => format!("`{}`", x.borrow()),
60            None => "eof".to_owned(),
61        }
62    }
63
64    let mut advance = true;
65
66    #[allow(unused_assignments)]
67    macro_rules! take {
68        ($($pat: pat)|+) => {
69            match date.peek().cloned() {
70                $(Some($pat))|+ => {date.next().unwrap()},
71                x => return Err(format!("Expected {}, got {}", stringify!($($pat)|+), ts(x)))
72            }
73        };
74        ($pat:pat, $var:ident) => {
75            match date.peek().cloned() {
76                Some($pat) => {date.next(); $var},
77                x => return Err(format!("Expected {}, got {}", stringify!($pat), ts(x)))
78            }
79        }
80    }
81
82    let res = match pat.first() {
83        None => return Ok(()),
84        Some(&DatePattern::Literal(ref l)) => match tok {
85            Some(DateToken::Literal(ref s)) if s == l => Ok(()),
86            x => Err(format!("Expected `{}`, got {}", l, ts(x))),
87        },
88        Some(&DatePattern::Match(ref what)) => match &**what {
89            "fullyear" => numeric_match(tok.as_ref(), "fullyear", 4, 0..=9999).and_then(|v| {
90                out.year = Some(v);
91                Ok(())
92            }),
93            "shortyear" => numeric_match(tok.as_ref(), "shortyear", 2, 0..=99).and_then(|v| {
94                out.year_mod_100 = Some(v);
95                Ok(())
96            }),
97            "century" => numeric_match(tok.as_ref(), "century", 2, 0..=99).and_then(|v| {
98                out.year_div_100 = Some(v);
99                Ok(())
100            }),
101            "monthnum" => numeric_match(tok.as_ref(), "monthnum", 2, 1..=12).and_then(|v| {
102                out.month = Some(v as u32);
103                Ok(())
104            }),
105            "day" => numeric_match(tok.as_ref(), "day", 0, 1..=31).and_then(|v| {
106                out.day = Some(v as u32);
107                Ok(())
108            }),
109            "fullday" => numeric_match(tok.as_ref(), "fullday", 2, 1..=31).and_then(|v| {
110                out.day = Some(v as u32);
111                Ok(())
112            }),
113            "min" => numeric_match(tok.as_ref(), "min", 2, 0..=60).and_then(|v| {
114                out.minute = Some(v as u32);
115                Ok(())
116            }),
117            "ordinal" => numeric_match(tok.as_ref(), "ordinal", 3, 1..=366).and_then(|v| {
118                out.ordinal = Some(v as u32);
119                Ok(())
120            }),
121            "isoyear" => numeric_match(tok.as_ref(), "isoyear", 4, 0..=9999).and_then(|v| {
122                out.isoyear = Some(v);
123                Ok(())
124            }),
125            "isoweek" => numeric_match(tok.as_ref(), "isoweek", 2, 1..=53).and_then(|v| {
126                out.isoweek = Some(v as u32);
127                Ok(())
128            }),
129            "unix" => numeric_match(tok.as_ref(), "unix", 0, 0..=i32::MAX).and_then(|v| {
130                out.timestamp = Some(v as i64);
131                Ok(())
132            }),
133            "year" => {
134                advance = false;
135                let x = take!(DateToken::Dash | DateToken::Plus | DateToken::Number(_, None));
136                let (sign, num) = match x {
137                    DateToken::Dash => (-1, None),
138                    DateToken::Plus => (1, None),
139                    DateToken::Number(i, None) => (1, Some(i)),
140                    _ => unreachable!(),
141                };
142                let num = match num {
143                    Some(x) => x,
144                    None => take!(DateToken::Number(x, None), x),
145                };
146                let value = i32::from_str_radix(&*num, 10);
147                if let Ok(value) = value {
148                    out.year = Some(value * sign);
149                    Ok(())
150                } else {
151                    Err(format!("Expected year, got out of range value"))
152                }
153            }
154            "adbc" => match tok {
155                Some(DateToken::Literal(ref s))
156                    if { s.to_lowercase() == "ad" || s.to_lowercase() == "ce" } =>
157                {
158                    Ok(())
159                }
160                Some(DateToken::Literal(ref s))
161                    if { s.to_lowercase() == "bc" || s.to_lowercase() == "bce" } =>
162                {
163                    out.year = out.year.map(|x| -x + 1);
164                    Ok(())
165                }
166                x => Err(format!("Expected AD/BC or CE/BCE, got {}", ts(x))),
167            },
168            "hour12" => match tok {
169                Some(DateToken::Number(ref s, None)) => {
170                    if let Some(value) = parse_range(s, 2, 1..=12) {
171                        out.hour_mod_12 = Some(value as u32 % 12);
172                        Ok(())
173                    } else {
174                        Err(format!(
175                            "Expected 2-digit hour12, got out of range value {}",
176                            s
177                        ))
178                    }
179                }
180                x => Err(format!("Expected 2-digit hour12, got {}", ts(x))),
181            },
182            "hour24" => match tok {
183                Some(DateToken::Number(ref s, None)) => {
184                    if let Some(value) = parse_range(s, 2, 0..=23) {
185                        out.hour_div_12 = Some(value as u32 / 12);
186                        out.hour_mod_12 = Some(value as u32 % 12);
187                        Ok(())
188                    } else {
189                        Err(format!(
190                            "Expected 2-digit hour24, got out of range value {}",
191                            s
192                        ))
193                    }
194                }
195                x => Err(format!("Expected 2-digit hour24, got {}", ts(x))),
196            },
197            "meridiem" => match tok {
198                Some(DateToken::Literal(ref s)) if s.to_lowercase() == "am" => {
199                    out.hour_div_12 = Some(0);
200                    Ok(())
201                }
202                Some(DateToken::Literal(ref s)) if s.to_lowercase() == "pm" => {
203                    out.hour_div_12 = Some(1);
204                    Ok(())
205                }
206                x => Err(format!("Expected AM/PM, got {}", ts(x))),
207            },
208            "sec" => match tok {
209                Some(DateToken::Number(ref s, None)) => {
210                    if let Some(value) = parse_range(s, 2, 0..=60) {
211                        out.second = Some(value as u32);
212                        Ok(())
213                    } else {
214                        Err(format!("Expected 2-digit sec in range 0..=60, got {}", s))
215                    }
216                }
217                Some(DateToken::Number(ref s, Some(ref f))) if s.len() == 2 => {
218                    let secs = u32::from_str_radix(&**s, 10);
219                    let nsecs = u32::from_str_radix(&**f, 10);
220                    if let (Ok(secs), Ok(nsecs)) = (secs, nsecs) {
221                        let nsecs = nsecs * 10u32.pow(9 - f.len() as u32);
222                        out.second = Some(secs);
223                        out.nanosecond = Some(nsecs);
224                        Ok(())
225                    } else {
226                        Err(format!("Expected 2-digit sec, got {}.{}", s, f))
227                    }
228                }
229                x => Err(format!("Expected 2-digit sec, got {}", ts(x))),
230            },
231            "offset" => {
232                advance = false;
233                if let Some(DateToken::Literal(ref s)) = date.peek().cloned() {
234                    date.next();
235                    if let Ok(tz) = Tz::from_str(s) {
236                        *out_tz = Some(tz);
237                        Ok(())
238                    } else {
239                        Err(format!("Invalid timezone {}", s))
240                    }
241                } else {
242                    let s = match take!(DateToken::Plus | DateToken::Dash) {
243                        DateToken::Plus => 1,
244                        DateToken::Dash => -1,
245                        _ => unreachable!(),
246                    };
247                    let h = take!(DateToken::Number(s, None), s);
248                    if let Some(hm) = parse_fixed(&h, 4) {
249                        let m = hm % 100;
250                        let h = hm / 100;
251                        out.offset = Some(s * (h * 3600 + m * 60));
252                        Ok(())
253                    } else if let Ok(h) = i32::from_str_radix(&h, 10) {
254                        take!(DateToken::Colon);
255                        let m = take!(DateToken::Number(s, None), s);
256                        if let Some(m) = parse_range(&m, 2, 0..=59) {
257                            out.offset = Some(s * (h * 3600 + m * 60));
258                            Ok(())
259                        } else {
260                            Err(format!("Expected 2 digits after : in offset, got {}", m))
261                        }
262                    } else {
263                        Err(format!("Expected offset, got {}", h))
264                    }
265                }
266            }
267            "monthname" => match tok {
268                Some(DateToken::Literal(ref s)) => {
269                    let res = match &*s.to_lowercase() {
270                        "jan" | "january" => 1,
271                        "feb" | "february" => 2,
272                        "mar" | "march" => 3,
273                        "apr" | "april" => 4,
274                        "may" => 5,
275                        "jun" | "june" => 6,
276                        "jul" | "july" => 7,
277                        "aug" | "august" => 8,
278                        "sep" | "september" => 9,
279                        "oct" | "october" => 10,
280                        "nov" | "november" => 11,
281                        "dec" | "december" => 12,
282                        x => return Err(format!("Unknown month name: {}", x)),
283                    };
284                    out.month = Some(res);
285                    Ok(())
286                }
287                x => Err(format!("Expected month name, got {}", ts(x))),
288            },
289            "weekday" => match tok {
290                Some(DateToken::Literal(ref s)) => {
291                    let res = match &*s.to_lowercase() {
292                        "mon" | "monday" => Weekday::Mon,
293                        "tue" | "tuesday" => Weekday::Tue,
294                        "wed" | "wednesday" => Weekday::Wed,
295                        "thu" | "thursday" => Weekday::Thu,
296                        "fri" | "friday" => Weekday::Fri,
297                        "sat" | "saturday" => Weekday::Sat,
298                        "sun" | "sunday" => Weekday::Sun,
299                        x => return Err(format!("Unknown weekday: {}", x)),
300                    };
301                    out.weekday = Some(res);
302                    Ok(())
303                }
304                x => Err(format!("Expected weekday, got {}", ts(x))),
305            },
306            x => Err(format!("Unknown match pattern `{}`", x)),
307        },
308        Some(&DatePattern::Optional(ref pats)) => {
309            advance = false;
310            let mut iter = date.clone();
311            if let Ok(()) = parse_date(out, out_tz, &mut iter, &pats[..]) {
312                *date = iter
313            }
314            Ok(())
315        }
316        Some(&DatePattern::Dash) => match tok {
317            Some(DateToken::Dash) => Ok(()),
318            x => Err(format!("Expected `-`, got {}", ts(x))),
319        },
320        Some(&DatePattern::Colon) => match tok {
321            Some(DateToken::Colon) => Ok(()),
322            x => Err(format!("Expected `:`, got {}", ts(x))),
323        },
324        Some(&DatePattern::Space) => match tok {
325            Some(DateToken::Space) => Ok(()),
326            x => Err(format!("Expected ` `, got {}", ts(x))),
327        },
328    };
329    if advance {
330        date.next();
331    }
332    res.and_then(|_| parse_date(out, out_tz, date, &pat[1..]))
333}
334
335fn attempt(
336    now: DateTime<Local>,
337    date: &[DateToken],
338    pat: &[DatePattern],
339) -> Result<GenericDateTime, (String, usize)> {
340    let mut parsed = Parsed::new();
341    let mut tz = None;
342    let mut iter = date.iter().cloned().peekable();
343    let res = parse_date(&mut parsed, &mut tz, &mut iter, pat);
344    let count = iter.count();
345    let res = if count > 0 && res.is_ok() {
346        Err(format!(
347            "Expected eof, got {}",
348            date[date.len() - count..]
349                .iter()
350                .map(ToString::to_string)
351                .collect::<Vec<_>>()
352                .join("")
353        ))
354    } else {
355        res
356    };
357    res.map_err(|e| (e, count))?;
358    let time = parsed.to_naive_time();
359    let date = parsed.to_naive_date();
360    if let Some(tz) = tz {
361        match (time, date) {
362            (Ok(time), Ok(date)) => tz
363                .from_local_datetime(&date.and_time(time))
364                .earliest()
365                .ok_or_else(|| {
366                    (
367                        "Datetime does not represent a valid moment in time".to_string(),
368                        count,
369                    )
370                })
371                .map(GenericDateTime::Timezone),
372            (Ok(time), Err(_)) => {
373                Ok(now.with_timezone(&tz).with_time(time).unwrap()).map(GenericDateTime::Timezone)
374            }
375            (Err(_), Ok(date)) => tz
376                .from_local_datetime(&date.and_hms_opt(0, 0, 0).unwrap())
377                .earliest()
378                .ok_or_else(|| {
379                    (
380                        "Datetime does not represent a valid moment in time".to_string(),
381                        count,
382                    )
383                })
384                .map(GenericDateTime::Timezone),
385            _ => Err(("Failed to construct a useful datetime".to_string(), count)),
386        }
387    } else {
388        let offset = parsed
389            .to_fixed_offset()
390            .unwrap_or_else(|_| FixedOffset::east_opt(0).unwrap());
391        match (time, date) {
392            (Ok(time), Ok(date)) => offset
393                .from_local_datetime(&date.and_time(time))
394                .earliest()
395                .ok_or_else(|| {
396                    (
397                        "Datetime does not represent a valid moment in time".to_string(),
398                        count,
399                    )
400                })
401                .map(GenericDateTime::Fixed),
402            (Ok(time), Err(_)) => Ok(GenericDateTime::Fixed(
403                now.with_timezone(&offset).with_time(time).unwrap(),
404            )),
405            (Err(_), Ok(date)) => offset
406                .from_local_datetime(&date.and_hms_opt(0, 0, 0).unwrap())
407                .earliest()
408                .ok_or_else(|| {
409                    (
410                        "Datetime does not represent a valid moment in time".to_string(),
411                        count,
412                    )
413                })
414                .map(GenericDateTime::Fixed),
415            _ => Err(("Failed to construct a useful datetime".to_string(), count)),
416        }
417    }
418}
419
420pub fn try_decode(date: &[DateToken], context: &Context) -> Result<GenericDateTime, String> {
421    let mut best = None;
422    for pat in &context.registry.datepatterns {
423        match attempt(context.now, date, pat) {
424            Ok(datetime) => return Ok(datetime),
425            Err((e, c)) => {
426                //println!("{}", e);
427                let better = if let Some((count, _, _)) = best {
428                    c < count
429                } else {
430                    true
431                };
432                if better {
433                    best = Some((c, pat, e.clone()));
434                }
435            }
436        }
437    }
438    if let Some((_, pat, err)) = best {
439        Err(format!(
440            "Most likely pattern `{}` failed: {}",
441            DatePattern::show(pat),
442            err
443        ))
444    } else {
445        Err("Invalid date literal".to_string())
446    }
447}
448
449pub fn to_duration(num: &Number) -> Result<Duration, String> {
450    if num.unit != Dimensionality::base_unit(BaseUnit::new("s")) {
451        return Err("Expected seconds".to_string());
452    }
453    let max = Numeric::from(i64::max_value() / 1000);
454    if num.value.abs() > max {
455        return Err(format!(
456            "Implementation error: Number is out of range ({:?})",
457            max
458        ));
459    }
460    let ms = &num.value * &Numeric::from(1000);
461    let (ms, rem) = ms.div_rem(&Numeric::from(1));
462    let ns = &rem * &Numeric::from(1_000_000_000);
463    Ok(Duration::milliseconds(ms.to_int().unwrap()) + Duration::nanoseconds(ns.to_int().unwrap()))
464}
465
466pub fn from_duration(duration: &Duration) -> Result<Number, String> {
467    let ms = duration.num_milliseconds();
468    let ns = (*duration - Duration::milliseconds(ms))
469        .num_nanoseconds()
470        .unwrap();
471    let ms_div = BigInt::from(1_000u64);
472    let ns_div = BigInt::from(1_000_000_000u64);
473    let ms = BigRat::ratio(&BigInt::from(ms), &ms_div);
474    let ns = BigRat::ratio(&BigInt::from(ns), &ns_div);
475    Ok(Number::new_unit(
476        Numeric::Rational(&ms + &ns),
477        BaseUnit::new("s"),
478    ))
479}
480
481pub fn parse_datepattern<I>(iter: &mut Peekable<I>) -> Result<Vec<DatePattern>, String>
482where
483    I: Iterator<Item = char>,
484{
485    let mut out = vec![];
486    while iter.peek().is_some() {
487        let res = match iter.peek().cloned().unwrap() {
488            '-' => DatePattern::Dash,
489            ':' => DatePattern::Colon,
490            '[' => {
491                iter.next();
492                let res = DatePattern::Optional(parse_datepattern(iter)?);
493                if iter.peek().cloned() != Some(']') {
494                    return Err("Expected ]".to_string());
495                } else {
496                    res
497                }
498            }
499            ']' => break,
500            '\'' => {
501                iter.next();
502                let mut buf = String::new();
503                while let Some(c) = iter.peek().cloned() {
504                    if c == '\'' {
505                        break;
506                    } else {
507                        iter.next();
508                        buf.push(c);
509                    }
510                }
511                DatePattern::Literal(buf)
512            }
513            x if x.is_whitespace() => {
514                while iter.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
515                    iter.next();
516                }
517                out.push(DatePattern::Space);
518                continue;
519            }
520            x if x.is_alphabetic() => {
521                let mut buf = String::new();
522                while let Some(c) = iter.peek().cloned() {
523                    if c.is_alphanumeric() {
524                        iter.next();
525                        buf.push(c);
526                    } else {
527                        break;
528                    }
529                }
530                out.push(DatePattern::Match(buf));
531                continue;
532            }
533            x => return Err(format!("Unrecognized character {}", x)),
534        };
535        out.push(res);
536        iter.next();
537    }
538    Ok(out)
539}
540
541pub fn parse_datefile(file: &str) -> Vec<Vec<DatePattern>> {
542    let mut defs = vec![];
543    for (num, line) in file.lines().enumerate() {
544        let line = line.split('#').next().unwrap();
545        let line = line.trim();
546        if line.is_empty() {
547            continue;
548        }
549        let res = parse_datepattern(&mut line.chars().peekable());
550        match res {
551            Ok(res) => defs.push(res),
552            Err(e) => println!("Line {}: {}: {}", num, e, line),
553        }
554    }
555    defs
556}
557
558pub fn humanize<Tz: TimeZone>(now: DateTime<Local>, date: DateTime<Tz>) -> Option<String> {
559    if cfg!(feature = "chrono-humanize") {
560        use chrono_humanize::HumanTime;
561        let now = now.with_timezone(&date.timezone());
562        let duration = date - now;
563        Some(HumanTime::from(duration).to_string())
564    } else {
565        None
566    }
567}
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572
573    fn pattern(s: &str) -> Vec<DatePattern> {
574        parse_datepattern(&mut s.chars().peekable()).unwrap()
575    }
576
577    fn parse_with_tz(date: Vec<DateToken>, pat: &str) -> (Result<(), String>, Parsed, Option<Tz>) {
578        let mut parsed = Parsed::new();
579        let mut tz = None;
580        let pat = pattern(pat);
581        let res = parse_date(&mut parsed, &mut tz, &mut date.into_iter().peekable(), &pat);
582
583        (res, parsed, tz)
584    }
585
586    fn parse(date: Vec<DateToken>, pat: &str) -> (Result<(), String>, Parsed) {
587        let (res, parsed, _) = parse_with_tz(date, pat);
588        (res, parsed)
589    }
590
591    #[test]
592    fn test_literal() {
593        let date = vec![DateToken::Literal("abc".into())];
594        let (res, parsed) = parse(date.clone(), "'abc'");
595        assert_eq!(parsed, Parsed::new());
596        assert!(res.is_ok());
597
598        let (res, parsed) = parse(date, "'def'");
599        assert_eq!(parsed, Parsed::new());
600        assert_eq!(res, Err("Expected `def`, got `abc`".into()));
601    }
602
603    #[test]
604    fn test_year_plus() {
605        let mut expected = Parsed::new();
606        expected.set_year(123).unwrap();
607
608        let date = vec![
609            DateToken::Plus,
610            DateToken::Number(expected.year.unwrap().to_string(), None),
611        ];
612        let (res, parsed) = parse(date.clone(), "year");
613        assert!(res.is_ok());
614        assert_eq!(parsed, expected);
615
616        let date = vec![DateToken::Number(expected.year.unwrap().to_string(), None)];
617        let (res, parsed2) = parse(date.clone(), "year");
618        assert!(res.is_ok());
619        assert_eq!(parsed2, parsed);
620    }
621
622    #[test]
623    fn test_complicated_date_input() {
624        let mut expected = Parsed::new();
625        expected.set_year(123).unwrap();
626        expected.set_month(5).unwrap();
627        expected.set_day(2).unwrap();
628        expected.set_ampm(true).unwrap();
629        expected.set_hour(13).unwrap();
630        expected.set_minute(57).unwrap();
631
632        let date = vec![
633            DateToken::Number(expected.day.unwrap().to_string(), None),
634            DateToken::Space,
635            DateToken::Literal("Pm".into()),
636            DateToken::Dash,
637            DateToken::Number(format!("{:02}", expected.month.unwrap()), None),
638            DateToken::Colon,
639            DateToken::Number(expected.year.unwrap().to_string(), None),
640            DateToken::Space,
641            DateToken::Number(format!("{:02}", expected.hour_mod_12.unwrap()), None),
642            DateToken::Dash,
643            DateToken::Number(format!("{:02}", expected.minute.unwrap()), None),
644            DateToken::Space,
645            DateToken::Literal("May".into()),
646        ];
647
648        let (res, parsed) = parse(date, "day meridiem-monthnum:year hour12-min monthname");
649        assert!(res.is_ok());
650        assert_eq!(parsed, expected);
651    }
652
653    #[test]
654    fn ad_bc() {
655        let year = -100;
656        let mut expected = Parsed::new();
657        expected.set_year(year + 1).unwrap();
658        expected.set_hour(7).unwrap();
659
660        let date = vec![
661            DateToken::Number(year.abs().to_string(), None),
662            DateToken::Space,
663            DateToken::Literal("bce".into()),
664            DateToken::Space,
665            DateToken::Number(format!("{:02}", expected.hour_mod_12.unwrap()), None),
666            DateToken::Space,
667            DateToken::Literal("am".into()),
668        ];
669
670        let (res, parsed) = parse(date, "year adbc hour12 meridiem");
671        assert!(res.is_ok(), "{}", res.unwrap_err());
672        assert_eq!(parsed, expected);
673    }
674
675    #[test]
676    fn ad_bc_wrong() {
677        for date in vec![
678            vec![DateToken::Literal("foo".into())],
679            vec![DateToken::Plus],
680        ] {
681            let (res, _) = parse(date, "adbc");
682            assert!(res.is_err());
683        }
684    }
685
686    #[test]
687    fn wrong_length_24h() {
688        let date = vec![DateToken::Number("7".into(), None)];
689        let (res, _) = parse(date, "hour24");
690        assert_eq!(
691            res,
692            Err(format!("Expected 2-digit hour24, got out of range value 7"))
693        );
694    }
695
696    #[test]
697    fn test_24h() {
698        let (res, parsed) = parse(vec![DateToken::Number("23".into(), None)], "hour24");
699        assert!(res.is_ok());
700        assert_eq!(parsed.hour_div_12, Some(1));
701        assert_eq!(parsed.hour_mod_12, Some(11));
702    }
703
704    #[test]
705    fn seconds() {
706        let mut expected = Parsed::new();
707        expected.set_second(27).unwrap();
708        expected.set_nanosecond(12345).unwrap();
709
710        let date = vec![DateToken::Number("27".into(), Some("000012345".into()))];
711        let (res, parsed) = parse(date, "sec");
712        assert!(res.is_ok());
713        assert_eq!(parsed, expected);
714
715        let date = vec![DateToken::Number("27".into(), None)];
716        let (res, parsed) = parse(date, "sec");
717        assert!(res.is_ok());
718        expected.nanosecond = None;
719        assert_eq!(parsed, expected);
720    }
721
722    #[test]
723    fn test_offset() {
724        let date = vec![DateToken::Plus, DateToken::Number("0200".into(), None)];
725        let (res, parsed) = parse(date, "offset");
726        assert!(res.is_ok());
727        assert_eq!(parsed.offset, Some(2 * 3600));
728
729        let date = vec![
730            DateToken::Dash,
731            DateToken::Number("01".into(), None),
732            DateToken::Colon,
733            DateToken::Number("23".into(), None),
734        ];
735        let (res, parsed) = parse(date, "offset");
736        assert!(res.is_ok());
737        assert_eq!(parsed.offset, Some(-(1 * 60 + 23) * 60));
738
739        let date = vec![DateToken::Literal("Europe/London".into())];
740        let (res, parsed, tz) = parse_with_tz(date, "offset");
741        assert!(res.is_ok(), "{}", res.unwrap_err());
742        assert_eq!(tz.unwrap(), Tz::Europe__London);
743        assert_eq!(parsed.offset, None);
744    }
745
746    #[test]
747    fn test_weekday() {
748        let date = vec![DateToken::Literal("saturday".into())];
749        let (res, parsed) = parse(date, "weekday");
750        assert!(res.is_ok());
751        assert_eq!(parsed.weekday, Some(Weekday::Sat));
752
753        let date = vec![DateToken::Literal("sun".into())];
754        assert!(parse(date, "weekday").0.is_ok());
755
756        let date = vec![DateToken::Literal("snu".into())];
757        assert_eq!(parse(date, "weekday").0, Err("Unknown weekday: snu".into()));
758    }
759
760    #[test]
761    fn test_monthname() {
762        for (i, &s) in [
763            "jan", "feb", "mar", "apr", "may", "june", "jul", "AUGUST", "SEp", "Oct", "novemBer",
764            "dec",
765        ]
766        .iter()
767        .enumerate()
768        {
769            let date = vec![DateToken::Literal(s.into())];
770            let (res, parsed) = parse(date, "monthname");
771            assert!(res.is_ok());
772            assert_eq!(parsed.month, Some(i as u32 + 1));
773        }
774
775        let date = vec![DateToken::Literal("foobar".into())];
776        let (res, parsed) = parse(date, "monthname");
777        assert_eq!(res, Err("Unknown month name: foobar".into()));
778        assert_eq!(parsed.month, None);
779    }
780
781    #[test]
782    fn test_parse_datepattern() {
783        use self::DatePattern::*;
784
785        fn parse(s: &str) -> Result<Vec<DatePattern>, String> {
786            parse_datepattern(&mut s.chars().peekable())
787        }
788
789        assert_eq!(
790            parse("-:['abc']"),
791            Ok(vec![Dash, Colon, Optional(vec![Literal("abc".into())])])
792        );
793        assert!(parse("-:['abc'").is_err());
794        assert!(parse("*").is_err());
795    }
796
797    #[test]
798    fn test_attempt() {
799        use self::DateToken::*;
800        fn n(x: &str) -> DateToken {
801            Number(x.into(), None)
802        }
803
804        let now = Local::now();
805
806        macro_rules! check_attempt {
807            ($date:expr, $pat:expr) => {{
808                let pat = parse_datepattern(&mut $pat.chars().peekable()).unwrap();
809                attempt(now, $date, pat.as_ref())
810            }};
811        }
812
813        let tz = Literal("Europe/London".into());
814
815        let date = &[n("23"), Space, n("05"), Space, tz.clone()];
816        let res = check_attempt!(date, "hour24 min offset");
817        assert!(res.is_ok(), "{:?}", res);
818
819        let date = &[n("23"), Space, tz.clone()];
820        let res = check_attempt!(date, "hour24 offset");
821        assert_eq!(
822            res,
823            Err(("Failed to construct a useful datetime".into(), 0))
824        );
825
826        let date = &[n("2018"), Space, n("01"), Space, n("01"), Space, tz.clone()];
827        let res = check_attempt!(date, "year monthnum day offset");
828        assert!(res.is_ok(), "{:?}", res);
829    }
830}