| use std::fmt; |
| use std::time::Duration; |
| |
| // Number of seconds in a day is a constant. |
| // We do not support leap seconds here. |
| const SECONDS_IN_DAY: u64 = 86400; |
| |
| // Gregorian calendar has 400 years cycles, this is a procedure |
| // for computing if a year is a leap year. |
| fn is_leap_year(year: i64) -> bool { |
| if year % 4 != 0 { |
| false |
| } else if year % 100 != 0 { |
| true |
| } else if year % 400 != 0 { |
| false |
| } else { |
| true |
| } |
| } |
| |
| fn days_in_year(year: i64) -> u32 { |
| if is_leap_year(year) { |
| 366 |
| } else { |
| 365 |
| } |
| } |
| |
| // Number of leap years among 400 consecutive years. |
| const CYCLE_LEAP_YEARS: u32 = 400 / 4 - 400 / 100 + 400 / 400; |
| // Number of days in 400 years cycle. |
| const CYCLE_DAYS: u32 = 400 * 365 + CYCLE_LEAP_YEARS; |
| // Number of seconds in 400 years cycle. |
| const CYCLE_SECONDS: u64 = CYCLE_DAYS as u64 * SECONDS_IN_DAY; |
| |
| // Number of seconds between 1 Jan 1970 and 1 Jan 2000. |
| // Check with: |
| // `TZ=UTC gdate --rfc-3339=seconds --date @946684800` |
| const YEARS_1970_2000_SECONDS: u64 = 946684800; |
| // Number of seconds between 1 Jan 1600 and 1 Jan 1970. |
| const YEARS_1600_1970_SECONDS: u64 = CYCLE_SECONDS - YEARS_1970_2000_SECONDS; |
| |
| // For each year in the cycle, number of leap years before in the cycle. |
| #[cfg_attr(rustfmt, rustfmt_skip)] |
| static YEAR_DELTAS: [u8; 401] = [ |
| 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, |
| 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, |
| 10, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, |
| 15, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, |
| 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, // 100 |
| 25, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, |
| 29, 30, 30, 30, 30, 31, 31, 31, 31, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, |
| 34, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 39, 39, |
| 39, 40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 43, 43, 44, 44, 44, |
| 44, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 48, 48, 48, 48, 49, 49, 49, // 200 |
| 49, 49, 49, 49, 49, 50, 50, 50, 50, 51, 51, 51, 51, 52, 52, 52, 52, 53, 53, 53, |
| 53, 54, 54, 54, 54, 55, 55, 55, 55, 56, 56, 56, 56, 57, 57, 57, 57, 58, 58, 58, |
| 58, 59, 59, 59, 59, 60, 60, 60, 60, 61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, |
| 63, 64, 64, 64, 64, 65, 65, 65, 65, 66, 66, 66, 66, 67, 67, 67, 67, 68, 68, 68, |
| 68, 69, 69, 69, 69, 70, 70, 70, 70, 71, 71, 71, 71, 72, 72, 72, 72, 73, 73, 73, // 300 |
| 73, 73, 73, 73, 73, 74, 74, 74, 74, 75, 75, 75, 75, 76, 76, 76, 76, 77, 77, 77, |
| 77, 78, 78, 78, 78, 79, 79, 79, 79, 80, 80, 80, 80, 81, 81, 81, 81, 82, 82, 82, |
| 82, 83, 83, 83, 83, 84, 84, 84, 84, 85, 85, 85, 85, 86, 86, 86, 86, 87, 87, 87, |
| 87, 88, 88, 88, 88, 89, 89, 89, 89, 90, 90, 90, 90, 91, 91, 91, 91, 92, 92, 92, |
| 92, 93, 93, 93, 93, 94, 94, 94, 94, 95, 95, 95, 95, 96, 96, 96, 96, 97, 97, 97, 97, |
| ]; |
| |
| /// UTC time |
| pub struct TmUtc { |
| /// Year |
| year: i64, |
| /// 1..=12 |
| month: u32, |
| /// 1-based day of month |
| day: u32, |
| /// 0..=23 |
| hour: u32, |
| /// 0..=59 |
| minute: u32, |
| /// 0..=59; no leap seconds |
| second: u32, |
| /// 0..=999_999_999 |
| nanos: u32, |
| } |
| |
| #[derive(Debug, thiserror::Error)] |
| pub enum Rfc3339ParseError { |
| #[error("Unexpected EOF")] |
| UnexpectedEof, |
| #[error("Trailing characters")] |
| TrailngCharacters, |
| #[error("Expecting digits")] |
| ExpectingDigits, |
| #[error("Expecting character: {:?}", .0)] |
| ExpectingChar(char), |
| #[error("Expecting timezone")] |
| ExpectingTimezone, |
| #[error("No digits after dot")] |
| NoDigitsAfterDot, |
| #[error("Date-time field is out of range")] |
| DateTimeFieldOutOfRange, |
| #[error("Expecting date-time separator")] |
| ExpectingDateTimeSeparator, |
| } |
| |
| pub type Rfc3339ParseResult<A> = Result<A, Rfc3339ParseError>; |
| |
| impl TmUtc { |
| fn day_of_cycle_to_year_day_of_year(day_of_cycle: u32) -> (i64, u32) { |
| debug_assert!(day_of_cycle < CYCLE_DAYS); |
| |
| let mut year_mod_400 = (day_of_cycle / 365) as i64; |
| let mut day_or_year = (day_of_cycle % 365) as u32; |
| |
| let delta = YEAR_DELTAS[year_mod_400 as usize] as u32; |
| if day_or_year < delta { |
| year_mod_400 -= 1; |
| day_or_year += 365 - YEAR_DELTAS[year_mod_400 as usize] as u32; |
| } else { |
| day_or_year -= delta; |
| } |
| |
| (year_mod_400, day_or_year) |
| } |
| |
| fn year_day_of_year_to_day_of_cycle(year_mod_400: u32, day_of_year: u32) -> u32 { |
| debug_assert!(year_mod_400 < 400); |
| debug_assert!(day_of_year < days_in_year(year_mod_400 as i64)); |
| |
| year_mod_400 * 365 + YEAR_DELTAS[year_mod_400 as usize] as u32 + day_of_year |
| } |
| |
| // Convert seconds of the day of hour, minute and second |
| fn second_of_day_to_h_m_s(seconds: u32) -> (u32, u32, u32) { |
| debug_assert!(seconds < 86400); |
| |
| let hour = seconds / 3600; |
| let minute = seconds % 3600 / 60; |
| let second = seconds % 60; |
| |
| (hour, minute, second) |
| } |
| |
| fn h_m_s_to_second_of_day(hour: u32, minute: u32, second: u32) -> u32 { |
| debug_assert!(hour < 24); |
| debug_assert!(minute < 60); |
| debug_assert!(second < 60); |
| |
| hour * 3600 + minute * 60 + second |
| } |
| |
| fn days_in_months(year: i64) -> &'static [u32] { |
| if is_leap_year(year) { |
| &[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] |
| } else { |
| &[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] |
| } |
| } |
| |
| // Convert day of year (0-based) to month and day |
| fn day_of_year_to_month_day(year: i64, day_of_year: u32) -> (u32, u32) { |
| debug_assert!(day_of_year < days_in_year(year)); |
| |
| let days_in_months = TmUtc::days_in_months(year); |
| |
| let mut rem_days = day_of_year; |
| let mut month = 1; |
| while rem_days >= days_in_months[month - 1] { |
| rem_days -= days_in_months[month - 1]; |
| month += 1; |
| } |
| |
| debug_assert!(rem_days + 1 <= days_in_months[month - 1]); |
| |
| (month as u32, rem_days + 1) |
| } |
| |
| fn month_day_to_day_of_year(year: i64, month: u32, day: u32) -> u32 { |
| debug_assert!(month >= 1); |
| debug_assert!(month <= 12); |
| |
| debug_assert!(day >= 1); |
| |
| let days_in_months = TmUtc::days_in_months(year); |
| |
| // TODO: replace loop with precomputed table |
| let mut day_of_year = 0; |
| for next_month in 1..month { |
| day_of_year += days_in_months[next_month as usize - 1]; |
| } |
| |
| debug_assert!(day <= days_in_months[month as usize - 1]); |
| |
| day_of_year + day - 1 |
| } |
| |
| // Construct from duration added to cycle start year |
| fn from_cycle_start_add_duration(mut cycle_start: i64, add: Duration) -> TmUtc { |
| debug_assert!(cycle_start % 400 == 0); |
| |
| // Split duration to days and duration within day |
| |
| let days = add.as_secs() / SECONDS_IN_DAY; |
| let duration_of_day = add - Duration::from_secs(days * SECONDS_IN_DAY); |
| |
| let cycles = days / CYCLE_DAYS as u64; |
| cycle_start += cycles as i64 * 400; |
| let day_of_cycle = days % CYCLE_DAYS as u64; |
| |
| let (year_mod_400, day_of_year) = |
| TmUtc::day_of_cycle_to_year_day_of_year(day_of_cycle as u32); |
| |
| let (year,) = (cycle_start + year_mod_400,); |
| let (month, day) = TmUtc::day_of_year_to_month_day(year, day_of_year); |
| let (hour, minute, second) = |
| TmUtc::second_of_day_to_h_m_s(duration_of_day.as_secs() as u32); |
| |
| TmUtc { |
| year, |
| month, |
| day, |
| hour, |
| minute, |
| second, |
| nanos: duration_of_day.subsec_nanos(), |
| } |
| } |
| |
| // Protobuf timestamp: seconds from epoch, and nanos 0..=999_999_999 counting forward. |
| pub fn from_protobuf_timestamp(seconds: i64, nanos: u32) -> TmUtc { |
| assert!(nanos <= 999_999_999); |
| |
| let (mut year, mut seconds) = if seconds >= 0 { |
| (1970, seconds as u64) |
| } else { |
| let minus_seconds = if seconds == i64::MIN { |
| i64::MIN as u64 |
| } else { |
| -seconds as u64 |
| }; |
| |
| let cycles = (minus_seconds + CYCLE_SECONDS) / CYCLE_SECONDS; |
| |
| ( |
| 1970 - 400 * cycles as i64, |
| cycles * CYCLE_SECONDS - minus_seconds, |
| ) |
| }; |
| |
| year -= 370; |
| seconds += YEARS_1600_1970_SECONDS; |
| |
| TmUtc::from_cycle_start_add_duration(year, Duration::new(seconds, nanos)) |
| } |
| |
| pub fn to_protobuf_timestamp(&self) -> (i64, u32) { |
| assert!(self.year >= 0); |
| assert!(self.year <= 9999); |
| |
| let year_mod_400 = ((self.year % 400 + 400) % 400) as u32; |
| let cycle_start = self.year - year_mod_400 as i64; |
| |
| let day_of_year = TmUtc::month_day_to_day_of_year(self.year, self.month, self.day); |
| let day_of_cycle = TmUtc::year_day_of_year_to_day_of_cycle(year_mod_400, day_of_year); |
| let second_of_day = TmUtc::h_m_s_to_second_of_day(self.hour, self.minute, self.second); |
| |
| let second_of_cycle = day_of_cycle as u64 * SECONDS_IN_DAY + second_of_day as u64; |
| |
| let epoch_seconds = (cycle_start - 1600) / 400 * CYCLE_SECONDS as i64 |
| - YEARS_1600_1970_SECONDS as i64 |
| + second_of_cycle as i64; |
| |
| (epoch_seconds, self.nanos) |
| } |
| |
| pub fn parse_rfc_3339(s: &str) -> Rfc3339ParseResult<(i64, u32)> { |
| struct Parser<'a> { |
| s: &'a [u8], |
| pos: usize, |
| } |
| |
| impl<'a> Parser<'a> { |
| fn next_number(&mut self, len: usize) -> Rfc3339ParseResult<u32> { |
| let end_pos = self.pos + len; |
| if end_pos > self.s.len() { |
| return Err(Rfc3339ParseError::UnexpectedEof); |
| } |
| let mut r = 0; |
| for i in 0..len { |
| let c = self.s[self.pos + i]; |
| if c >= b'0' && c <= b'9' { |
| r = r * 10 + (c - b'0') as u32; |
| } else { |
| return Err(Rfc3339ParseError::ExpectingDigits); |
| } |
| } |
| self.pos += len; |
| Ok(r) |
| } |
| |
| fn lookahead_char(&self) -> Rfc3339ParseResult<u8> { |
| if self.pos == self.s.len() { |
| return Err(Rfc3339ParseError::UnexpectedEof); |
| } |
| Ok(self.s[self.pos]) |
| } |
| |
| fn next_char(&mut self, expect: u8) -> Rfc3339ParseResult<()> { |
| assert!(expect < 0x80); |
| let c = self.lookahead_char()?; |
| if c != expect { |
| return Err(Rfc3339ParseError::ExpectingChar(expect as char)); |
| } |
| self.pos += 1; |
| Ok(()) |
| } |
| } |
| |
| let mut parser = Parser { |
| s: s.as_bytes(), |
| pos: 0, |
| }; |
| |
| let year = parser.next_number(4)? as i64; |
| parser.next_char(b'-')?; |
| let month = parser.next_number(2)?; |
| parser.next_char(b'-')?; |
| let day = parser.next_number(2)?; |
| |
| if month < 1 || month > 12 { |
| return Err(Rfc3339ParseError::DateTimeFieldOutOfRange); |
| } |
| |
| if day < 1 || day > TmUtc::days_in_months(year as i64)[month as usize - 1] { |
| return Err(Rfc3339ParseError::DateTimeFieldOutOfRange); |
| } |
| |
| match parser.lookahead_char()? { |
| b'T' | b't' | b' ' => parser.pos += 1, |
| _ => return Err(Rfc3339ParseError::ExpectingDateTimeSeparator), |
| } |
| |
| let hour = parser.next_number(2)?; |
| parser.next_char(b':')?; |
| let minute = parser.next_number(2)?; |
| parser.next_char(b':')?; |
| let second = parser.next_number(2)?; |
| |
| if hour > 23 || minute > 59 || second > 60 { |
| return Err(Rfc3339ParseError::DateTimeFieldOutOfRange); |
| } |
| |
| // round down leap second |
| let second = if second == 60 { 59 } else { second }; |
| |
| let nanos = if parser.lookahead_char()? == b'.' { |
| parser.pos += 1; |
| let mut digits = 0; |
| let mut nanos = 0; |
| while parser.lookahead_char()? >= b'0' && parser.lookahead_char()? <= b'9' { |
| let digit = (parser.lookahead_char()? - b'0') as u32; |
| parser.pos += 1; |
| if digits == 9 { |
| continue; |
| } |
| digits += 1; |
| nanos = nanos * 10 + digit; |
| } |
| |
| if digits == 0 { |
| return Err(Rfc3339ParseError::NoDigitsAfterDot); |
| } |
| |
| for _ in digits..9 { |
| nanos *= 10; |
| } |
| nanos |
| } else { |
| 0 |
| }; |
| |
| let offset_seconds = if parser.lookahead_char()? == b'Z' || parser.lookahead_char()? == b'z' |
| { |
| parser.pos += 1; |
| 0 |
| } else { |
| let sign = if parser.lookahead_char()? == b'+' { |
| 1 |
| } else if parser.lookahead_char()? == b'-' { |
| -1 |
| } else { |
| return Err(Rfc3339ParseError::ExpectingTimezone); |
| }; |
| |
| parser.pos += 1; |
| |
| let hour_offset = parser.next_number(2)?; |
| parser.next_char(b':')?; |
| let minute_offset = parser.next_number(2)?; |
| |
| (hour_offset * 3600 + 60 * minute_offset) as i64 * sign |
| }; |
| |
| if parser.pos != parser.s.len() { |
| return Err(Rfc3339ParseError::TrailngCharacters); |
| } |
| |
| let (seconds, nanos) = TmUtc { |
| year, |
| month, |
| day, |
| hour, |
| minute, |
| second, |
| nanos, |
| } |
| .to_protobuf_timestamp(); |
| |
| Ok((seconds - offset_seconds, nanos)) |
| } |
| } |
| |
| impl fmt::Display for TmUtc { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| if self.year > 9999 { |
| write!(f, "+{}", self.year)?; |
| } else if self.year < 0 { |
| write!(f, "{:05}", self.year)?; |
| } else { |
| write!(f, "{:04}", self.year)?; |
| } |
| write!( |
| f, |
| "-{:02}-{:02}T{:02}:{:02}:{:02}", |
| self.month, self.day, self.hour, self.minute, self.second |
| )?; |
| |
| // if precision is not specified, print nanoseconds |
| let subsec_digits = f.precision().unwrap_or(9); |
| if subsec_digits != 0 { |
| let mut subsec_digits = subsec_digits; |
| |
| let width = if subsec_digits > 9 { 9 } else { subsec_digits }; |
| |
| // "Truncated" nanonseconds. |
| let mut subsec = self.nanos; |
| |
| // Performs 8 iterations when precision=1, |
| // but that's probably not a issue compared to other computations. |
| for _ in width..9 { |
| subsec /= 10; |
| } |
| |
| write!(f, ".{:0width$}", subsec, width = width as usize)?; |
| |
| // Adding more than 9 digits is meaningless, |
| // but if user requests it, we should print zeros. |
| for _ in 9..subsec_digits { |
| write!(f, "0")?; |
| subsec_digits -= 1; |
| } |
| } |
| |
| write!(f, "Z") |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| #[test] |
| fn test_fmt() { |
| fn test_impl(expected: &str, secs: i64, nanos: u32, subsec_digits: u32) { |
| let tm_utc = TmUtc::from_protobuf_timestamp(secs, nanos); |
| |
| assert_eq!( |
| expected, |
| format!("{:.prec$}", tm_utc, prec = subsec_digits as usize) |
| ); |
| } |
| |
| // Tests can be validated with with GNU date: |
| // `TZ=UTC gdate --date @1535585179 --iso-8601=seconds` |
| |
| test_impl("1970-01-01T00:00:00Z", 0, 0, 0); |
| test_impl("2018-08-29T23:26:19Z", 1535585179, 0, 0); |
| test_impl("2018-08-29T23:26:19.123Z", 1535585179, 123456789, 3); |
| test_impl("1646-04-01T03:45:44Z", -10216613656, 0, 0); |
| test_impl("1970-01-01T00:00:00.000000001000Z", 0, 1, 12); |
| test_impl("5138-11-16T09:46:40Z", 100000000000, 0, 0); |
| test_impl("+33658-09-27T01:46:41Z", 1000000000001, 0, 0); |
| // Leading zero |
| test_impl("0000-12-31T00:00:00Z", -62135683200, 0, 0); |
| // Minus zero |
| test_impl("-0003-10-30T14:13:20Z", -62235683200, 0, 0); |
| // More than 4 digits |
| // Largest value GNU date can handle |
| test_impl("+2147485547-12-31T23:59:59Z", 67768036191676799, 0, 0); |
| // Negative dates |
| test_impl("1969-12-31T23:59:59Z", -1, 0, 0); |
| test_impl("1969-12-31T23:59:00Z", -60, 0, 0); |
| test_impl("1969-12-31T23:59:58.900Z", -2, 900_000_000, 3); |
| test_impl("1966-10-31T14:13:20Z", -100000000, 0, 0); |
| test_impl("-29719-04-05T22:13:19Z", -1000000000001, 0, 0); |
| // Smallest value GNU date can handle |
| test_impl("-2147481748-01-01T00:00:00Z", -67768040609740800, 0, 0); |
| } |
| |
| #[test] |
| fn test_parse_fmt() { |
| fn test_impl(s: &str, width: usize) { |
| let (seconds, nanos) = TmUtc::parse_rfc_3339(s).unwrap(); |
| let formatted = format!( |
| "{:.width$}", |
| TmUtc::from_protobuf_timestamp(seconds, nanos), |
| width = width |
| ); |
| assert_eq!(formatted, s); |
| } |
| |
| test_impl("1970-01-01T00:00:00Z", 0); |
| test_impl("1970-01-01T00:00:00.000Z", 3); |
| test_impl("1970-01-01T00:00:00.000000000Z", 9); |
| test_impl("1970-01-02T00:00:00Z", 0); |
| test_impl("1970-03-01T00:00:00Z", 0); |
| test_impl("1974-01-01T00:00:00Z", 0); |
| test_impl("2018-01-01T00:00:00Z", 0); |
| test_impl("2018-09-02T05:49:10.123456789Z", 9); |
| test_impl("0001-01-01T00:00:00.000000000Z", 9); |
| test_impl("9999-12-31T23:59:59.999999999Z", 9); |
| } |
| |
| #[test] |
| fn test_parse_alt() { |
| fn test_impl(alt: &str, parse: &str) { |
| let reference = TmUtc::parse_rfc_3339(alt).unwrap(); |
| let parsed = TmUtc::parse_rfc_3339(parse).unwrap(); |
| assert_eq!(reference, parsed, "{} - {}", alt, parse); |
| } |
| |
| // alternative spelling |
| test_impl("1970-01-01 00:00:00Z", "1970-01-01T00:00:00Z"); |
| test_impl("1970-01-01 00:00:00Z", "1970-01-01t00:00:00Z"); |
| test_impl("1970-01-01 00:00:00Z", "1970-01-01 00:00:00z"); |
| // leap second is rounded down |
| test_impl("2016-12-31 23:59:59Z", "2016-12-31 23:59:60Z"); |
| // TZ offset |
| test_impl("1970-01-01 00:00:00Z", "1970-01-01T03:00:00+03:00"); |
| test_impl("1970-01-01 00:00:00Z", "1969-12-31 22:15:00-01:45"); |
| } |
| |
| #[test] |
| fn test_parse_incorrect_inputs() { |
| fn test_impl(s: &str) { |
| assert!(TmUtc::parse_rfc_3339(s).is_err(), "{}", s); |
| } |
| |
| test_impl("1970-01-01T00:00:61Z"); |
| test_impl("1970-01-01T00:60:61Z"); |
| test_impl("1970-01-01T24:00:61Z"); |
| test_impl("1970-01-01T00:00:00.Z"); |
| test_impl("1970-01-32T00:00:00Z"); |
| test_impl("1970-02-29T00:00:00Z"); |
| test_impl("1980-02-30T00:00:00Z"); |
| test_impl("1980-13-01T00:00:00Z"); |
| test_impl("1970-01-01T00:00:00"); |
| test_impl("1970-01-01T00:00Z"); |
| } |
| |
| #[test] |
| fn test_fmt_max_duration() { |
| // Simply check that there are no integer overflows. |
| // I didn't check that resulting strings are correct. |
| assert_eq!( |
| "-292277022657-01-27T08:29:52.000000000Z", |
| format!("{}", TmUtc::from_protobuf_timestamp(i64::MIN, 0)) |
| ); |
| assert_eq!( |
| "+292277026596-12-04T15:30:07.999999999Z", |
| format!("{}", TmUtc::from_protobuf_timestamp(i64::MAX, 999_999_999)) |
| ); |
| } |
| } |