rink-core 0.6.2

Unit conversion library behind rink
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::ast::{DatePattern, DateToken};
use crate::bigint::BigInt;
use crate::bigrat::BigRat;
use crate::context::Context;
use crate::number::{Dimension, Number};
use crate::numeric::Numeric;
use chrono::format::Parsed;
use chrono::{DateTime, Duration, FixedOffset, Local, TimeZone, Weekday};
use chrono_tz::Tz;
use std::iter::Peekable;
use std::str::FromStr;

pub fn parse_date<I>(
    out: &mut Parsed,
    out_tz: &mut Option<Tz>,
    date: &mut Peekable<I>,
    pat: &[DatePattern],
) -> Result<(), String>
where
    I: Iterator<Item = DateToken> + Clone,
{
    use std::borrow::Borrow;

    let tok = date.peek().cloned();

    fn ts<T>(x: Option<T>) -> String
    where
        T: Borrow<DateToken>,
    {
        match x {
            Some(ref x) => format!("`{}`", x.borrow()),
            None => "eof".to_owned(),
        }
    }

    macro_rules! numeric_match {
        ($name:expr, $digits:expr, $field:ident) => {
            match tok {
                Some(DateToken::Number(ref s, None)) if $digits == 0 || s.len() == $digits => {
                    let value = i32::from_str_radix(&**s, 10).unwrap();
                    out.$field = Some(value as _);
                    Ok(())
                }
                Some(DateToken::Number(ref s, None)) => Err(format!(
                    "Expected {}-digit {}, got {} digits",
                    $digits,
                    $name,
                    s.len()
                )),
                x => Err(format!(
                    "Expected {}-digit {}, got {}",
                    $digits,
                    $name,
                    ts(x)
                )),
            }
        };
    }

    let mut advance = true;

    #[allow(unused_assignments)]
    macro_rules! take {
        ($($pat: pat)|+) => {
            match date.peek().cloned() {
                $(Some($pat))|+ => {date.next().unwrap()},
                x => return Err(format!("Expected {}, got {}", stringify!($($pat)|+), ts(x)))
            }
        };
        ($pat:pat, $var:ident) => {
            match date.peek().cloned() {
                Some($pat) => {date.next(); $var},
                x => return Err(format!("Expected {}, got {}", stringify!($pat), ts(x)))
            }
        }
    }

    let res = match pat.first() {
        None => return Ok(()),
        Some(&DatePattern::Literal(ref l)) => match tok {
            Some(DateToken::Literal(ref s)) if s == l => Ok(()),
            x => Err(format!("Expected `{}`, got {}", l, ts(x))),
        },
        Some(&DatePattern::Match(ref what)) => match &**what {
            "fullyear" => numeric_match!("fullyear", 4, year),
            "shortyear" => numeric_match!("shortyear", 2, year_mod_100),
            "century" => numeric_match!("century", 2, year_div_100),
            "monthnum" => numeric_match!("monthnum", 2, month),
            "day" => numeric_match!("day", 0, day),
            "fullday" => numeric_match!("fullday", 2, day),
            "min" => numeric_match!("min", 2, minute),
            "ordinal" => numeric_match!("ordinal", 3, ordinal),
            "isoyear" => numeric_match!("isoyear", 4, isoyear),
            "isoweek" => numeric_match!("isoweek", 2, isoweek),
            "unix" => numeric_match!("unix", 0, timestamp),
            "year" => {
                advance = false;
                let x = take!(DateToken::Dash | DateToken::Plus | DateToken::Number(_, None));
                let (sign, num) = match x {
                    DateToken::Dash => (-1, None),
                    DateToken::Plus => (1, None),
                    DateToken::Number(i, None) => (1, Some(i)),
                    _ => unreachable!(),
                };
                let num = match num {
                    Some(x) => x,
                    None => take!(DateToken::Number(x, None), x),
                };
                let value = i32::from_str_radix(&*num, 10).unwrap();
                out.year = Some(value * sign);
                Ok(())
            }
            "adbc" => match tok {
                Some(DateToken::Literal(ref s))
                    if { s.to_lowercase() == "ad" || s.to_lowercase() == "ce" } =>
                {
                    Ok(())
                }
                Some(DateToken::Literal(ref s))
                    if { s.to_lowercase() == "bc" || s.to_lowercase() == "bce" } =>
                {
                    out.year = out.year.map(|x| -x + 1);
                    Ok(())
                }
                x => Err(format!("Expected AD/BC or CE/BCE, got {}", ts(x))),
            },
            "hour12" => match tok {
                Some(DateToken::Number(ref s, None)) if s.len() == 2 => {
                    let value = u32::from_str_radix(&**s, 10).unwrap();
                    out.hour_mod_12 = Some(value % 12);
                    Ok(())
                }
                x => Err(format!("Expected 2-digit hour12, got {}", ts(x))),
            },
            "hour24" => match tok {
                Some(DateToken::Number(ref s, None)) if s.len() == 2 => {
                    let value = u32::from_str_radix(&**s, 10).unwrap();
                    out.hour_div_12 = Some(value / 12);
                    out.hour_mod_12 = Some(value % 12);
                    Ok(())
                }
                x => Err(format!("Expected 2-digit hour24, got {}", ts(x))),
            },
            "meridiem" => match tok {
                Some(DateToken::Literal(ref s)) if s.to_lowercase() == "am" => {
                    out.hour_div_12 = Some(0);
                    Ok(())
                }
                Some(DateToken::Literal(ref s)) if s.to_lowercase() == "pm" => {
                    out.hour_div_12 = Some(1);
                    Ok(())
                }
                x => Err(format!("Expected AM/PM, got {}", ts(x))),
            },
            "sec" => match tok {
                Some(DateToken::Number(ref s, None)) if s.len() == 2 => {
                    let value = u32::from_str_radix(&**s, 10).unwrap();
                    out.second = Some(value);
                    Ok(())
                }
                Some(DateToken::Number(ref s, Some(ref f))) if s.len() == 2 => {
                    let secs = u32::from_str_radix(&**s, 10).unwrap();
                    let nsecs =
                        u32::from_str_radix(&**f, 10).unwrap() * 10u32.pow(9 - f.len() as u32);
                    out.second = Some(secs);
                    out.nanosecond = Some(nsecs);
                    Ok(())
                }
                x => Err(format!("Expected 2-digit sec, got {}", ts(x))),
            },
            "offset" => {
                advance = false;
                if let Some(DateToken::Literal(ref s)) = date.peek().cloned() {
                    date.next();
                    if let Ok(tz) = Tz::from_str(s) {
                        *out_tz = Some(tz);
                        Ok(())
                    } else {
                        Err(format!("Invalid timezone {}", s))
                    }
                } else {
                    let s = match take!(DateToken::Plus | DateToken::Dash) {
                        DateToken::Plus => 1,
                        DateToken::Dash => -1,
                        _ => unreachable!(),
                    };
                    let h = take!(DateToken::Number(s, None), s);
                    if h.len() == 4 {
                        let h = i32::from_str_radix(&*h, 10).unwrap();
                        let m = h % 100;
                        let h = h / 100;
                        out.offset = Some(s * (h * 3600 + m * 60));
                    } else {
                        let h = i32::from_str_radix(&*h, 10).unwrap();
                        take!(DateToken::Colon);
                        let m = take!(DateToken::Number(s, None), s);
                        let m = i32::from_str_radix(&*m, 10).unwrap();
                        out.offset = Some(s * (h * 3600 + m * 60));
                    }
                    Ok(())
                }
            }
            "monthname" => match tok {
                Some(DateToken::Literal(ref s)) => {
                    let res = match &*s.to_lowercase() {
                        "jan" | "january" => 1,
                        "feb" | "february" => 2,
                        "mar" | "march" => 3,
                        "apr" | "april" => 4,
                        "may" => 5,
                        "jun" | "june" => 6,
                        "jul" | "july" => 7,
                        "aug" | "august" => 8,
                        "sep" | "september" => 9,
                        "oct" | "october" => 10,
                        "nov" | "november" => 11,
                        "dec" | "december" => 12,
                        x => return Err(format!("Unknown month name: {}", x)),
                    };
                    out.month = Some(res);
                    Ok(())
                }
                x => Err(format!("Expected month name, got {}", ts(x))),
            },
            "weekday" => match tok {
                Some(DateToken::Literal(ref s)) => {
                    let res = match &*s.to_lowercase() {
                        "mon" | "monday" => Weekday::Mon,
                        "tue" | "tuesday" => Weekday::Tue,
                        "wed" | "wednesday" => Weekday::Wed,
                        "thu" | "thursday" => Weekday::Thu,
                        "fri" | "friday" => Weekday::Fri,
                        "sat" | "saturday" => Weekday::Sat,
                        "sun" | "sunday" => Weekday::Sun,
                        x => return Err(format!("Unknown weekday: {}", x)),
                    };
                    out.weekday = Some(res);
                    Ok(())
                }
                x => Err(format!("Expected weekday, got {}", ts(x))),
            },
            x => Err(format!("Unknown match pattern `{}`", x)),
        },
        Some(&DatePattern::Optional(ref pats)) => {
            advance = false;
            let mut iter = date.clone();
            if let Ok(()) = parse_date(out, out_tz, &mut iter, &pats[..]) {
                *date = iter
            }
            Ok(())
        }
        Some(&DatePattern::Dash) => match tok {
            Some(DateToken::Dash) => Ok(()),
            x => Err(format!("Expected `-`, got {}", ts(x))),
        },
        Some(&DatePattern::Colon) => match tok {
            Some(DateToken::Colon) => Ok(()),
            x => Err(format!("Expected `:`, got {}", ts(x))),
        },
        Some(&DatePattern::Space) => match tok {
            Some(DateToken::Space) => Ok(()),
            x => Err(format!("Expected ` `, got {}", ts(x))),
        },
    };
    if advance {
        date.next();
    }
    res.and_then(|_| parse_date(out, out_tz, date, &pat[1..]))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GenericDateTime {
    Fixed(DateTime<FixedOffset>),
    Timezone(DateTime<Tz>),
}

impl GenericDateTime {
    pub fn with_timezone<Tz: TimeZone>(&self, tz: &Tz) -> DateTime<Tz> {
        match *self {
            GenericDateTime::Fixed(ref d) => d.with_timezone(tz),
            GenericDateTime::Timezone(ref d) => d.with_timezone(tz),
        }
    }
}

fn attempt(
    now: DateTime<Local>,
    date: &[DateToken],
    pat: &[DatePattern],
) -> Result<GenericDateTime, (String, usize)> {
    let mut parsed = Parsed::new();
    let mut tz = None;
    let mut iter = date.iter().cloned().peekable();
    let res = parse_date(&mut parsed, &mut tz, &mut iter, pat);
    let count = iter.count();
    let res = if count > 0 && res.is_ok() {
        Err(format!(
            "Expected eof, got {}",
            date[date.len() - count..]
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("")
        ))
    } else {
        res
    };
    res.map_err(|e| (e, count))?;
    let time = parsed.to_naive_time();
    let date = parsed.to_naive_date();
    if let Some(tz) = tz {
        match (time, date) {
            (Ok(time), Ok(date)) => tz
                .from_local_datetime(&date.and_time(time))
                .earliest()
                .ok_or_else(|| {
                    (
                        "Datetime does not represent a valid moment in time".to_string(),
                        count,
                    )
                })
                .map(GenericDateTime::Timezone),
            (Ok(time), Err(_)) => Ok(now.with_timezone(&tz).date().and_time(time).unwrap())
                .map(GenericDateTime::Timezone),
            (Err(_), Ok(date)) => tz
                .from_local_date(&date)
                .earliest()
                .map(|x| x.and_hms(0, 0, 0))
                .ok_or_else(|| {
                    (
                        "Datetime does not represent a valid moment in time".to_string(),
                        count,
                    )
                })
                .map(GenericDateTime::Timezone),
            _ => Err(("Failed to construct a useful datetime".to_string(), count)),
        }
    } else {
        let offset = parsed
            .to_fixed_offset()
            .unwrap_or_else(|_| FixedOffset::east(0));
        match (time, date) {
            (Ok(time), Ok(date)) => offset
                .from_local_datetime(&date.and_time(time))
                .earliest()
                .ok_or_else(|| {
                    (
                        "Datetime does not represent a valid moment in time".to_string(),
                        count,
                    )
                })
                .map(GenericDateTime::Fixed),
            (Ok(time), Err(_)) => Ok(GenericDateTime::Fixed(
                now.with_timezone(&offset).date().and_time(time).unwrap(),
            )),
            (Err(_), Ok(date)) => offset
                .from_local_date(&date)
                .earliest()
                .map(|x| x.and_hms(0, 0, 0))
                .ok_or_else(|| {
                    (
                        "Datetime does not represent a valid moment in time".to_string(),
                        count,
                    )
                })
                .map(GenericDateTime::Fixed),
            _ => Err(("Failed to construct a useful datetime".to_string(), count)),
        }
    }
}

pub fn try_decode(date: &[DateToken], context: &Context) -> Result<GenericDateTime, String> {
    let mut best = None;
    for pat in &context.datepatterns {
        match attempt(context.now, date, pat) {
            Ok(datetime) => return Ok(datetime),
            Err((e, c)) => {
                //println!("{}", e);
                let better = if let Some((count, _, _)) = best {
                    c < count
                } else {
                    true
                };
                if better {
                    best = Some((c, pat, e.clone()));
                }
            }
        }
    }
    if let Some((_, pat, err)) = best {
        Err(format!(
            "Most likely pattern `{}` failed: {}",
            DatePattern::show(pat),
            err
        ))
    } else {
        Err("Invalid date literal".to_string())
    }
}

pub fn to_duration(num: &Number) -> Result<Duration, String> {
    if num.unit.len() != 1 || num.unit.get("s") != Some(&1) {
        return Err("Expected seconds".to_string());
    }
    let max = Numeric::from(i64::max_value() / 1000);
    if num.value.abs() > max {
        return Err(format!(
            "Implementation error: Number is out of range ({:?})",
            max
        ));
    }
    let ms = &num.value * &Numeric::from(1000);
    let (ms, rem) = ms.div_rem(&Numeric::from(1));
    let ns = &rem * &Numeric::from(1_000_000_000);
    Ok(Duration::milliseconds(ms.to_int().unwrap()) + Duration::nanoseconds(ns.to_int().unwrap()))
}

pub fn from_duration(duration: &Duration) -> Result<Number, String> {
    let ms = duration.num_milliseconds();
    let ns = (*duration - Duration::milliseconds(ms))
        .num_nanoseconds()
        .unwrap();
    let ms_div = BigInt::from(1_000u64);
    let ns_div = BigInt::from(1_000_000_000u64);
    let ms = BigRat::ratio(&BigInt::from(ms), &ms_div);
    let ns = BigRat::ratio(&BigInt::from(ns), &ns_div);
    Ok(Number::new_unit(
        Numeric::Rational(&ms + &ns),
        Dimension::new("s"),
    ))
}

pub fn parse_datepattern<I>(iter: &mut Peekable<I>) -> Result<Vec<DatePattern>, String>
where
    I: Iterator<Item = char>,
{
    let mut out = vec![];
    while iter.peek().is_some() {
        let res = match iter.peek().cloned().unwrap() {
            '-' => DatePattern::Dash,
            ':' => DatePattern::Colon,
            '[' => {
                iter.next();
                let res = DatePattern::Optional(parse_datepattern(iter)?);
                if iter.peek().cloned() != Some(']') {
                    return Err("Expected ]".to_string());
                } else {
                    res
                }
            }
            ']' => break,
            '\'' => {
                iter.next();
                let mut buf = String::new();
                while let Some(c) = iter.peek().cloned() {
                    if c == '\'' {
                        break;
                    } else {
                        iter.next();
                        buf.push(c);
                    }
                }
                DatePattern::Literal(buf)
            }
            x if x.is_whitespace() => {
                while iter.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
                    iter.next();
                }
                out.push(DatePattern::Space);
                continue;
            }
            x if x.is_alphabetic() => {
                let mut buf = String::new();
                while let Some(c) = iter.peek().cloned() {
                    if c.is_alphanumeric() {
                        iter.next();
                        buf.push(c);
                    } else {
                        break;
                    }
                }
                out.push(DatePattern::Match(buf));
                continue;
            }
            x => return Err(format!("Unrecognized character {}", x)),
        };
        out.push(res);
        iter.next();
    }
    Ok(out)
}

pub fn parse_datefile(file: &str) -> Vec<Vec<DatePattern>> {
    let mut defs = vec![];
    for (num, line) in file.lines().enumerate() {
        let line = line.split('#').next().unwrap();
        let line = line.trim();
        if line.is_empty() {
            continue;
        }
        let res = parse_datepattern(&mut line.chars().peekable());
        match res {
            Ok(res) => defs.push(res),
            Err(e) => println!("Line {}: {}: {}", num, e, line),
        }
    }
    defs
}

impl Context {
    #[cfg(feature = "chrono-humanize")]
    pub fn humanize<Tz: TimeZone>(&self, date: DateTime<Tz>) -> Option<String> {
        if self.use_humanize {
            use chrono_humanize::HumanTime;
            Some(HumanTime::from(date).to_string())
        } else {
            None
        }
    }

    #[cfg(not(feature = "chrono-humanize"))]
    pub fn humanize<Tz: TimeZone>(&self, _date: DateTime<Tz>) -> Option<String> {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn pattern(s: &str) -> Vec<DatePattern> {
        parse_datepattern(&mut s.chars().peekable()).unwrap()
    }

    fn parse_with_tz(date: Vec<DateToken>, pat: &str) -> (Result<(), String>, Parsed, Option<Tz>) {
        let mut parsed = Parsed::new();
        let mut tz = None;
        let pat = pattern(pat);
        let res = parse_date(&mut parsed, &mut tz, &mut date.into_iter().peekable(), &pat);

        (res, parsed, tz)
    }

    fn parse(date: Vec<DateToken>, pat: &str) -> (Result<(), String>, Parsed) {
        let (res, parsed, _) = parse_with_tz(date, pat);
        (res, parsed)
    }

    #[test]
    fn test_literal() {
        let date = vec![DateToken::Literal("abc".into())];
        let (res, parsed) = parse(date.clone(), "'abc'");
        assert_eq!(parsed, Parsed::new());
        assert!(res.is_ok());

        let (res, parsed) = parse(date, "'def'");
        assert_eq!(parsed, Parsed::new());
        assert_eq!(res, Err("Expected `def`, got `abc`".into()));
    }

    #[test]
    fn test_year_plus() {
        let mut expected = Parsed::new();
        expected.set_year(123).unwrap();

        let date = vec![
            DateToken::Plus,
            DateToken::Number(expected.year.unwrap().to_string(), None),
        ];
        let (res, parsed) = parse(date.clone(), "year");
        assert!(res.is_ok());
        assert_eq!(parsed, expected);

        let date = vec![DateToken::Number(expected.year.unwrap().to_string(), None)];
        let (res, parsed2) = parse(date.clone(), "year");
        assert!(res.is_ok());
        assert_eq!(parsed2, parsed);
    }

    #[test]
    fn test_complicated_date_input() {
        let mut expected = Parsed::new();
        expected.set_year(123).unwrap();
        expected.set_month(5).unwrap();
        expected.set_day(2).unwrap();
        expected.set_ampm(true).unwrap();
        expected.set_hour(13).unwrap();
        expected.set_minute(57).unwrap();

        let date = vec![
            DateToken::Number(expected.day.unwrap().to_string(), None),
            DateToken::Space,
            DateToken::Literal("Pm".into()),
            DateToken::Dash,
            DateToken::Number(format!("{:02}", expected.month.unwrap()), None),
            DateToken::Colon,
            DateToken::Number(expected.year.unwrap().to_string(), None),
            DateToken::Space,
            DateToken::Number(format!("{:02}", expected.hour_mod_12.unwrap()), None),
            DateToken::Dash,
            DateToken::Number(format!("{:02}", expected.minute.unwrap()), None),
            DateToken::Space,
            DateToken::Literal("May".into()),
        ];

        let (res, parsed) = parse(date, "day meridiem-monthnum:year hour12-min monthname");
        assert!(res.is_ok());
        assert_eq!(parsed, expected);
    }

    #[test]
    fn ad_bc() {
        let year = -100;
        let mut expected = Parsed::new();
        expected.set_year(year + 1).unwrap();
        expected.set_hour(7).unwrap();

        let date = vec![
            DateToken::Number(year.abs().to_string(), None),
            DateToken::Space,
            DateToken::Literal("bce".into()),
            DateToken::Space,
            DateToken::Number(format!("{:02}", expected.hour_mod_12.unwrap()), None),
            DateToken::Space,
            DateToken::Literal("am".into()),
        ];

        let (res, parsed) = parse(date, "year adbc hour12 meridiem");
        assert!(res.is_ok(), "{}", res.unwrap_err());
        assert_eq!(parsed, expected);
    }

    #[test]
    fn ad_bc_wrong() {
        for date in vec![
            vec![DateToken::Literal("foo".into())],
            vec![DateToken::Plus],
        ] {
            let (res, _) = parse(date, "adbc");
            assert!(res.is_err());
        }
    }

    #[test]
    fn wrong_length_24h() {
        let date = vec![DateToken::Number("7".into(), None)];
        let (res, _) = parse(date, "hour24");
        assert_eq!(res, Err(format!("Expected 2-digit hour24, got `{}`", 7)));
    }

    #[test]
    fn test_24h() {
        let (res, parsed) = parse(vec![DateToken::Number("23".into(), None)], "hour24");
        assert!(res.is_ok());
        assert_eq!(parsed.hour_div_12, Some(1));
        assert_eq!(parsed.hour_mod_12, Some(11));
    }

    #[test]
    fn seconds() {
        let mut expected = Parsed::new();
        expected.set_second(27).unwrap();
        expected.set_nanosecond(12345).unwrap();

        let date = vec![DateToken::Number("27".into(), Some("000012345".into()))];
        let (res, parsed) = parse(date, "sec");
        assert!(res.is_ok());
        assert_eq!(parsed, expected);

        let date = vec![DateToken::Number("27".into(), None)];
        let (res, parsed) = parse(date, "sec");
        assert!(res.is_ok());
        expected.nanosecond = None;
        assert_eq!(parsed, expected);
    }

    #[test]
    fn test_offset() {
        let date = vec![DateToken::Plus, DateToken::Number("0200".into(), None)];
        let (res, parsed) = parse(date, "offset");
        assert!(res.is_ok());
        assert_eq!(parsed.offset, Some(2 * 3600));

        let date = vec![
            DateToken::Dash,
            DateToken::Number("01".into(), None),
            DateToken::Colon,
            DateToken::Number("23".into(), None),
        ];
        let (res, parsed) = parse(date, "offset");
        assert!(res.is_ok());
        assert_eq!(parsed.offset, Some(-(1 * 60 + 23) * 60));

        let date = vec![DateToken::Literal("Europe/London".into())];
        let (res, parsed, tz) = parse_with_tz(date, "offset");
        assert!(res.is_ok(), "{}", res.unwrap_err());
        assert_eq!(tz.unwrap(), Tz::Europe__London);
        assert_eq!(parsed.offset, None);
    }

    #[test]
    fn test_weekday() {
        let date = vec![DateToken::Literal("saturday".into())];
        let (res, parsed) = parse(date, "weekday");
        assert!(res.is_ok());
        assert_eq!(parsed.weekday, Some(Weekday::Sat));

        let date = vec![DateToken::Literal("sun".into())];
        assert!(parse(date, "weekday").0.is_ok());

        let date = vec![DateToken::Literal("snu".into())];
        assert_eq!(parse(date, "weekday").0, Err("Unknown weekday: snu".into()));
    }

    #[test]
    fn test_monthname() {
        for (i, &s) in [
            "jan", "feb", "mar", "apr", "may", "june", "jul", "AUGUST", "SEp", "Oct", "novemBer",
            "dec",
        ]
        .iter()
        .enumerate()
        {
            let date = vec![DateToken::Literal(s.into())];
            let (res, parsed) = parse(date, "monthname");
            assert!(res.is_ok());
            assert_eq!(parsed.month, Some(i as u32 + 1));
        }

        let date = vec![DateToken::Literal("foobar".into())];
        let (res, parsed) = parse(date, "monthname");
        assert_eq!(res, Err("Unknown month name: foobar".into()));
        assert_eq!(parsed.month, None);
    }

    #[test]
    fn test_parse_datepattern() {
        use self::DatePattern::*;

        fn parse(s: &str) -> Result<Vec<DatePattern>, String> {
            parse_datepattern(&mut s.chars().peekable())
        }

        assert_eq!(
            parse("-:['abc']"),
            Ok(vec![Dash, Colon, Optional(vec![Literal("abc".into())])])
        );
        assert!(parse("-:['abc'").is_err());
        assert!(parse("*").is_err());
    }

    #[test]
    fn test_attempt() {
        use self::DateToken::*;
        fn n(x: &str) -> DateToken {
            Number(x.into(), None)
        }

        let now = Local::now();

        macro_rules! check_attempt {
            ($date:expr, $pat:expr) => {{
                let pat = parse_datepattern(&mut $pat.chars().peekable()).unwrap();
                attempt(now, $date, pat.as_ref())
            }};
        }

        let tz = Literal("Europe/London".into());

        let date = &[n("23"), Space, n("05"), Space, tz.clone()];
        let res = check_attempt!(date, "hour24 min offset");
        assert!(res.is_ok(), "{:?}", res);

        let date = &[n("23"), Space, tz.clone()];
        let res = check_attempt!(date, "hour24 offset");
        assert_eq!(
            res,
            Err(("Failed to construct a useful datetime".into(), 0))
        );

        let date = &[n("2018"), Space, n("01"), Space, n("01"), Space, tz.clone()];
        let res = check_attempt!(date, "year monthnum day offset");
        assert!(res.is_ok(), "{:?}", res);
    }
}