Skip to main content

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