1extern crate chrono;
76extern crate scanlex;
77use chrono::prelude::*;
78
79mod errors;
80mod parser;
81mod types;
82use errors::*;
83use types::*;
84
85pub use errors::{DateError, DateResult};
86pub use types::Interval;
87
88#[derive(Clone, Copy, Debug)]
89pub enum Dialect {
90 Uk,
91 Us,
92}
93
94pub fn parse_date_string<Tz: TimeZone>(
95 s: &str,
96 now: DateTime<Tz>,
97 dialect: Dialect,
98) -> DateResult<DateTime<Tz>>
99where
100 Tz::Offset: Copy,
101{
102 let mut dp = parser::DateParser::new(s);
103 if let Dialect::Us = dialect {
104 dp = dp.american_date();
105 }
106 let d = dp.parse()?;
107
108 let tspec = match d.time {
110 Some(tspec) => tspec,
111 None => TimeSpec::new_empty(),
112 };
113 if tspec.offset.is_some() {
114 }
116 let date_time = if let Some(dspec) = d.date {
117 dspec
118 .to_date_time(now, tspec, dp.american)
119 .or_err("bad date")?
120 } else {
121 tspec.to_date_time(now.date()).or_err("bad time")?
123 };
124 Ok(date_time)
125}
126
127pub fn parse_duration(s: &str) -> DateResult<Interval> {
128 let mut dp = parser::DateParser::new(s);
129 let d = dp.parse()?;
130
131 if d.time.is_some() {
132 return date_result("unexpected time component");
133 }
134
135 if d.date.is_none() {
137 return date_result("could not parse date");
138 }
139
140 match d.date.unwrap() {
141 DateSpec::Absolute(_) => date_result("unexpected absolute date"),
142 DateSpec::FromName(_) => date_result("unexpected date component"),
143 DateSpec::Relative(skip) => Ok(skip.to_interval()),
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 const FMT_ISO: &str = "%+";
152
153 fn display(t: DateResult<DateTime<Utc>>) -> String {
154 t.unwrap().format(FMT_ISO).to_string()
155 }
156
157 #[test]
158 fn basics() {
159 let base = parse_date_string("2018-03-21 11:00", Utc::now(), Dialect::Uk).unwrap();
160
161 assert_eq!(
163 display(parse_date_string("friday", base, Dialect::Uk)),
164 "2018-03-23T00:00:00+00:00"
165 );
166 assert_eq!(
167 display(parse_date_string("friday 10:30", base, Dialect::Uk)),
168 "2018-03-23T10:30:00+00:00"
169 );
170 assert_eq!(
171 display(parse_date_string("friday 8pm", base, Dialect::Uk)),
172 "2018-03-23T20:00:00+00:00"
173 );
174
175 assert_eq!(
177 display(parse_date_string("tues", base, Dialect::Uk)),
178 "2018-03-27T00:00:00+00:00"
179 );
180
181 assert_eq!(
184 display(parse_date_string("next mon", base, Dialect::Us)),
185 "2018-03-26T00:00:00+00:00"
186 );
187 assert_eq!(
189 display(parse_date_string("next mon", base, Dialect::Uk)),
190 "2018-04-02T00:00:00+00:00"
191 );
192
193 assert_eq!(
194 display(parse_date_string("last fri 9.30", base, Dialect::Uk)),
195 "2018-03-16T09:30:00+00:00"
196 );
197
198 assert_eq!(
200 display(parse_date_string("9/11", base, Dialect::Us)),
201 "2018-09-11T00:00:00+00:00"
202 );
203 assert_eq!(
204 display(parse_date_string("last 9/11", base, Dialect::Us)),
205 "2017-09-11T00:00:00+00:00"
206 );
207 assert_eq!(
208 display(parse_date_string("last 9/11 9am", base, Dialect::Us)),
209 "2017-09-11T09:00:00+00:00"
210 );
211 assert_eq!(
212 display(parse_date_string("April 1 8.30pm", base, Dialect::Uk)),
213 "2018-04-01T20:30:00+00:00"
214 );
215
216 assert_eq!(
219 display(parse_date_string("2d", base, Dialect::Uk)),
220 "2018-03-23T11:00:00+00:00"
221 );
222 assert_eq!(
223 display(parse_date_string("2d 03:00", base, Dialect::Uk)),
224 "2018-03-23T03:00:00+00:00"
225 );
226 assert_eq!(
227 display(parse_date_string("3 weeks", base, Dialect::Uk)),
228 "2018-04-11T11:00:00+00:00"
229 );
230 assert_eq!(
231 display(parse_date_string("3h", base, Dialect::Uk)),
232 "2018-03-21T14:00:00+00:00"
233 );
234 assert_eq!(
235 display(parse_date_string("6 months", base, Dialect::Uk)),
236 "2018-09-21T00:00:00+00:00"
237 );
238 assert_eq!(
239 display(parse_date_string("6 months ago", base, Dialect::Uk)),
240 "2017-09-21T00:00:00+00:00"
241 );
242 assert_eq!(
243 display(parse_date_string("3 hours ago", base, Dialect::Uk)),
244 "2018-03-21T08:00:00+00:00"
245 );
246 assert_eq!(
247 display(parse_date_string(" -3h", base, Dialect::Uk)),
248 "2018-03-21T08:00:00+00:00"
249 );
250 assert_eq!(
251 display(parse_date_string(" -3 month", base, Dialect::Uk)),
252 "2017-12-21T00:00:00+00:00"
253 );
254
255 assert_eq!(
257 display(parse_date_string("2017-06-30", base, Dialect::Uk)),
258 "2017-06-30T00:00:00+00:00"
259 );
260 assert_eq!(
261 display(parse_date_string("30/06/17", base, Dialect::Uk)),
262 "2017-06-30T00:00:00+00:00"
263 );
264 assert_eq!(
265 display(parse_date_string("06/30/17", base, Dialect::Us)),
266 "2017-06-30T00:00:00+00:00"
267 );
268
269 assert_eq!(
271 display(parse_date_string("2017-06-30 08:20:30", base, Dialect::Uk)),
272 "2017-06-30T08:20:30+00:00"
273 );
274 assert_eq!(
275 display(parse_date_string(
276 "2017-06-30 08:20:30 +02:00",
277 base,
278 Dialect::Uk
279 )),
280 "2017-06-30T06:20:30+00:00"
281 );
282 assert_eq!(
283 display(parse_date_string(
284 "2017-06-30 08:20:30 +0200",
285 base,
286 Dialect::Uk
287 )),
288 "2017-06-30T06:20:30+00:00"
289 );
290 assert_eq!(
291 display(parse_date_string("2017-06-30T08:20:30Z", base, Dialect::Uk)),
292 "2017-06-30T08:20:30+00:00"
293 );
294 assert_eq!(
295 display(parse_date_string("2017-06-30T08:20:30", base, Dialect::Uk)),
296 "2017-06-30T08:20:30+00:00"
297 );
298 assert_eq!(
299 display(parse_date_string("2017-06-30 8.20", base, Dialect::Uk)),
300 "2017-06-30T08:20:00+00:00"
301 );
302 assert_eq!(
303 display(parse_date_string("2017-06-30 8.30pm", base, Dialect::Uk)),
304 "2017-06-30T20:30:00+00:00"
305 );
306 assert_eq!(
307 display(parse_date_string("2017-06-30 8:30pm", base, Dialect::Uk)),
308 "2017-06-30T20:30:00+00:00"
309 );
310 assert_eq!(
311 display(parse_date_string("2017-06-30 2am", base, Dialect::Uk)),
312 "2017-06-30T02:00:00+00:00"
313 );
314 assert_eq!(
315 display(parse_date_string("30 June 2018", base, Dialect::Uk)),
316 "2018-06-30T00:00:00+00:00"
317 );
318 assert_eq!(
319 display(parse_date_string("June 30, 2018", base, Dialect::Uk)),
320 "2018-06-30T00:00:00+00:00"
321 );
322 assert_eq!(
323 display(parse_date_string("June 30, 2018", base, Dialect::Uk)),
324 "2018-06-30T00:00:00+00:00"
325 );
326 }
327
328 fn get_err(r: DateResult<Interval>) -> String {
329 r.err().unwrap().to_string()
330 }
331
332 #[test]
333 fn durations() {
334 assert_eq!(parse_duration("6h").unwrap(), Interval::Seconds(6 * 3600));
335 assert_eq!(
336 parse_duration("4 hours ago").unwrap(),
337 Interval::Seconds(-4 * 3600)
338 );
339 assert_eq!(parse_duration("5 min").unwrap(), Interval::Seconds(5 * 60));
340 assert_eq!(parse_duration("10m").unwrap(), Interval::Seconds(10 * 60));
341 assert_eq!(
342 parse_duration("15m ago").unwrap(),
343 Interval::Seconds(-15 * 60)
344 );
345
346 assert_eq!(parse_duration("1 day").unwrap(), Interval::Days(1));
347 assert_eq!(parse_duration("2 days ago").unwrap(), Interval::Days(-2));
348 assert_eq!(parse_duration("3 weeks").unwrap(), Interval::Days(21));
349 assert_eq!(parse_duration("2 weeks ago").unwrap(), Interval::Days(-14));
350
351 assert_eq!(parse_duration("1 month").unwrap(), Interval::Months(1));
352 assert_eq!(parse_duration("6 months").unwrap(), Interval::Months(6));
353 assert_eq!(parse_duration("8 years").unwrap(), Interval::Months(12 * 8));
354
355 assert_eq!(
357 get_err(parse_duration("2020-01-01")),
358 "unexpected absolute date"
359 );
360 assert_eq!(
361 get_err(parse_duration("2 days 15:00")),
362 "unexpected time component"
363 );
364 assert_eq!(
365 get_err(parse_duration("tuesday")),
366 "unexpected date component"
367 );
368 assert_eq!(
369 get_err(parse_duration("bananas")),
370 "expected week day or month name"
371 );
372 }
373}