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