Skip to main content

muon_rs/
datetime.rs

1// datetime.rs
2//
3// Copyright (c) 2019  Douglas Lau
4//
5//! Module for RFC 3339 dates and times.
6use crate::error::ParseError;
7use serde::{de, ser};
8use std::fmt;
9use std::str::FromStr;
10
11/// Date and time with offset
12///
13/// Formatted and validated as
14/// [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6) `date-time`.
15/// ```
16/// use muon_rs::DateTime;
17/// let datetime = "2019-08-07T16:35:21.363-06:00".parse::<DateTime>().unwrap();
18/// let date = datetime.date();
19/// let time = datetime.time();
20/// let offset = datetime.time_offset();
21/// ```
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct DateTime {
24    date: Date,
25    time: Time,
26    time_offset: TimeOffset,
27}
28
29/// Date with no time or offset
30///
31/// Formatted and validated as
32/// [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6) `full-date`.
33/// ```
34/// use muon_rs::Date;
35/// let date = "2019-08-07".parse::<Date>().unwrap();
36/// let year = date.year();
37/// let month = date.month();
38/// let day = date.day();
39/// ```
40#[derive(Clone, Copy, Debug, PartialEq)]
41pub struct Date {
42    year: u16,
43    month: u8,
44    day: u8,
45}
46
47/// Time with no date or offset
48///
49/// Formatted and validated as
50/// [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6) `partial-time`.
51/// ```
52/// use muon_rs::Time;
53/// let time = "16:35:21.363".parse::<Time>().unwrap();
54/// let hour = time.hour();
55/// let minute = time.minute();
56/// let second = time.second();
57/// let nanosecond = time.nanosecond();
58/// ```
59#[derive(Clone, Copy, Debug, PartialEq)]
60pub struct Time {
61    hour: u8,
62    minute: u8,
63    second: u8,
64    nanosecond: u32,
65}
66
67/// Fixed time offset
68///
69/// Formatted and validated as
70/// [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6) `time-offset`.
71/// ```
72/// use muon_rs::TimeOffset;
73/// let offset = "-05:00".parse::<TimeOffset>().unwrap();
74/// let seconds = offset.seconds();
75/// ```
76#[derive(Clone, Copy, Debug, PartialEq)]
77pub struct TimeOffset(_TimeOffset);
78
79/// Private time offset
80#[derive(Clone, Copy, Debug, PartialEq)]
81enum _TimeOffset {
82    Z,
83    Positive(u8, u8),
84    Negative(u8, u8),
85}
86
87/// Determine the number of days in a month
88fn days_in_month(year: u16, month: u8) -> Option<u8> {
89    match month {
90        // April, June, Septemper, November
91        4 | 6 | 9 | 11 => Some(30),
92        // January, March, May, July, August, October, December
93        1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
94        // February
95        2 => Some(if is_leap_year(year) { 29 } else { 28 }),
96        // Not a real month
97        _ => None,
98    }
99}
100
101/// Check if a year is a leap year
102fn is_leap_year(year: u16) -> bool {
103    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
104}
105
106/// Convert ASCII digit to a number
107fn digit(b: u8) -> Option<u8> {
108    if b >= b'0' && b <= b'9' {
109        Some(b - b'0')
110    } else {
111        None
112    }
113}
114
115/// Parse a 4-digit ASCII decimal number
116fn parse_4_digits(ascii: &[u8]) -> Option<u16> {
117    if ascii.len() == 4 {
118        if let (Some(b0), Some(b1), Some(b2), Some(b3)) = (
119            digit(ascii[0]),
120            digit(ascii[1]),
121            digit(ascii[2]),
122            digit(ascii[3]),
123        ) {
124            return Some(
125                u16::from(b0) * 1000
126                    + u16::from(b1) * 100
127                    + u16::from(b2) * 10
128                    + u16::from(b3),
129            );
130        }
131    }
132    None
133}
134
135/// Parse a 4-digit year
136fn parse_year(year: &[u8]) -> Option<u16> {
137    match parse_4_digits(year) {
138        Some(year) => Some(year),
139        _ => None,
140    }
141}
142
143/// Parse a 2-digit ASCII decimal number
144fn parse_2_digits(ascii: &[u8]) -> Option<u8> {
145    if ascii.len() == 2 {
146        if let (Some(b0), Some(b1)) = (digit(ascii[0]), digit(ascii[1])) {
147            return Some(b0 * 10 + b1);
148        }
149    }
150    None
151}
152
153/// Parse a 2-digit month
154fn parse_month(month: &[u8]) -> Option<u8> {
155    match parse_2_digits(month) {
156        Some(month) if month >= 1 && month <= 12 => Some(month),
157        _ => None,
158    }
159}
160
161/// Parse a 2-digit day
162fn parse_day(day: &[u8]) -> Option<u8> {
163    match parse_2_digits(day) {
164        Some(day) if day >= 1 && day <= 31 => Some(day),
165        _ => None,
166    }
167}
168
169/// Parse a 2-digit hour
170fn parse_hour(hour: &[u8]) -> Option<u8> {
171    match parse_2_digits(hour) {
172        Some(hour) if hour < 24 => Some(hour),
173        _ => None,
174    }
175}
176
177/// Parse a 2-digit minute
178fn parse_minute(minute: &[u8]) -> Option<u8> {
179    match parse_2_digits(minute) {
180        Some(minute) if minute < 60 => Some(minute),
181        _ => None,
182    }
183}
184
185/// Parse a 2-digit second
186fn parse_second(second: &[u8], leap_sec: bool) -> Option<u8> {
187    let max_seconds = if leap_sec { 60 } else { 59 };
188    match parse_2_digits(second) {
189        Some(second) if second <= max_seconds => Some(second),
190        _ => None,
191    }
192}
193
194/// Parse a nanosecond
195fn parse_nanosecond(nano: &[u8]) -> Option<u32> {
196    if nano.len() == 0 {
197        Some(0)
198    } else if nano.len() >= 2 && nano[0] == b'.' {
199        let mut ns = 0;
200        for (i, b) in nano[1..].iter().enumerate() {
201            match digit(*b) {
202                Some(b) => {
203                    if i < 9 {
204                        ns += u32::from(b) * 10_u32.pow(8 - i as u32);
205                    }
206                }
207                None => return None,
208            }
209        }
210        Some(ns)
211    } else {
212        None
213    }
214}
215
216impl fmt::Display for DateTime {
217    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
218        self.date.fmt(f)?;
219        write!(f, "T")?;
220        self.time.fmt(f)?;
221        self.time_offset.fmt(f)
222    }
223}
224
225impl FromStr for DateTime {
226    type Err = ParseError;
227
228    fn from_str(datetime: &str) -> Result<Self, Self::Err> {
229        DateTime::new(datetime.as_bytes())
230    }
231}
232
233impl ser::Serialize for DateTime {
234    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
235    where
236        S: ser::Serializer,
237    {
238        serializer.serialize_str(&self.to_string())
239    }
240}
241
242impl<'de> de::Deserialize<'de> for DateTime {
243    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
244    where
245        D: de::Deserializer<'de>,
246    {
247        struct DateTimeVisitor;
248
249        impl<'de> de::Visitor<'de> for DateTimeVisitor {
250            type Value = DateTime;
251
252            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
253                write!(formatter, "DateTime")
254            }
255
256            fn visit_str<E: de::Error>(
257                self,
258                s: &str,
259            ) -> Result<Self::Value, E> {
260                match s.parse() {
261                    Ok(datetime) => Ok(datetime),
262                    Err(_) => Err(de::Error::invalid_value(
263                        de::Unexpected::Str(&s),
264                        &self,
265                    )),
266                }
267            }
268        }
269        deserializer.deserialize_str(DateTimeVisitor)
270    }
271}
272
273impl DateTime {
274    /// Create a new datetime
275    fn new(bytes: &[u8]) -> Result<DateTime, ParseError> {
276        // FIXME: check for leap seconds
277        // date (10 bytes) + "T" + time (8+ bytes) + offset (1 or 6 bytes)
278        let len = bytes.len();
279        if len >= 20 {
280            let offset = TimeOffset::rindex(bytes);
281            if offset >= 11 && bytes[10] == b'T' {
282                let date = Date::new(&bytes[..10])?;
283                let time = Time::new(&bytes[11..offset], false)?;
284                let time_offset = TimeOffset::new(&bytes[offset..])?;
285                return Ok(DateTime {
286                    date,
287                    time,
288                    time_offset,
289                });
290            }
291        }
292        Err(ParseError::ExpectedDateTime)
293    }
294    /// Get the date
295    pub fn date(&self) -> Date {
296        self.date
297    }
298    /// Get the time
299    pub fn time(&self) -> Time {
300        self.time
301    }
302    /// Get the time offset
303    pub fn time_offset(&self) -> TimeOffset {
304        self.time_offset
305    }
306}
307
308impl fmt::Display for Date {
309    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
310        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
311    }
312}
313
314impl FromStr for Date {
315    type Err = ParseError;
316
317    fn from_str(date: &str) -> Result<Self, Self::Err> {
318        Date::new(date.as_bytes())
319    }
320}
321
322impl ser::Serialize for Date {
323    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
324    where
325        S: ser::Serializer,
326    {
327        serializer.serialize_str(&self.to_string())
328    }
329}
330
331impl<'de> de::Deserialize<'de> for Date {
332    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
333    where
334        D: de::Deserializer<'de>,
335    {
336        struct DateVisitor;
337
338        impl<'de> de::Visitor<'de> for DateVisitor {
339            type Value = Date;
340
341            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
342                write!(formatter, "Date")
343            }
344
345            fn visit_str<E: de::Error>(
346                self,
347                s: &str,
348            ) -> Result<Self::Value, E> {
349                match s.parse() {
350                    Ok(date) => Ok(date),
351                    Err(_) => Err(de::Error::invalid_value(
352                        de::Unexpected::Str(&s),
353                        &self,
354                    )),
355                }
356            }
357        }
358        deserializer.deserialize_str(DateVisitor)
359    }
360}
361
362impl Date {
363    /// Create a new date
364    fn new(bytes: &[u8]) -> Result<Self, ParseError> {
365        if bytes.len() == 10 && bytes[4] == b'-' && bytes[7] == b'-' {
366            if let Some(year) = parse_year(&bytes[..4]) {
367                if let Some(month) = parse_month(&bytes[5..7]) {
368                    if let Some(mdays) = days_in_month(year, month) {
369                        if let Some(day) = parse_day(&bytes[8..]) {
370                            if day <= mdays {
371                                return Ok(Date { year, month, day });
372                            }
373                        }
374                    }
375                }
376            }
377        }
378        Err(ParseError::ExpectedDate)
379    }
380    /// Get the year
381    pub fn year(&self) -> u16 {
382        self.year
383    }
384    /// Get the month (1-12)
385    pub fn month(&self) -> u8 {
386        self.month
387    }
388    /// Get the day of month (1-31)
389    pub fn day(&self) -> u8 {
390        self.day
391    }
392}
393
394impl fmt::Display for Time {
395    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
396        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
397        if self.nanosecond > 0 {
398            let ns = format!("{:09}", self.nanosecond);
399            write!(f, ".{}", ns.trim_end_matches('0'))
400        } else {
401            Ok(())
402        }
403    }
404}
405
406impl FromStr for Time {
407    type Err = ParseError;
408
409    fn from_str(time: &str) -> Result<Self, Self::Err> {
410        Time::new(time.as_bytes(), false)
411    }
412}
413
414impl ser::Serialize for Time {
415    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
416    where
417        S: ser::Serializer,
418    {
419        serializer.serialize_str(&self.to_string())
420    }
421}
422
423impl<'de> de::Deserialize<'de> for Time {
424    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
425    where
426        D: de::Deserializer<'de>,
427    {
428        struct TimeVisitor;
429
430        impl<'de> de::Visitor<'de> for TimeVisitor {
431            type Value = Time;
432
433            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
434                write!(formatter, "Time")
435            }
436
437            fn visit_str<E: de::Error>(
438                self,
439                s: &str,
440            ) -> Result<Self::Value, E> {
441                match s.parse::<Time>() {
442                    Ok(time) => Ok(time),
443                    Err(_) => Err(de::Error::invalid_value(
444                        de::Unexpected::Str(&s),
445                        &self,
446                    )),
447                }
448            }
449        }
450        deserializer.deserialize_str(TimeVisitor)
451    }
452}
453
454impl Time {
455    /// Create a new time
456    fn new(bytes: &[u8], leap_sec: bool) -> Result<Self, ParseError> {
457        if bytes.len() >= 8 && bytes[2] == b':' && bytes[5] == b':' {
458            if let Some(hour) = parse_hour(&bytes[..2]) {
459                if let Some(minute) = parse_minute(&bytes[3..5]) {
460                    if let Some(second) = parse_second(&bytes[6..8], leap_sec) {
461                        if let Some(nanosecond) = parse_nanosecond(&bytes[8..])
462                        {
463                            return Ok(Time {
464                                hour,
465                                minute,
466                                second,
467                                nanosecond,
468                            });
469                        }
470                    }
471                }
472            }
473        }
474        Err(ParseError::ExpectedTime)
475    }
476    /// Get the hour (0-23)
477    pub fn hour(&self) -> u8 {
478        self.hour
479    }
480    /// Get the minute (0-59)
481    pub fn minute(&self) -> u8 {
482        self.minute
483    }
484    /// Get the second (0-59, or 60 for leap second)
485    pub fn second(&self) -> u8 {
486        self.second
487    }
488    /// Get the nanosecond (0-999_999_999)
489    pub fn nanosecond(&self) -> u32 {
490        self.nanosecond
491    }
492}
493
494impl fmt::Display for TimeOffset {
495    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
496        match self.0 {
497            _TimeOffset::Z => write!(f, "Z"),
498            _TimeOffset::Positive(h, m) => write!(f, "+{:02}:{:02}", h, m),
499            _TimeOffset::Negative(h, m) => write!(f, "-{:02}:{:02}", h, m),
500        }
501    }
502}
503
504impl FromStr for TimeOffset {
505    type Err = ParseError;
506
507    fn from_str(offset: &str) -> Result<Self, Self::Err> {
508        TimeOffset::new(offset.as_bytes())
509    }
510}
511
512impl TimeOffset {
513    /// Create a new time offset
514    fn new(bytes: &[u8]) -> Result<Self, ParseError> {
515        if bytes.len() == 1 && bytes[0] == b'Z' {
516            return Ok(TimeOffset(_TimeOffset::Z));
517        } else if bytes.len() == 6
518            && bytes[3] == b':'
519            && (bytes[0] == b'+' || bytes[0] == b'-')
520        {
521            if let Some(h) = parse_hour(&bytes[1..3]) {
522                if let Some(m) = parse_minute(&bytes[4..6]) {
523                    if bytes[0] == b'+' {
524                        return Ok(TimeOffset(_TimeOffset::Positive(h, m)));
525                    } else {
526                        return Ok(TimeOffset(_TimeOffset::Negative(h, m)));
527                    }
528                }
529            }
530        }
531        Err(ParseError::ExpectedTimeOffset)
532    }
533    /// Find possible index of a TimeOffset at the end of a byte slice
534    fn rindex(bytes: &[u8]) -> usize {
535        const MAX: usize = std::usize::MAX;
536        let len = bytes.len();
537        match len {
538            1..=MAX if bytes[len - 1] == b'Z' => len - 1,
539            6..=MAX => len - 6,
540            _ => 0,
541        }
542    }
543    /// Get the time offset in seconds
544    pub fn seconds(&self) -> i32 {
545        match self.0 {
546            _TimeOffset::Z => 0,
547            _TimeOffset::Positive(h, m) => hour_minute_to_seconds(h, m),
548            _TimeOffset::Negative(h, m) => -hour_minute_to_seconds(h, m),
549        }
550    }
551}
552
553/// Calculate seconds from hour and minute
554fn hour_minute_to_seconds(hour: u8, minute: u8) -> i32 {
555    3600 * i32::from(hour) + 60 * i32::from(minute)
556}
557
558#[cfg(test)]
559mod test {
560    use super::*;
561
562    #[test]
563    fn date_time_ok() -> Result<(), Box<ParseError>> {
564        assert_eq!(
565            2011,
566            "2011-01-01T12:30:15Z".parse::<DateTime>()?.date().year()
567        );
568        assert_eq!(
569            4,
570            "2002-04-02T04:57:19.001+00:00"
571                .parse::<DateTime>()?
572                .date()
573                .month()
574        );
575        assert_eq!(
576            15,
577            "1975-03-15T19:23:00+07:00"
578                .parse::<DateTime>()?
579                .date()
580                .day()
581        );
582        assert_eq!(
583            22,
584            "2009-10-03T22:03:19-05:00"
585                .parse::<DateTime>()?
586                .time()
587                .hour()
588        );
589        assert_eq!(
590            59,
591            "2025-09-29T14:59:13.392853953+10:45"
592                .parse::<DateTime>()?
593                .time()
594                .minute()
595        );
596        assert_eq!(
597            48,
598            "2015-05-27T18:31:48.123-06:00"
599                .parse::<DateTime>()?
600                .time()
601                .second()
602        );
603        assert_eq!(
604            987_654_321,
605            "2003-08-22T01:55:11.987654321+02:30"
606                .parse::<DateTime>()?
607                .time()
608                .nanosecond()
609        );
610        assert_eq!(
611            0,
612            "2007-01-11T05:45:12+04:00"
613                .parse::<DateTime>()?
614                .time()
615                .nanosecond()
616        );
617        assert_eq!(
618            -21600,
619            "2012-06-21T19:03:00.0-06:00"
620                .parse::<DateTime>()?
621                .time_offset()
622                .seconds()
623        );
624        Ok(())
625    }
626
627    #[test]
628    fn date_time_err() -> Result<(), Box<ParseError>> {
629        assert!("".parse::<DateTime>().is_err());
630        assert!("0000".parse::<DateTime>().is_err());
631        assert!("0000-00-00T00:00:00Z".parse::<DateTime>().is_err());
632        assert!("2000-01-01t00:00:00Z".parse::<DateTime>().is_err());
633        assert!("2000-01-01TT00:00:00Z".parse::<DateTime>().is_err());
634        assert!("2000-01-01 00:00:00Z".parse::<DateTime>().is_err());
635        assert!("2000-01-01T00:00:00 Z".parse::<DateTime>().is_err());
636        assert!("2000-01-01T00:00:00=00:00".parse::<DateTime>().is_err());
637        assert!("2000-01-01T00:00:00.00 +00:00".parse::<DateTime>().is_err());
638        assert!("2000-01-01T00:00:00.00.-00:00".parse::<DateTime>().is_err());
639        Ok(())
640    }
641
642    #[test]
643    fn date_ok() -> Result<(), Box<ParseError>> {
644        assert_eq!(2011, "2011-01-01".parse::<Date>()?.year());
645        assert_eq!(2050, "2050-04-30".parse::<Date>()?.year());
646        assert_eq!(1, "1999-01-31".parse::<Date>()?.month());
647        assert_eq!(12, "2004-12-01".parse::<Date>()?.month());
648        assert_eq!(1, "1950-09-01".parse::<Date>()?.day());
649        assert_eq!(31, "2019-07-31".parse::<Date>()?.day());
650        assert_eq!(29, "2400-02-29".parse::<Date>()?.day());
651        assert_eq!(29, "2004-02-29".parse::<Date>()?.day());
652        assert_eq!(29, "2000-02-29".parse::<Date>()?.day());
653        Ok(())
654    }
655
656    #[test]
657    fn date_err() -> Result<(), Box<ParseError>> {
658        assert!("".parse::<Date>().is_err());
659        assert!("0000".parse::<Date>().is_err());
660        assert!("0000-00".parse::<Date>().is_err());
661        assert!("0000-00-".parse::<Date>().is_err());
662        assert!("0000-00-0".parse::<Date>().is_err());
663        assert!("0000-00-00".parse::<Date>().is_err());
664        assert!("1999-00-01".parse::<Date>().is_err());
665        assert!("2010-01-32".parse::<Date>().is_err());
666        assert!("2011-04-31".parse::<Date>().is_err());
667        assert!("2015-13-01".parse::<Date>().is_err());
668        assert!("2018-01-00".parse::<Date>().is_err());
669        assert!("1900-02-29".parse::<Date>().is_err());
670        assert!("2019:07-31".parse::<Date>().is_err());
671        Ok(())
672    }
673
674    #[test]
675    fn time_ok() -> Result<(), Box<ParseError>> {
676        assert_eq!(0, "00:00:00".parse::<Time>()?.hour());
677        assert_eq!(23, "23:00:00".parse::<Time>()?.hour());
678        assert_eq!(0, "12:00:34".parse::<Time>()?.minute());
679        assert_eq!(45, "12:45:34".parse::<Time>()?.minute());
680        assert_eq!(0, "12:34:00".parse::<Time>()?.second());
681        assert_eq!(15, "12:45:15".parse::<Time>()?.second());
682        Ok(())
683    }
684
685    #[test]
686    fn time_err() -> Result<(), Box<ParseError>> {
687        assert!("".parse::<Time>().is_err());
688        assert!("00".parse::<Time>().is_err());
689        assert!("00:00".parse::<Time>().is_err());
690        assert!("00:00:".parse::<Time>().is_err());
691        assert!("00:00:0".parse::<Time>().is_err());
692        assert!("00;00:00".parse::<Time>().is_err());
693        assert!("00:00:00:0".parse::<Time>().is_err());
694        assert!("24:00:00".parse::<Time>().is_err());
695        assert!("00:60:00".parse::<Time>().is_err());
696        assert!("00:00:60".parse::<Time>().is_err());
697        Ok(())
698    }
699
700    #[test]
701    fn offset_ok() -> Result<(), Box<ParseError>> {
702        assert_eq!(0, "Z".parse::<TimeOffset>()?.seconds());
703        assert_eq!(0, "-00:00".parse::<TimeOffset>()?.seconds());
704        assert_eq!(3600, "+01:00".parse::<TimeOffset>()?.seconds());
705        assert_eq!(-18000, "-05:00".parse::<TimeOffset>()?.seconds());
706        assert_eq!(-1800, "-00:30".parse::<TimeOffset>()?.seconds());
707        assert_eq!(38700, "+10:45".parse::<TimeOffset>()?.seconds());
708        assert_eq!(86340, "+23:59".parse::<TimeOffset>()?.seconds());
709        Ok(())
710    }
711
712    #[test]
713    fn offset_err() -> Result<(), Box<ParseError>> {
714        assert!("".parse::<TimeOffset>().is_err());
715        assert!("00:00".parse::<TimeOffset>().is_err());
716        assert!("0000".parse::<TimeOffset>().is_err());
717        assert!("_00;00".parse::<TimeOffset>().is_err());
718        assert!(" 00:00".parse::<TimeOffset>().is_err());
719        assert!("+0A:00".parse::<TimeOffset>().is_err());
720        assert!("+00:60".parse::<TimeOffset>().is_err());
721        assert!("+24:00".parse::<TimeOffset>().is_err());
722        Ok(())
723    }
724}