1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
use chrono::prelude::*; #[derive(PartialEq, Debug)] pub enum DateTimeFormat { Missing, EpochMillis, Rfc3339, } impl DateTimeFormat { fn parse(&self, input: &str) -> Option<ParsedInput> { match self { DateTimeFormat::EpochMillis => parse_from_epoch_millis(input), DateTimeFormat::Rfc3339 => parse_from_rfc3339(input), DateTimeFormat::Missing => None, } } } #[derive(PartialEq, Debug)] pub struct ParsedInput { pub input_format: DateTimeFormat, pub input_zone: Option<FixedOffset>, pub value: DateTime<Utc>, } fn parse_from_epoch_millis(input: &str) -> Option<ParsedInput> { input .parse::<i64>() .ok() .and_then(|e| Utc.timestamp_millis_opt(e).single()) .map(|d| ParsedInput { input_format: DateTimeFormat::EpochMillis, input_zone: None, value: d, }) } fn parse_from_rfc3339(input: &str) -> Option<ParsedInput> { DateTime::parse_from_rfc3339(&input.replace(" ", "T")) .ok() .map(|d| ParsedInput { input_format: DateTimeFormat::Rfc3339, input_zone: Some(d.timezone()), value: d.with_timezone(&Utc), }) } pub fn parse_input(input: &Option<String>) -> Result<ParsedInput, &'static str> { input .as_ref() .map(|i| { DateTimeFormat::EpochMillis .parse(&i) .or_else(|| DateTimeFormat::Rfc3339.parse(&i)) .ok_or("Input format not recognized") }) .unwrap_or_else(|| { Ok(ParsedInput { input_format: DateTimeFormat::Missing, input_zone: None, value: Utc::now(), }) }) } #[cfg(test)] mod tests { use super::*; #[test] fn missing_input() { let now = Utc::now(); let result = parse_input(None).unwrap(); assert_eq!(result.input_format, crate::parsing::DateTimeFormat::Missing); assert_eq!(result.input_zone, None); assert!( result.value.timestamp_millis() >= now.timestamp_millis(), "Provided time {} was not after the start of the test {}", result.value, now ); assert!( result.value.timestamp_millis() < now.timestamp_millis() + 1000, "Provided time {} was more than one second after the start of the test {}", result.value, now ); } #[test] fn epoch_millis_input() { let result = parse_input(Some(String::from("1572213799747"))).unwrap(); assert_eq!(result.input_format, DateTimeFormat::EpochMillis); assert_eq!(result.input_zone, None); assert_eq!(result.value, Utc.timestamp_millis(1572213799747)); } #[test] fn rfc3339_input() { let result = parse_input(Some(String::from("2019-10-27T15:03:19.747-07:00"))).unwrap(); assert_eq!(result.input_format, DateTimeFormat::Rfc3339); assert_eq!(result.input_zone, Some(FixedOffset::west(25200))); assert_eq!(result.value, Utc.timestamp_millis(1572213799747)); } #[test] fn rfc3339_input_no_partial_seconds() { let result = parse_input(Some(String::from("2019-10-27T15:03:19-07:00"))).unwrap(); assert_eq!(result.input_format, DateTimeFormat::Rfc3339); assert_eq!(result.input_zone, Some(FixedOffset::west(25200))); assert_eq!(result.value, Utc.timestamp_millis(1572213799000)); } #[test] fn rfc3339_input_zulu() { let result = parse_input(Some(String::from("2019-10-27T22:03:19.747Z"))).unwrap(); assert_eq!(result.input_format, DateTimeFormat::Rfc3339); assert_eq!(result.input_zone, Some(FixedOffset::west(0))); assert_eq!(result.value, Utc.timestamp_millis(1572213799747)); } #[test] fn rfc3339_input_space_instead_of_t() { let result = parse_input(Some(String::from("2019-10-27 15:03:19.747-07:00"))).unwrap(); assert_eq!(result.input_format, DateTimeFormat::Rfc3339); assert_eq!(result.input_zone, Some(FixedOffset::west(25200))); assert_eq!(result.value, Utc.timestamp_millis(1572213799747)); } #[test] fn invalid_input() { let result = parse_input(Some(String::from("not a date"))).err(); assert_eq!(result, Some("Input format not recognized")); } }