edtf/
chrono_interop.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright © 2021 Corporation for Digital Scholarship
6
7use core::convert::TryFrom;
8use core::num::NonZeroU8;
9
10use chrono::{Datelike, NaiveDate, Offset, TimeZone, Timelike};
11
12use crate::level_1::packed::{Certainty, PackedInt, PackedU8, PackedYear};
13use crate::{DateComplete, DateTime, GetTimezone, Time, TzOffset};
14
15/// This implementation maps to an EDTF timestamp without any timezone information attached.
16impl GetTimezone for chrono::NaiveDate {
17    fn tz_offset(&self) -> TzOffset {
18        TzOffset::Unspecified
19    }
20}
21
22/// This implementation maps to an EDTF timestamp with a `Z` on the end.
23impl GetTimezone for chrono::DateTime<chrono::Utc> {
24    fn tz_offset(&self) -> TzOffset {
25        TzOffset::Utc
26    }
27}
28
29/// This implementation maps to an EDTF timestamp with a timezone offset like `+04:00`.
30impl GetTimezone for chrono::DateTime<chrono::FixedOffset> {
31    fn tz_offset(&self) -> TzOffset {
32        let offset = self.offset();
33        TzOffset::Minutes(offset.local_minus_utc() / 60)
34    }
35}
36
37/// This implementation maps to an EDTF timestamp with a timezone offset like `+04:00`.
38impl GetTimezone for chrono::DateTime<TzOffset> {
39    fn tz_offset(&self) -> TzOffset {
40        *self.offset()
41    }
42}
43
44impl<DT> From<DT> for DateTime
45where
46    DT: Datelike,
47    DT: Timelike,
48    DT: GetTimezone,
49{
50    fn from(chrono_dt: DT) -> DateTime {
51        let year = chrono_dt.year();
52        let month = NonZeroU8::new(chrono_dt.month() as u8).unwrap();
53        let day = NonZeroU8::new(chrono_dt.day() as u8).unwrap();
54        let hh = chrono_dt.hour() as u8;
55        let mm = chrono_dt.minute() as u8;
56        let ss = chrono_dt.second() as u8;
57        let date = DateComplete { year, month, day };
58        let date = date
59            .validate()
60            .expect("chrono::Datelike should return valid values");
61        let tz = chrono_dt.tz_offset();
62        let time = Time { hh, mm, ss, tz };
63        DateTime { date, time }
64    }
65}
66
67impl<DT> From<DT> for crate::level_0::Edtf
68where
69    DT: Datelike,
70    DT: Timelike,
71    DT: GetTimezone,
72{
73    fn from(chrono_dt: DT) -> crate::level_0::Edtf {
74        crate::level_0::Edtf::DateTime(chrono_dt.into())
75    }
76}
77
78impl<DT> From<DT> for crate::level_1::Edtf
79where
80    DT: Datelike,
81    DT: Timelike,
82    DT: GetTimezone,
83{
84    fn from(chrono_dt: DT) -> crate::level_1::Edtf {
85        crate::level_1::Edtf::DateTime(chrono_dt.into())
86    }
87}
88
89impl DateTime {
90    fn with_date(&self, date: DateComplete) -> Self {
91        let Self { date: _, time } = *self;
92        Self { date, time }
93    }
94    fn with_time(&self, time: Time) -> Self {
95        let Self { date, time: _ } = *self;
96        Self { date, time }
97    }
98}
99
100impl DateComplete {
101    /// Converts self to a [chrono::NaiveDate]
102    pub fn to_chrono(&self) -> NaiveDate {
103        NaiveDate::from_ymd(self.year, self.month.get() as u32, self.day.get() as u32)
104    }
105}
106
107/// Converts from [chrono::NaiveDate].
108impl From<NaiveDate> for DateComplete {
109    fn from(naive: NaiveDate) -> Self {
110        Self {
111            year: naive.year(),
112            month: NonZeroU8::new(naive.month() as u8).unwrap(),
113            day: NonZeroU8::new(naive.day() as u8).unwrap(),
114        }
115    }
116}
117
118impl crate::level_0::Date {
119    /// If this date is complete, i.e. it has a month and a day, produces a [chrono::NaiveDate].
120    /// Also available via an [core::convert::TryFrom] implementation on [chrono::NaiveDate].
121    pub fn to_chrono(&self) -> Option<NaiveDate> {
122        if let (Some(month), Some(day)) = (self.month, self.day) {
123            return Some(NaiveDate::from_ymd(
124                self.year,
125                month.get() as u32,
126                day.get() as u32,
127            ));
128        }
129        None
130    }
131}
132
133/// Attempts conversion via [crate::level_0::Date::to_chrono].
134impl TryFrom<crate::level_0::Date> for NaiveDate {
135    type Error = ();
136    fn try_from(value: crate::level_0::Date) -> Result<Self, Self::Error> {
137        value.to_chrono().ok_or(())
138    }
139}
140
141impl crate::level_1::Date {
142    /// If this date is complete, i.e. it has a month and a day, produces a [chrono::NaiveDate].
143    /// Also available via an [core::convert::TryFrom] implementation on [chrono::NaiveDate].
144    pub fn to_chrono(&self) -> Option<NaiveDate> {
145        if let (Some(month), Some(day)) = (self.month, self.day) {
146            return Some(NaiveDate::from_ymd(
147                self.year.unpack().0,
148                month.unpack().0 as u32,
149                day.unpack().0 as u32,
150            ));
151        }
152        None
153    }
154}
155
156/// Attempts conversion via [crate::level_1::Date::to_chrono].
157impl TryFrom<crate::level_1::Date> for NaiveDate {
158    type Error = ();
159    fn try_from(value: crate::level_1::Date) -> Result<Self, Self::Error> {
160        value.to_chrono().ok_or(())
161    }
162}
163
164/// Converts from [chrono::NaiveDate], into a Date with day precision, and with no uncertainty
165/// flags set.
166impl From<NaiveDate> for crate::level_1::Date {
167    fn from(naive: NaiveDate) -> Self {
168        Self {
169            year: PackedYear::pack(naive.year(), Default::default()).unwrap(),
170            month: PackedU8::pack(naive.month() as u8, Default::default()),
171            day: PackedU8::pack(naive.day() as u8, Default::default()),
172            certainty: Certainty::Certain,
173        }
174    }
175}
176
177/// Convenience [chrono::Datelike] implementation, which mostly relies on internal conversion to
178/// [chrono::NaiveDate].
179impl Datelike for DateComplete {
180    fn year(&self) -> i32 {
181        self.year
182    }
183
184    fn month(&self) -> u32 {
185        self.month.get() as u32
186    }
187
188    fn month0(&self) -> u32 {
189        self.month.get() as u32 - 1
190    }
191
192    fn day(&self) -> u32 {
193        self.day.get() as u32
194    }
195
196    fn day0(&self) -> u32 {
197        self.day() - 1
198    }
199
200    fn ordinal(&self) -> u32 {
201        self.to_chrono().ordinal()
202    }
203    fn ordinal0(&self) -> u32 {
204        self.to_chrono().ordinal0()
205    }
206
207    fn weekday(&self) -> chrono::Weekday {
208        self.to_chrono().weekday()
209    }
210
211    fn iso_week(&self) -> chrono::IsoWeek {
212        self.to_chrono().iso_week()
213    }
214
215    fn with_year(&self, year: i32) -> Option<Self> {
216        self.to_chrono().with_year(year).map(Self::from)
217    }
218
219    fn with_month(&self, month: u32) -> Option<Self> {
220        self.to_chrono().with_month(month).map(Self::from)
221    }
222
223    fn with_month0(&self, month0: u32) -> Option<Self> {
224        self.to_chrono().with_month0(month0).map(Self::from)
225    }
226
227    fn with_day(&self, day: u32) -> Option<Self> {
228        self.to_chrono().with_day(day).map(Self::from)
229    }
230
231    fn with_day0(&self, day0: u32) -> Option<Self> {
232        self.to_chrono().with_day0(day0).map(Self::from)
233    }
234
235    fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
236        self.to_chrono().with_ordinal(ordinal).map(Self::from)
237    }
238
239    fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
240        self.to_chrono().with_ordinal0(ordinal0).map(Self::from)
241    }
242}
243
244/// Convenience [chrono::Datelike] implementation, which mostly relies on internal conversion to
245/// [chrono::NaiveDate].
246impl Datelike for DateTime {
247    fn year(&self) -> i32 {
248        self.date.year()
249    }
250
251    fn month(&self) -> u32 {
252        self.date.month()
253    }
254
255    fn month0(&self) -> u32 {
256        self.date.month0()
257    }
258
259    fn day(&self) -> u32 {
260        self.date.day()
261    }
262
263    fn day0(&self) -> u32 {
264        self.date.day0()
265    }
266
267    fn ordinal(&self) -> u32 {
268        self.date.ordinal()
269    }
270
271    fn ordinal0(&self) -> u32 {
272        self.date.ordinal0()
273    }
274
275    fn weekday(&self) -> chrono::Weekday {
276        self.date.weekday()
277    }
278
279    fn iso_week(&self) -> chrono::IsoWeek {
280        self.date.iso_week()
281    }
282
283    fn with_year(&self, year: i32) -> Option<Self> {
284        self.date.with_year(year).map(|date| self.with_date(date))
285    }
286
287    fn with_month(&self, month: u32) -> Option<Self> {
288        self.date.with_month(month).map(|date| self.with_date(date))
289    }
290
291    fn with_month0(&self, month0: u32) -> Option<Self> {
292        self.date
293            .with_month0(month0)
294            .map(|date| self.with_date(date))
295    }
296
297    fn with_day(&self, day: u32) -> Option<Self> {
298        self.date.with_day(day).map(|date| self.with_date(date))
299    }
300
301    fn with_day0(&self, day0: u32) -> Option<Self> {
302        self.date.with_day0(day0).map(|date| self.with_date(date))
303    }
304
305    fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
306        self.date
307            .with_ordinal(ordinal)
308            .map(|date| self.with_date(date))
309    }
310
311    fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
312        self.date
313            .with_ordinal0(ordinal0)
314            .map(|date| self.with_date(date))
315    }
316}
317
318impl Timelike for Time {
319    fn hour(&self) -> u32 {
320        self.hh as u32
321    }
322
323    fn minute(&self) -> u32 {
324        self.mm as u32
325    }
326
327    fn second(&self) -> u32 {
328        self.ss as u32
329    }
330
331    fn nanosecond(&self) -> u32 {
332        0
333    }
334
335    fn with_hour(&self, hour: u32) -> Option<Self> {
336        if hour > 23 {
337            return None;
338        }
339        Some(Self {
340            hh: hour as u8,
341            ..*self
342        })
343    }
344
345    fn with_minute(&self, min: u32) -> Option<Self> {
346        if min > 59 {
347            return None;
348        }
349        Some(Self {
350            mm: min as u8,
351            ..*self
352        })
353    }
354
355    fn with_second(&self, sec: u32) -> Option<Self> {
356        if sec > 60 {
357            return None;
358        }
359        if sec == 60 && !(self.hh == 23 && self.mm == 59) {
360            return None;
361        }
362        Some(Self {
363            ss: sec as u8,
364            ..*self
365        })
366    }
367
368    fn with_nanosecond(&self, _nano: u32) -> Option<Self> {
369        Some(*self)
370    }
371}
372
373impl Timelike for DateTime {
374    fn hour(&self) -> u32 {
375        self.time.hour()
376    }
377
378    fn minute(&self) -> u32 {
379        self.time.minute()
380    }
381
382    fn second(&self) -> u32 {
383        self.time.second()
384    }
385
386    fn nanosecond(&self) -> u32 {
387        self.time.nanosecond()
388    }
389
390    fn with_hour(&self, hour: u32) -> Option<Self> {
391        self.time.with_hour(hour).map(|t| self.with_time(t))
392    }
393
394    fn with_minute(&self, min: u32) -> Option<Self> {
395        self.time.with_minute(min).map(|t| self.with_time(t))
396    }
397
398    fn with_second(&self, sec: u32) -> Option<Self> {
399        self.time.with_second(sec).map(|t| self.with_time(t))
400    }
401
402    fn with_nanosecond(&self, _nano: u32) -> Option<Self> {
403        Some(*self)
404    }
405}
406
407impl Offset for TzOffset {
408    fn fix(&self) -> chrono::FixedOffset {
409        match *self {
410            TzOffset::Unspecified => chrono::FixedOffset::east(0),
411            TzOffset::Utc => chrono::FixedOffset::east(0),
412            TzOffset::Hours(h) => chrono::FixedOffset::east(h * 3600),
413            TzOffset::Minutes(min) => chrono::FixedOffset::east(min * 60),
414        }
415    }
416}
417
418impl TimeZone for TzOffset {
419    type Offset = Self;
420
421    fn from_offset(offset: &Self::Offset) -> Self {
422        *offset
423    }
424
425    fn offset_from_local_date(&self, _local: &NaiveDate) -> chrono::LocalResult<Self::Offset> {
426        chrono::LocalResult::Single(*self)
427    }
428
429    fn offset_from_local_datetime(
430        &self,
431        _local: &chrono::NaiveDateTime,
432    ) -> chrono::LocalResult<Self::Offset> {
433        chrono::LocalResult::Single(*self)
434    }
435
436    fn offset_from_utc_date(&self, _utc: &NaiveDate) -> Self::Offset {
437        *self
438    }
439
440    fn offset_from_utc_datetime(&self, _utc: &chrono::NaiveDateTime) -> Self::Offset {
441        *self
442    }
443}
444
445#[test]
446fn timezone_impl() {
447    let off = TzOffset::Hours(4);
448    let ch = off.ymd(2019, 8, 7).and_hms(19, 7, 56);
449    let edtf = crate::level_1::Edtf::from(ch).as_datetime();
450    assert_eq!(
451        edtf,
452        Some(DateTime {
453            date: DateComplete::from_ymd(2019, 8, 7),
454            time: Time {
455                hh: 19,
456                mm: 7,
457                ss: 56,
458                tz: TzOffset::Hours(4)
459            }
460        })
461    );
462}
463
464#[test]
465fn timezone_impl_unspec() {
466    let off = TzOffset::Unspecified;
467    let ch = off.ymd(2019, 8, 7).and_hms(19, 7, 56);
468    let edtf = crate::level_1::Edtf::from(ch).as_datetime();
469    assert_eq!(
470        edtf,
471        Some(DateTime {
472            date: DateComplete::from_ymd(2019, 8, 7),
473            time: Time {
474                hh: 19,
475                mm: 7,
476                ss: 56,
477                tz: TzOffset::Unspecified,
478            }
479        })
480    );
481}
482
483#[cfg(test)]
484mod test {
485    #[test]
486    fn to_chrono() {
487        use crate::level_1::Edtf;
488        use chrono::TimeZone;
489        let utc = chrono::Utc;
490        assert_eq!(
491            Edtf::parse("2004-02-29T01:47:00+00:00")
492                .unwrap()
493                .as_datetime()
494                .unwrap()
495                .to_chrono(&utc),
496            utc.ymd(2004, 02, 29).and_hms(01, 47, 00)
497        );
498        assert_eq!(
499            Edtf::parse("2004-02-29T01:47:00")
500                .unwrap()
501                .as_datetime()
502                .unwrap()
503                .to_chrono(&utc),
504            utc.ymd(2004, 02, 29).and_hms(01, 47, 00)
505        );
506    }
507}