chrono_english/
types.rs

1use chrono::prelude::*;
2use chrono::Duration;
3
4// implements next/last direction in expressions like 'next friday' and 'last 4 july'
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum Direction {
7    Next,
8    Last,
9    Here,
10}
11
12impl Direction {
13    pub fn from_name(s: &str) -> Option<Direction> {
14        use Direction::*;
15        match s {
16            "next" => Some(Next),
17            "last" => Some(Last),
18            _ => None,
19        }
20    }
21}
22
23// this is a day-month with direction, like 'next 10 Dec'
24#[derive(Debug)]
25pub struct YearDate {
26    pub direct: Direction,
27    pub month: u32,
28    pub day: u32,
29}
30
31// for expressions like 'friday' and 'July' modifiable with next/last
32#[derive(Debug)]
33pub struct NamedDate {
34    pub direct: Direction,
35    pub unit: u32,
36}
37
38impl NamedDate {
39    pub fn new(direct: Direction, unit: u32) -> NamedDate {
40        NamedDate {
41            direct: direct,
42            unit: unit,
43        }
44    }
45}
46
47// all expressions modifiable with next/last; 'fri', 'jul', '5 may'.
48#[derive(Debug)]
49pub enum ByName {
50    WeekDay(NamedDate),
51    MonthName(NamedDate),
52    DayMonth(YearDate),
53}
54
55fn add_days<Tz: TimeZone>(base: DateTime<Tz>, days: i64) -> Option<DateTime<Tz>> {
56    base.checked_add_signed(Duration::days(days))
57}
58
59//fn next_last_direction<Tz: TimeZone>(date: Date<Tz>, base: Date<Tz>, direct: Direction) -> Option<i32> {
60
61fn next_last_direction<T: PartialOrd + Copy>(date: T, base: T, direct: Direction) -> Option<i32> {
62    let mut res = None;
63    if date > base {
64        if direct == Direction::Last {
65            res = Some(-1);
66        }
67    } else if date < base {
68        if direct == Direction::Next {
69            res = Some(1)
70        }
71    }
72    res
73}
74
75impl ByName {
76    pub fn from_name(s: &str, direct: Direction) -> Option<ByName> {
77        Some(if let Some(wd) = week_day(s) {
78            ByName::WeekDay(NamedDate::new(direct, wd))
79        } else if let Some(mn) = month_name(s) {
80            ByName::MonthName(NamedDate::new(direct, mn))
81        } else {
82            return None;
83        })
84    }
85
86    pub fn as_month(&self) -> Option<u32> {
87        match *self {
88            ByName::MonthName(ref nd) => Some(nd.unit),
89            _ => None,
90        }
91    }
92
93    pub fn from_day_month(d: u32, m: u32, direct: Direction) -> ByName {
94        ByName::DayMonth(YearDate {
95            direct: direct,
96            day: d,
97            month: m,
98        })
99    }
100
101    pub fn to_date_time<Tz: TimeZone>(
102        self,
103        base: DateTime<Tz>,
104        ts: TimeSpec,
105        american: bool,
106    ) -> Option<DateTime<Tz>>
107    where
108        <Tz as TimeZone>::Offset: Copy,
109    {
110        let this_year = base.year();
111        match self {
112            ByName::WeekDay(mut nd) => {
113                // a plain 'Friday' means the same as 'next Friday'.
114                // an _explicit_ 'next Friday' has dialect-dependent meaning!
115                // In UK English, it means 'Friday of next week',
116                // but in US English, just the next Friday
117                let mut extra_week = 0;
118                match nd.direct {
119                    Direction::Here => nd.direct = Direction::Next,
120                    Direction::Next => {
121                        if !american {
122                            extra_week = 7;
123                        }
124                    }
125                    _ => (),
126                };
127                let this_day = base.weekday().num_days_from_monday() as i64;
128                let that_day = nd.unit as i64;
129                let diff_days = that_day - this_day;
130                let mut date = add_days(base, diff_days)?;
131                if let Some(correct) = next_last_direction(date, base, nd.direct) {
132                    date = add_days(date, 7 * correct as i64)?;
133                }
134                if extra_week > 0 {
135                    date = add_days(date, extra_week)?;
136                }
137                if diff_days == 0 {
138                    // same day - comparing times will determine which way we swing...
139                    let base_time = base.time();
140                    let this_time = NaiveTime::from_hms(ts.hour, ts.min, ts.sec);
141                    if let Some(correct) = next_last_direction(this_time, base_time, nd.direct) {
142                        date = add_days(date, 7 * correct as i64)?;
143                    }
144                }
145                ts.to_date_time(date.date())
146            }
147            ByName::MonthName(nd) => {
148                let mut date = base.timezone().ymd_opt(this_year, nd.unit, 1).single()?;
149                if let Some(correct) = next_last_direction(date, base.date(), nd.direct) {
150                    date = base
151                        .timezone()
152                        .ymd_opt(this_year + correct, nd.unit, 1)
153                        .single()?;
154                }
155                ts.to_date_time(date)
156            }
157            ByName::DayMonth(yd) => {
158                let mut date = base
159                    .timezone()
160                    .ymd_opt(this_year, yd.month, yd.day)
161                    .single()?;
162                if let Some(correct) = next_last_direction(date, base.date(), yd.direct) {
163                    date = base
164                        .timezone()
165                        .ymd_opt(this_year + correct, yd.month, yd.day)
166                        .single()?;
167                }
168                ts.to_date_time(date)
169            }
170        }
171    }
172}
173
174#[derive(Debug)]
175pub struct AbsDate {
176    pub year: i32,
177    pub month: u32,
178    pub day: u32,
179}
180
181impl AbsDate {
182    pub fn to_date<Tz: TimeZone>(self, base: DateTime<Tz>) -> Option<Date<Tz>> {
183        base.timezone()
184            .ymd_opt(self.year, self.month, self.day)
185            .single()
186    }
187}
188
189/// A generic amount of time, in either seconds, days, or months.
190///
191/// This way, a user can decide how they want to treat days (which do
192/// not always have the same number of seconds) or months (which do not always
193/// have the same number of days).
194//
195// Skipping a given number of time units.
196// The subtlety is that we treat duration as seconds until we get
197// to months, where we want to preserve dates. So adding a month to
198// '5 May' gives '5 June'. Adding a month to '30 Jan' gives 'Feb 28' or 'Feb 29'
199// depending on whether this is a leap year.
200#[derive(Debug, PartialEq)]
201pub enum Interval {
202    Seconds(i32),
203    Days(i32),
204    Months(i32),
205}
206
207#[derive(Debug)]
208pub struct Skip {
209    pub unit: Interval,
210    pub skip: i32,
211}
212
213impl Skip {
214    pub fn to_date_time<Tz: TimeZone>(
215        self,
216        base: DateTime<Tz>,
217        ts: TimeSpec,
218    ) -> Option<DateTime<Tz>> {
219        Some(match self.unit {
220            Interval::Seconds(secs) => {
221                base.checked_add_signed(Duration::seconds((secs as i64) * (self.skip as i64)))
222                    .unwrap() // <--- !!!!
223            }
224            Interval::Days(days) => {
225                let secs = 60 * 60 * 24 * days;
226                let date = base
227                    .checked_add_signed(Duration::seconds((secs as i64) * (self.skip as i64)))
228                    .unwrap();
229                if !ts.empty() {
230                    ts.to_date_time(date.date())?
231                } else {
232                    date
233                }
234            }
235            Interval::Months(mm) => {
236                let (y, m0, d) = (base.year(), (base.month() - 1) as i32, base.day());
237                let delta = mm * self.skip;
238                // our new month number
239                let mm = m0 + delta;
240                // which may run over to the next year and so forth
241                let (y, m) = if mm >= 0 {
242                    (y + mm / 12, mm % 12 + 1)
243                } else {
244                    let pmm = 12 - mm;
245                    (y - pmm / 12, 12 - pmm % 12 + 1)
246                };
247                // let chrono work out if the result makes sense
248                let mut date = base.timezone().ymd_opt(y, m as u32, d).single();
249                // dud dates like Feb 30 may result, so we back off...
250                let mut d = d;
251                while date.is_none() {
252                    d -= 1;
253                    if d == 0 || d < 28 {
254                        // sanity check...
255                        eprintln!("fkd date");
256                        return None;
257                    }
258                    date = base.timezone().ymd_opt(y, m as u32, d).single();
259                }
260                ts.to_date_time(date.unwrap())?
261            }
262        })
263    }
264
265    pub fn to_interval(self) -> Interval {
266        use Interval::*;
267
268        match self.unit {
269            Seconds(s) => Seconds(s * self.skip),
270            Days(d) => Days(d * self.skip),
271            Months(m) => Months(m * self.skip),
272        }
273    }
274}
275
276#[derive(Debug)]
277pub enum DateSpec {
278    Absolute(AbsDate), // Y M D (e.g. 2018-06-02, 4 July 2017)
279    Relative(Skip),    // n U (e.g. 2min, 3 years ago, -2d)
280    FromName(ByName),  // (e.g. 'next fri', 'jul')
281}
282
283impl DateSpec {
284    pub fn absolute(y: u32, m: u32, d: u32) -> DateSpec {
285        DateSpec::Absolute(AbsDate {
286            year: y as i32,
287            month: m,
288            day: d,
289        })
290    }
291
292    pub fn from_day_month(d: u32, m: u32, direct: Direction) -> DateSpec {
293        DateSpec::FromName(ByName::from_day_month(d, m, direct))
294    }
295
296    pub fn skip(unit: Interval, n: i32) -> DateSpec {
297        DateSpec::Relative(Skip {
298            unit: unit,
299            skip: n,
300        })
301    }
302
303    pub fn to_date_time<Tz: TimeZone>(
304        self,
305        base: DateTime<Tz>,
306        ts: TimeSpec,
307        american: bool,
308    ) -> Option<DateTime<Tz>>
309    where
310        Tz::Offset: Copy,
311    {
312        use DateSpec::*;
313        match self {
314            Absolute(ad) => ts.to_date_time(ad.to_date(base)?),
315            Relative(skip) => skip.to_date_time(base, ts), // might need time
316            FromName(byname) => byname.to_date_time(base, ts, american),
317        }
318    }
319}
320
321#[derive(Debug)]
322pub struct TimeSpec {
323    pub hour: u32,
324    pub min: u32,
325    pub sec: u32,
326    pub empty: bool,
327    pub offset: Option<i64>,
328    pub microsec: u32,
329}
330
331impl TimeSpec {
332    pub fn new(hour: u32, min: u32, sec: u32, microsec: u32) -> TimeSpec {
333        TimeSpec {
334            hour,
335            min,
336            sec,
337            empty: false,
338            offset: None,
339            microsec,
340        }
341    }
342
343    pub fn new_with_offset(hour: u32, min: u32, sec: u32, offset: i64, microsec: u32) -> TimeSpec {
344        TimeSpec {
345            hour,
346            min,
347            sec,
348            empty: false,
349            offset: Some(offset),
350            microsec,
351        }
352    }
353
354    pub fn new_empty() -> TimeSpec {
355        TimeSpec {
356            hour: 0,
357            min: 0,
358            sec: 0,
359            empty: true,
360            offset: None,
361            microsec: 0,
362        }
363    }
364
365    pub fn empty(&self) -> bool {
366        self.empty
367    }
368
369    pub fn to_date_time<Tz: TimeZone>(self, d: Date<Tz>) -> Option<DateTime<Tz>> {
370        let dt = d.and_hms_micro(self.hour, self.min, self.sec, self.microsec);
371        if let Some(offs) = self.offset {
372            let zoffset = dt.offset().clone();
373            let tstamp = dt.timestamp() - offs + zoffset.fix().local_minus_utc() as i64;
374            let nd = NaiveDateTime::from_timestamp(tstamp, 1000 * self.microsec);
375            Some(DateTime::from_utc(nd, zoffset))
376        } else {
377            Some(dt)
378        }
379    }
380}
381
382#[derive(Debug)]
383pub struct DateTimeSpec {
384    pub date: Option<DateSpec>,
385    pub time: Option<TimeSpec>,
386}
387
388// same as chrono's 'count days from monday' convention
389pub fn week_day(s: &str) -> Option<u32> {
390    if s.len() < 3 {
391        return None;
392    }
393    Some(match &s[0..3] {
394        "sun" => 6,
395        "mon" => 0,
396        "tue" => 1,
397        "wed" => 2,
398        "thu" => 3,
399        "fri" => 4,
400        "sat" => 5,
401        _ => return None,
402    })
403}
404
405pub fn month_name(s: &str) -> Option<u32> {
406    if s.len() < 3 {
407        return None;
408    }
409    Some(match &s[0..3] {
410        "jan" => 1,
411        "feb" => 2,
412        "mar" => 3,
413        "apr" => 4,
414        "may" => 5,
415        "jun" => 6,
416        "jul" => 7,
417        "aug" => 8,
418        "sep" => 9,
419        "oct" => 10,
420        "nov" => 11,
421        "dec" => 12,
422        _ => return None,
423    })
424}
425
426pub fn time_unit(s: &str) -> Option<Interval> {
427    use Interval::*;
428    let name = if s.len() < 3 {
429        match &s[0..1] {
430            "s" => "sec",
431            "m" => "min",
432            "h" => "hou",
433            "w" => "wee",
434            "d" => "day",
435            "y" => "yea",
436            _ => return None,
437        }
438    } else {
439        &s[0..3]
440    };
441    Some(match name {
442        "sec" => Seconds(1),
443        "min" => Seconds(60),
444        "hou" => Seconds(60 * 60),
445        "day" => Days(1),
446        "wee" => Days(7),
447        "mon" => Months(1),
448        "yea" => Months(12),
449        _ => return None,
450    })
451}