Properly handle timezone offsets; Implement conversion to/from Unix
timestamps (i64);
This commit is contained in:
parent
03ac43b821
commit
8c6e15a0c3
5 changed files with 160 additions and 451 deletions
|
@ -16,7 +16,10 @@ pub struct Parser {
|
||||||
|
|
||||||
impl Parser {
|
impl Parser {
|
||||||
pub fn new(id: &str) -> Self {
|
pub fn new(id: &str) -> Self {
|
||||||
Self { id: id.to_string(), ..Default::default() }
|
Self {
|
||||||
|
id: id.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(mut self, content: &str) -> Result<Message, super::Error> {
|
pub fn parse(mut self, content: &str) -> Result<Message, super::Error> {
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
use super::{DateTime, TimeZone};
|
|
||||||
|
|
||||||
pub const SECS_PER_DAY: i64 = 86400;
|
|
||||||
|
|
||||||
fn days_since_epoch(ts: i64) -> i64 {
|
|
||||||
ts / SECS_PER_DAY
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_leap(year: i64) -> bool {
|
|
||||||
year % 4 == 0 && (year % 100 != 0 || year % 16 == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn year_from_ts_with_remainder(ts: i64) -> (i64, i64) {
|
|
||||||
let mut year = 1970;
|
|
||||||
let mut days = days_since_epoch(ts);
|
|
||||||
loop {
|
|
||||||
let days_in_year = if is_leap(year) {
|
|
||||||
366
|
|
||||||
} else {
|
|
||||||
365
|
|
||||||
};
|
|
||||||
if days > days_in_year {
|
|
||||||
days -= days_in_year;
|
|
||||||
year += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(year, days)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn days_in_month(month: u8, year: i64) -> i64 {
|
|
||||||
assert!(month < 13);
|
|
||||||
match month {
|
|
||||||
1 | 3 | 5 | 7 | 10 | 12 => 31,
|
|
||||||
2 => {
|
|
||||||
if is_leap(year) {
|
|
||||||
29
|
|
||||||
} else {
|
|
||||||
28
|
|
||||||
}
|
|
||||||
},
|
|
||||||
4 | 6 | 9 | 11 => 30,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn month_and_days_from_ordinal(days: i64, year: i64) -> (u8, i64) {
|
|
||||||
assert!(
|
|
||||||
if is_leap(year) {
|
|
||||||
days < 366
|
|
||||||
} else {
|
|
||||||
days < 365
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let mut days = days;
|
|
||||||
let mut month = 1;
|
|
||||||
while days_in_month(month, year) <= days {
|
|
||||||
days -= days_in_month(month, year);
|
|
||||||
month += 1;
|
|
||||||
}
|
|
||||||
(month, days)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn date_time_from_ts(ts: i64) -> DateTime {
|
|
||||||
let (year, days) = year_from_ts_with_remainder(ts);
|
|
||||||
let (month, days) = month_and_days_from_ordinal(days, year);
|
|
||||||
let seconds = ts % SECS_PER_DAY;
|
|
||||||
let minutes = seconds / 60;
|
|
||||||
let second = seconds % 60;
|
|
||||||
let hour = minutes / 60;
|
|
||||||
let minute = minutes % 60;
|
|
||||||
DateTime {
|
|
||||||
year: year as u32,
|
|
||||||
month: month.try_into().unwrap(),
|
|
||||||
day: (days + 1).try_into().unwrap(),
|
|
||||||
hour: Some((hour + 1).try_into().unwrap()),
|
|
||||||
minute: Some((minute).try_into().unwrap()),
|
|
||||||
second: Some(second.try_into().unwrap()),
|
|
||||||
tz: Some(TimeZone::UTC),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_year() {
|
|
||||||
let ts = 1686459092;
|
|
||||||
let (y, r) = year_from_ts_with_remainder(ts);
|
|
||||||
assert_eq!(y, 2023);
|
|
||||||
assert_eq!(r, 161);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_month_and_days() {
|
|
||||||
let ts = 1686459092;
|
|
||||||
let (year, days) = year_from_ts_with_remainder(ts);
|
|
||||||
let (month, days) = month_and_days_from_ordinal(days, year);
|
|
||||||
assert_eq!(month, 6);
|
|
||||||
assert_eq!(days, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn get_datetime() {
|
|
||||||
let ts = 1686462068;
|
|
||||||
let dt = date_time_from_ts(ts);
|
|
||||||
assert_eq!(dt.day, 11);
|
|
||||||
assert_eq!(dt.hour, Some(6));
|
|
||||||
assert_eq!(dt.minute, Some(41));
|
|
||||||
assert_eq!(dt.second, Some(8));
|
|
||||||
}
|
|
||||||
}
|
|
181
src/time/mod.rs
181
src/time/mod.rs
|
@ -1,7 +1,6 @@
|
||||||
//! A container representing a moment in ISO-8601 format Time
|
//! A container representing a moment in ISO-8601 format Time
|
||||||
use std::{fmt, str::FromStr};
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
pub mod epoch;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod parser;
|
mod parser;
|
||||||
pub use {error::Error, parser::Parser};
|
pub use {error::Error, parser::Parser};
|
||||||
|
@ -9,6 +8,32 @@ pub use {error::Error, parser::Parser};
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
const SECS_PER_DAY: i64 = 86400;
|
||||||
|
|
||||||
|
fn days_since_epoch(ts: i64) -> i64 {
|
||||||
|
ts / SECS_PER_DAY
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_leap(year: i64) -> bool {
|
||||||
|
year % 4 == 0 && (year % 100 != 0 || year % 16 == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn days_in_month(month: u8, year: i64) -> i64 {
|
||||||
|
assert!(month < 13);
|
||||||
|
match month {
|
||||||
|
1 | 3 | 5 | 7 | 10 | 12 => 31,
|
||||||
|
2 => {
|
||||||
|
if is_leap(year) {
|
||||||
|
29
|
||||||
|
} else {
|
||||||
|
28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4 | 6 | 9 | 11 => 30,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
|
||||||
pub enum Sign {
|
pub enum Sign {
|
||||||
|
@ -113,10 +138,9 @@ impl fmt::Display for DateTime {
|
||||||
|
|
||||||
impl PartialOrd for DateTime {
|
impl PartialOrd for DateTime {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
let a = self.normalize()?;
|
let a: i64 = (*self).into();
|
||||||
let b = other.normalize()?;
|
let b: i64 = (*other).into();
|
||||||
(a.year, a.month, a.day, a.hour, a.minute, a.second)
|
a.partial_cmp(&b)
|
||||||
.partial_cmp(&(b.year, b.month, b.day, b.hour, b.minute, b.second))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,38 +152,90 @@ impl FromStr for DateTime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DateTime {
|
impl From<i64> for DateTime {
|
||||||
pub fn normalize(&self) -> Option<Self> {
|
fn from(ts: i64) -> Self {
|
||||||
if self.tz == Some(TimeZone::UTC) {
|
// Only allow using this function for positive timestamp values,
|
||||||
Some(*self)
|
// ie no dates before 1970-01-01
|
||||||
} else if let Some(TimeZone::Offset(tz)) = self.tz {
|
assert!(ts.is_positive());
|
||||||
let hour = if let Some(hour) = self.hour {
|
|
||||||
match tz.sign {
|
let mut year = 1970;
|
||||||
Sign::Positive => Some(hour + tz.hours),
|
let mut days = days_since_epoch(ts);
|
||||||
Sign::Negative => Some(hour - tz.hours),
|
loop {
|
||||||
}
|
let days_in_year = if is_leap(year) { 366 } else { 365 };
|
||||||
|
if days > days_in_year {
|
||||||
|
days -= days_in_year;
|
||||||
|
year += 1;
|
||||||
} else {
|
} else {
|
||||||
None
|
break;
|
||||||
};
|
}
|
||||||
let minute = 'blk: {
|
|
||||||
if let Some(minutes) = self.minute {
|
|
||||||
if let Some(o) = tz.minutes {
|
|
||||||
match tz.sign {
|
|
||||||
Sign::Positive => break 'blk Some(minutes + o),
|
|
||||||
Sign::Negative => break 'blk Some(minutes - o),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Some(DateTime {
|
|
||||||
hour,
|
|
||||||
minute,
|
|
||||||
..*self
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
let mut month = 1;
|
||||||
|
while days_in_month(month, year) <= days {
|
||||||
|
days -= days_in_month(month, year);
|
||||||
|
month += 1;
|
||||||
|
}
|
||||||
|
let seconds = ts % SECS_PER_DAY;
|
||||||
|
let minutes = seconds / 60;
|
||||||
|
let second = seconds % 60;
|
||||||
|
let hour = minutes / 60 + 1;
|
||||||
|
let minute = minutes % 60;
|
||||||
|
DateTime {
|
||||||
|
year: year as u32,
|
||||||
|
month: month.try_into().unwrap(),
|
||||||
|
day: (days + 1).try_into().unwrap(),
|
||||||
|
hour: Some((hour).try_into().unwrap()),
|
||||||
|
minute: Some((minute).try_into().unwrap()),
|
||||||
|
second: Some(second.try_into().unwrap()),
|
||||||
|
tz: Some(TimeZone::UTC),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DateTime> for i64 {
|
||||||
|
fn from(dt: DateTime) -> Self {
|
||||||
|
let mut seconds = dt.second.unwrap_or(0).into();
|
||||||
|
seconds += dt.minute.unwrap_or(0) as i64 * 60;
|
||||||
|
seconds += (dt.hour.unwrap_or(1) - 1) as i64 * 3600;
|
||||||
|
seconds += SECS_PER_DAY * (dt.day as i64 - 1);
|
||||||
|
let mut m = 1;
|
||||||
|
while m < dt.month {
|
||||||
|
seconds += SECS_PER_DAY * days_in_month(m, dt.year.into());
|
||||||
|
m += 1;
|
||||||
|
}
|
||||||
|
let mut y = 1970;
|
||||||
|
while y < dt.year {
|
||||||
|
if is_leap(y.into()) {
|
||||||
|
seconds += SECS_PER_DAY * 366;
|
||||||
|
} else {
|
||||||
|
seconds += SECS_PER_DAY * 365;
|
||||||
|
}
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
if let Some(TimeZone::Offset(offset)) = dt.tz {
|
||||||
|
let mut offset_seconds = offset.hours as i64 * 3600;
|
||||||
|
if let Some(offset_minutes) = offset.minutes {
|
||||||
|
offset_seconds += offset_minutes as i64 * 60;
|
||||||
|
}
|
||||||
|
match offset.sign {
|
||||||
|
Sign::Positive => seconds += offset_seconds,
|
||||||
|
Sign::Negative => seconds -= offset_seconds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DateTime {
|
||||||
|
/// Expresses this `DateTime` as an equivalent with the timezone
|
||||||
|
/// set to UTC. If the timezone is already UTC, it will be returned
|
||||||
|
/// unchanged. If there is no timezone set, it is assumed to be UTC
|
||||||
|
/// and set accordingly. If the timezone is offset from UTC, the
|
||||||
|
/// offset is applied and the result returned.
|
||||||
|
pub fn normalize(&mut self) -> &Self {
|
||||||
|
if let Some(TimeZone::Offset(_)) = self.tz {
|
||||||
|
*self = i64::from(*self).into();
|
||||||
|
}
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,4 +366,39 @@ mod test {
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn convert_timestamp() {
|
||||||
|
let ts = 1686462068;
|
||||||
|
let dt: DateTime = ts.into();
|
||||||
|
assert_eq!(dt.year, 2023);
|
||||||
|
assert_eq!(dt.month, 6);
|
||||||
|
assert_eq!(dt.day, 11);
|
||||||
|
assert_eq!(dt.hour, Some(6));
|
||||||
|
assert_eq!(dt.minute, Some(41));
|
||||||
|
assert_eq!(dt.second, Some(8));
|
||||||
|
assert_eq!(ts, dt.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compare_offset() {
|
||||||
|
let a: DateTime = "2023-06-11T02:07:16-05".parse().unwrap();
|
||||||
|
let b: DateTime = "2023-06-11T02:07:16Z".parse().unwrap();
|
||||||
|
assert!(b > a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize() {
|
||||||
|
let mut dt: DateTime = "2023-06-11T22:29:48+05".parse().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
dt.tz.unwrap(),
|
||||||
|
TimeZone::Offset(Offset {
|
||||||
|
sign: Sign::Positive,
|
||||||
|
hours: 5,
|
||||||
|
minutes: None
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(dt.normalize().tz, Some(TimeZone::UTC));
|
||||||
|
assert_eq!(dt.to_string(), "2023-06-12T03:29:48Z");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//! Implements a parser for ISO-8601 format Time
|
//! Implements a parser for ISO-8601 format Time
|
||||||
use super::{DateTime, Error, Offset, Sign, TimeZone};
|
use super::{days_in_month, DateTime, Error, Offset, Sign, TimeZone};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum Format {
|
enum Format {
|
||||||
|
@ -53,20 +53,9 @@ impl<'a> Parser<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
fn validate_day(&self, day: u8) -> Result<(), Error> {
|
fn validate_day(&self, day: u8) -> Result<(), Error> {
|
||||||
let max = match self.month {
|
let max = days_in_month(self.month.unwrap(), self.year.unwrap() as i64) as u8;
|
||||||
Some(1 | 3 | 5 | 7 | 10 | 12) => 31,
|
|
||||||
Some(2) => {
|
|
||||||
let year = self.year.unwrap();
|
|
||||||
if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
|
|
||||||
29
|
|
||||||
} else {
|
|
||||||
28
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(4 | 6 | 9 | 11) => 30,
|
|
||||||
_ => return Err(Error::InvalidMonth),
|
|
||||||
};
|
|
||||||
if day > max {
|
if day > max {
|
||||||
Err(Error::InvalidDay)
|
Err(Error::InvalidDay)
|
||||||
} else {
|
} else {
|
||||||
|
@ -267,12 +256,12 @@ impl<'a> Parser<'a> {
|
||||||
let sign = match self.buffer.chars().next() {
|
let sign = match self.buffer.chars().next() {
|
||||||
Some('+') => Sign::Positive,
|
Some('+') => Sign::Positive,
|
||||||
Some('-') => Sign::Negative,
|
Some('-') => Sign::Negative,
|
||||||
|
None => return Ok(()),
|
||||||
_ => return Err(Error::InvalidOffset),
|
_ => return Err(Error::InvalidOffset),
|
||||||
};
|
};
|
||||||
let Some(n) = self.buffer.get(1..3) else {
|
let Some(n) = self.buffer.get(1..3) else {
|
||||||
return Err(Error::InvalidOffset);
|
return Err(Error::InvalidOffset);
|
||||||
};
|
};
|
||||||
println!("Hours: {n}");
|
|
||||||
let hours: u8 = n.parse()?;
|
let hours: u8 = n.parse()?;
|
||||||
let minutes: Option<u8> = if let Some(n) = self.buffer.get(3..5) {
|
let minutes: Option<u8> = if let Some(n) = self.buffer.get(3..5) {
|
||||||
Some(n.parse()?)
|
Some(n.parse()?)
|
||||||
|
@ -510,4 +499,10 @@ mod tests {
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_no_tz() {
|
||||||
|
let dt: Result<DateTime, Error> = Parser::new("2023-06-11T22:41:31").parse();
|
||||||
|
assert!(dt.unwrap().tz.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,286 +0,0 @@
|
||||||
//! Implements a parser for ISO-8601 format Time
|
|
||||||
use super::{DateTime, Error, TimeZone};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
enum Format {
|
|
||||||
Basic,
|
|
||||||
Extended,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
|
||||||
enum Mode {
|
|
||||||
Year,
|
|
||||||
Month,
|
|
||||||
Day,
|
|
||||||
Hour,
|
|
||||||
Minute,
|
|
||||||
Second,
|
|
||||||
TimeZone,
|
|
||||||
Finish,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Parser<'a> {
|
|
||||||
text: &'a str,
|
|
||||||
format: Format,
|
|
||||||
mode: Mode,
|
|
||||||
year: Option<u32>,
|
|
||||||
month: Option<u8>,
|
|
||||||
day: Option<u8>,
|
|
||||||
hour: Option<u8>,
|
|
||||||
minute: Option<u8>,
|
|
||||||
second: Option<u8>,
|
|
||||||
tz: Option<TimeZone>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
|
||||||
pub fn new(text: &'a str) -> Self {
|
|
||||||
Self {
|
|
||||||
text,
|
|
||||||
format: Format::Extended,
|
|
||||||
mode: Mode::Year,
|
|
||||||
year: None,
|
|
||||||
month: None,
|
|
||||||
day: None,
|
|
||||||
hour: None,
|
|
||||||
minute: None,
|
|
||||||
second: None,
|
|
||||||
tz: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_year(&mut self) -> Result<(), Error> {
|
|
||||||
assert_eq!(self.mode, Mode::Year);
|
|
||||||
let year = self.text.get(..3).ok_or(Error::Truncated)?.parse()?;
|
|
||||||
self.year = Some(year);
|
|
||||||
if let Some(c) = self.text.chars().nth(5) {
|
|
||||||
match c {
|
|
||||||
'-' => self.format = Format::Extended,
|
|
||||||
_ => self.format = Format::Basic,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.mode = Mode::Month;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_month(&mut self) -> Result<(), Error> {
|
|
||||||
assert_eq!(self.mode, Mode::Month);
|
|
||||||
let month = match self.format {
|
|
||||||
Format::Basic => self.text.get(4..6),
|
|
||||||
Format::Extended => self.text.get(5..7),
|
|
||||||
};
|
|
||||||
if let Some(m) = month {
|
|
||||||
let month = match m.parse() {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(_) => {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if month > 12 {
|
|
||||||
return Err(Error::InvalidMonth);
|
|
||||||
}
|
|
||||||
self.month = Some(month);
|
|
||||||
self.mode = Mode::Day;
|
|
||||||
} else {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_day(&mut self) -> Result<(), Error> {
|
|
||||||
assert_eq!(self.mode, Mode::Day);
|
|
||||||
let day = match self.format {
|
|
||||||
Format::Basic => self.text.get(6..8),
|
|
||||||
Format::Extended => self.text.get(8..10),
|
|
||||||
};
|
|
||||||
if let Some(d) = day {
|
|
||||||
let day = match d.parse() {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let max = match self.month {
|
|
||||||
Some(1|3|5|7|10|12) => 31,
|
|
||||||
Some(2) => {
|
|
||||||
if self.year.unwrap() % 4 == 0 {
|
|
||||||
29
|
|
||||||
} else {
|
|
||||||
28
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(4|6|9|11) => 30,
|
|
||||||
_ => return Err(Error::InvalidMonth),
|
|
||||||
};
|
|
||||||
if day > max {
|
|
||||||
return Err(Error::InvalidDay);
|
|
||||||
}
|
|
||||||
self.day = Some(day);
|
|
||||||
let tidx = match self.format {
|
|
||||||
Format::Basic => 8,
|
|
||||||
Format::Extended => 10,
|
|
||||||
};
|
|
||||||
if let Some(c) = self.text.chars().nth(tidx) {
|
|
||||||
match c {
|
|
||||||
'T' => self.mode = Mode::Hour,
|
|
||||||
'Z' | '-' | '+' => self.mode = Mode::TimeZone,
|
|
||||||
_ => return Err(Error::InvalidHour),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.mode = Mode::Finish;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_hour(&mut self) -> Result<(), Error> {
|
|
||||||
assert_eq!(self.mode, Mode::Hour);
|
|
||||||
let hour = match self.format {
|
|
||||||
Format::Basic => self.text.get(9..11),
|
|
||||||
Format::Extended => self.text.get(11..13),
|
|
||||||
};
|
|
||||||
if let Some(h) = hour {
|
|
||||||
let hour = match h.parse() {
|
|
||||||
Ok(h) => h,
|
|
||||||
Err(_) => {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if hour > 24 {
|
|
||||||
return Err(Error::InvalidHour);
|
|
||||||
}
|
|
||||||
self.hour = Some(hour);
|
|
||||||
self.mode = Mode::Minute;
|
|
||||||
} else {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_minute(&mut self) -> Result<(), Error> {
|
|
||||||
assert_eq!(self.mode, Mode::Minute);
|
|
||||||
let minute = match self.format {
|
|
||||||
Format::Basic => self.text.get(11..13),
|
|
||||||
Format::Extended => self.text.get(14..16),
|
|
||||||
};
|
|
||||||
if let Some(m) = minute {
|
|
||||||
let minute = match m.parse() {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(_) => {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if minute > 60 {
|
|
||||||
return Err(Error::InvalidMinute);
|
|
||||||
}
|
|
||||||
self.minute = Some(minute);
|
|
||||||
self.mode = Mode::Second;
|
|
||||||
} else {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_second(&mut self) -> Result<(), Error> {
|
|
||||||
assert_eq!(self.mode, Mode::Second);
|
|
||||||
let second = match self.format {
|
|
||||||
Format::Basic => self.text.get(13..15),
|
|
||||||
Format::Extended => self.text.get(17..19),
|
|
||||||
};
|
|
||||||
if let Some(s) = second {
|
|
||||||
let second = match s.parse() {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => {
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if second > 60 {
|
|
||||||
return Err(Error::InvalidSecond);
|
|
||||||
}
|
|
||||||
self.second = Some(second);
|
|
||||||
}
|
|
||||||
self.mode = Mode::TimeZone;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_timezone(&mut self) -> Result<(), Error> {
|
|
||||||
assert_eq!(self.mode, Mode::TimeZone);
|
|
||||||
let idx = if self.second.is_some() {
|
|
||||||
match self.format {
|
|
||||||
Format::Basic => 17,
|
|
||||||
Format::Extended => 21,
|
|
||||||
}
|
|
||||||
} else if self.minute.is_some() {
|
|
||||||
match self.format {
|
|
||||||
Format::Basic => 15,
|
|
||||||
Format::Extended => 19,
|
|
||||||
}
|
|
||||||
} else if self.hour.is_some() {
|
|
||||||
match self.format {
|
|
||||||
Format::Basic => 13,
|
|
||||||
Format::Extended => 16,
|
|
||||||
}
|
|
||||||
} else if self.day.is_some() {
|
|
||||||
match self.format {
|
|
||||||
Format::Basic => 9,
|
|
||||||
Format::Extended => 11,
|
|
||||||
}
|
|
||||||
} else if self.month.is_some() {
|
|
||||||
match self.format {
|
|
||||||
Format::Basic => 6,
|
|
||||||
Format::Extended => 8,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
4
|
|
||||||
};
|
|
||||||
match self.text.chars().nth(idx) {
|
|
||||||
Some('Z') => {
|
|
||||||
if self.text.len() > idx + 2 {
|
|
||||||
return Err(Error::TrailingGarbage);
|
|
||||||
} else {
|
|
||||||
self.tz = Some(TimeZone::UTC);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some('-') => todo!(),
|
|
||||||
Some('+') => todo!(),
|
|
||||||
None => self.mode = Mode::Finish,
|
|
||||||
_ => return Err(Error::InvalidTimezone),
|
|
||||||
}
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(mut self) -> Result<DateTime, Error> {
|
|
||||||
loop {
|
|
||||||
match self.mode {
|
|
||||||
Mode::Year => self.parse_year()?,
|
|
||||||
Mode::Month => self.parse_month()?,
|
|
||||||
Mode::Day => self.parse_day()?,
|
|
||||||
Mode::Hour => self.parse_hour()?,
|
|
||||||
Mode::Minute => self.parse_minute()?,
|
|
||||||
Mode::Second => self.parse_second()?,
|
|
||||||
Mode::TimeZone => {
|
|
||||||
self.parse_timezone()?;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Mode::Finish => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(DateTime {
|
|
||||||
year: self.year.unwrap(),
|
|
||||||
month: self.month,
|
|
||||||
day: self.day,
|
|
||||||
hour: self.hour,
|
|
||||||
minute: self.minute,
|
|
||||||
second: self.second,
|
|
||||||
tz: self.tz,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue