mirror of
https://github.com/tiffany352/rink-rs
synced 2024-11-10 13:44:15 +00:00
2d84daf064
There is now a dedicated lexer for date literals, which handles ISO 8601 dates (with the T) properly now. Parsing of date patterns is handled in date.rs now, instead of in unit_defs.rs.
327 lines
13 KiB
Rust
327 lines
13 KiB
Rust
// 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 ast::{DatePattern, DateToken, show_datepattern};
|
|
use chrono::format::Parsed;
|
|
use chrono::{Weekday, DateTime, UTC, FixedOffset, Duration, Date};
|
|
use eval::Context;
|
|
use number::Number;
|
|
use std::iter::Peekable;
|
|
|
|
pub fn parse_date<I>(
|
|
out: &mut Parsed,
|
|
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) => x.borrow().to_string(),
|
|
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 res = match pat.first() {
|
|
None if tok.is_some() => Err(format!("Expected eof, got {}", tok.unwrap())),
|
|
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", 2, day),
|
|
"hour12" => numeric_match!("hour12", 2, hour_mod_12),
|
|
"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),
|
|
"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)))
|
|
},
|
|
"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" => {
|
|
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 s = match take!(DateToken::Plus | DateToken::Dash) {
|
|
DateToken::Plus => 1, DateToken::Dash => -1, _ => panic!()
|
|
};
|
|
let h = take!(DateToken::Number(s, None), s);
|
|
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)) => {
|
|
let mut iter = date.clone();
|
|
match parse_date(out, &mut iter, &pats[..]) {
|
|
Ok(()) => *date = iter,
|
|
Err(_) => ()
|
|
};
|
|
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) => Ok(()),
|
|
};
|
|
date.next();
|
|
match res {
|
|
Ok(()) => parse_date(out, date, &pat[1..]),
|
|
Err(e) => Err(e)
|
|
}
|
|
}
|
|
|
|
pub fn try_decode(date: &[DateToken], context: &Context) -> Result<DateTime<FixedOffset>, String> {
|
|
let mut best = None;
|
|
for pat in &context.datepatterns {
|
|
let attempt = || {
|
|
let mut parsed = Parsed::new();
|
|
let mut iter = date.iter().cloned().peekable();
|
|
let res = parse_date(&mut parsed, &mut iter, &pat[..]);
|
|
let count = iter.count();
|
|
try!(res.map_err(|e| (e, count)));
|
|
let offset = parsed.to_fixed_offset().unwrap_or(FixedOffset::east(0));
|
|
let time = parsed.to_naive_time();
|
|
let date = parsed.to_naive_date();
|
|
match (time, date) {
|
|
(Ok(time), Ok(date)) =>
|
|
Ok(DateTime::<FixedOffset>::from_utc(date.and_time(time), offset)),
|
|
(Ok(time), Err(_)) =>
|
|
Ok(UTC::now().with_timezone(&offset).date().and_time(time).unwrap()),
|
|
(Err(_), Ok(date)) =>
|
|
Ok(Date::<FixedOffset>::from_utc(date, offset).and_hms(0, 0, 0)),
|
|
_ => Err((format!("Failed to construct a useful datetime"), count))
|
|
}
|
|
};
|
|
match attempt() {
|
|
Ok(datetime) => return Ok(datetime),
|
|
Err((e, c)) => {
|
|
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: {}", show_datepattern(pat), err))
|
|
} else {
|
|
Err(format!("Invalid date literal"))
|
|
}
|
|
}
|
|
|
|
pub fn to_duration(num: &Number) -> Result<Duration, String> {
|
|
use gmp::mpq::Mpq;
|
|
use gmp::mpz::Mpz;
|
|
|
|
if num.1.len() != 1 || num.1.get(&"s".to_owned()) != Some(&1) {
|
|
return Err(format!("Expected seconds"))
|
|
}
|
|
let max = Mpq::ratio(&Mpz::from(i64::max_value() / 1000), &Mpz::one());
|
|
if num.0.abs() > max {
|
|
return Err(format!("Implementation error: Number is out of range ({:?})", max))
|
|
}
|
|
let num: Option<i64> = (&(&num.0.get_num() / &num.0.get_den())).into();
|
|
Ok(Duration::seconds(num.unwrap()))
|
|
}
|
|
|
|
pub fn from_duration(duration: &Duration) -> Result<Number, String> {
|
|
use gmp::mpq::Mpq;
|
|
use gmp::mpz::Mpz;
|
|
use std::rc::Rc;
|
|
|
|
let ns = try!(duration.num_nanoseconds()
|
|
.ok_or(format!("Implementation error: Duration is out of range")));
|
|
let div = Mpz::from(1_000_000_000);
|
|
Ok(Number::new_unit(Mpq::ratio(&Mpz::from(ns), &div), Rc::new("s".to_owned())))
|
|
}
|
|
|
|
pub fn now() -> DateTime<FixedOffset> {
|
|
UTC::now().with_timezone(&FixedOffset::east(0))
|
|
}
|
|
|
|
pub fn parse_datepattern<I>(mut 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(try!(parse_datepattern(iter)));
|
|
if iter.peek().cloned() != Some(']') {
|
|
return Err(format!("Expected ]"))
|
|
} 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.len() == 0 {
|
|
continue
|
|
}
|
|
let res = parse_datepattern(&mut line.chars().peekable());
|
|
match res {
|
|
Ok(res) => defs.push(res),
|
|
Err(e) => println!("Line {}: {}: {}", num, e, line),
|
|
}
|
|
}
|
|
defs
|
|
}
|