use snafu::ResultExt;
use super::formatter::Item;
use crate::errors::ParseSnafu;
use crate::{parser::Token, ParsingError};
use crate::{Epoch, HifitimeError, MonthName, TimeScale, Unit, Weekday};
use core::fmt;
use core::str::FromStr;
const MAX_TOKENS: usize = 16;
#[cfg_attr(kani, derive(kani::Arbitrary))]
#[derive(Copy, Clone, Default, PartialEq)]
pub struct Format {
pub(crate) items: [Option<Item>; MAX_TOKENS],
pub(crate) num_items: usize,
}
impl Format {
pub(crate) fn need_gregorian(&self) -> bool {
let mut i: usize = 0;
#[cfg_attr(kani, kani::loop_invariant(i <= self.num_items && i <= MAX_TOKENS))]
while i < self.num_items && i < MAX_TOKENS {
if let Some(item) = self.items[i].as_ref() {
match item.token {
Token::Year
| Token::YearShort
| Token::Month
| Token::MonthName
| Token::MonthNameShort
| Token::Day
| Token::Hour
| Token::Minute
| Token::Second
| Token::Subsecond
| Token::OffsetHours
| Token::OffsetMinutes => return true,
Token::Timescale
| Token::DayOfYearInteger
| Token::DayOfYear
| Token::Weekday
| Token::WeekdayShort
| Token::WeekdayDecimal => {
}
}
}
i += 1;
}
false
}
pub fn parse(&self, s_in: &str) -> Result<Epoch, HifitimeError> {
let mut decomposed = [0_i32; MAX_TOKENS];
let mut ts = TimeScale::UTC;
let mut offset_sign = 1;
let mut day_of_year: Option<f64> = None;
let mut weekday: Option<Weekday> = None;
let mut prev_idx = 0;
let mut cur_item_idx = 0;
let mut cur_item = match self.items[cur_item_idx] {
Some(item) => item,
None => {
return Err(HifitimeError::Parse {
source: ParsingError::NothingToParse,
details: "format string contains no tokens",
})
}
};
let mut cur_token = cur_item.token;
let mut prev_item = cur_item;
let mut prev_token;
let s = s_in.trim();
for (idx, char) in s.char_indices() {
let reached_fixed_length = cur_item.sep_char.is_none()
&& cur_item.second_sep_char.is_none()
&& !cur_item.optional
&& cur_token
.fixed_length()
.map(|l| idx >= prev_idx && idx - prev_idx + 1 == l)
.unwrap_or(false);
if idx == s.len() - 1
|| ((cur_token.is_numeric() && !char.is_numeric())
|| (!cur_token.is_numeric() && (cur_item.sep_char_is(char))))
|| reached_fixed_length
{
if idx == prev_idx
&& (prev_item.second_sep_char.is_none() || prev_item.second_sep_char_is(char))
&& !reached_fixed_length
{
prev_idx += 1;
continue;
}
if cur_token == Token::Timescale {
if idx != s.len() - 1 {
ts = TimeScale::from_str(s[idx..].trim()).with_context(|_| ParseSnafu {
details: "when parsing from format string",
})?;
}
break;
} else if char == 'Z' {
break;
}
prev_item = cur_item;
prev_token = cur_token;
let end_idx = if reached_fixed_length {
if cur_item_idx < self.num_items {
cur_item_idx += 1;
if let Some(item) = self.items[cur_item_idx] {
cur_item = item;
cur_token = cur_item.token;
}
}
idx + 1
} else if idx != s.len() - 1 || !char.is_numeric() {
if !reached_fixed_length
&& cur_item.sep_char_is_not(char)
&& (cur_item.second_sep_char.is_none()
|| (cur_item.second_sep_char_is_not(char)))
{
return Err(HifitimeError::Parse {
source: ParsingError::UnexpectedCharacter {
found: char,
option1: cur_item.sep_char,
option2: cur_item.second_sep_char,
},
details: "when parsing from format string",
});
}
if cur_item_idx == self.num_items {
break;
}
cur_item_idx += 1;
match self.items[cur_item_idx] {
Some(item) => {
cur_item = item;
cur_token = cur_item.token;
}
None => break,
}
idx
} else {
idx + 1
};
let sub_str = &s[prev_idx..end_idx];
match prev_token {
Token::YearShort => {
decomposed[0] =
sub_str.parse::<i32>().map_err(|_| HifitimeError::Parse {
source: ParsingError::ValueError,
details: "could not parse year as i32",
})? + 2000;
}
Token::DayOfYear => {
match lexical_core::parse(sub_str.as_bytes()) {
Ok(val) => day_of_year = Some(val),
Err(_) => {
return Err(HifitimeError::Parse {
source: ParsingError::ValueError,
details: "could not parse day of year as f64",
})
}
}
}
Token::Weekday | Token::WeekdayShort => {
match Weekday::from_str(sub_str) {
Ok(day) => weekday = Some(day),
Err(source) => {
return Err(HifitimeError::Parse {
source,
details: "could not parse weekday",
})
}
}
}
Token::WeekdayDecimal => {
todo!()
}
Token::MonthName | Token::MonthNameShort => {
match MonthName::from_str(sub_str) {
Ok(month) => {
decomposed[1] = ((month as u8) + 1) as i32;
}
Err(_) => {
return Err(HifitimeError::Parse {
source: ParsingError::ValueError,
details: "could not parse month name",
})
}
}
}
_ => {
match lexical_core::parse(sub_str.as_bytes()) {
Ok(val) => {
prev_token.value_ok(val)?;
match prev_token.gregorian_position() {
Some(pos) => {
if prev_token == Token::Subsecond {
if end_idx - prev_idx != 9 {
decomposed[pos] = val
* 10_i32.pow((9 - (end_idx - prev_idx)) as u32);
} else {
decomposed[pos] = val;
}
} else {
decomposed[pos] = val
}
}
None => match prev_token {
Token::DayOfYearInteger => day_of_year = Some(val as f64),
Token::Weekday => todo!(),
Token::WeekdayShort => todo!(),
Token::WeekdayDecimal => todo!(),
Token::MonthName => todo!(),
Token::MonthNameShort => todo!(),
_ => unreachable!(),
},
}
}
Err(err) => {
return Err(HifitimeError::Parse {
source: ParsingError::Lexical { err },
details: "could not parse numerical",
});
}
}
}
}
prev_idx = idx + 1;
if cur_token == Token::OffsetHours {
let sign_idx = if reached_fixed_length { idx + 1 } else { idx };
if sign_idx < s.len() && &s[sign_idx..sign_idx + 1] == "-" {
offset_sign = -1;
}
prev_idx += 1;
}
}
}
let tz = if offset_sign > 0 {
-(i64::from(decomposed[7]) * Unit::Hour + i64::from(decomposed[8]) * Unit::Minute)
} else {
i64::from(decomposed[7]) * Unit::Hour + i64::from(decomposed[8]) * Unit::Minute
};
let epoch = match day_of_year {
Some(days) => {
let elapsed = (decomposed[3] as i64) * Unit::Hour
+ (decomposed[4] as i64) * Unit::Minute
+ (decomposed[5] as i64) * Unit::Second
+ (decomposed[6] as i64) * Unit::Nanosecond;
Epoch::from_day_of_year(decomposed[0], days, ts) + elapsed
}
None => Epoch::maybe_from_gregorian(
decomposed[0],
decomposed[1].try_into().unwrap(),
decomposed[2].try_into().unwrap(),
decomposed[3].try_into().unwrap(),
decomposed[4].try_into().unwrap(),
decomposed[5].try_into().unwrap(),
decomposed[6].try_into().unwrap(),
ts,
)?,
};
if let Some(weekday) = weekday {
if weekday != epoch.weekday() {
return Err(HifitimeError::Parse {
source: ParsingError::WeekdayMismatch {
found: weekday,
expected: epoch.weekday(),
},
details: "weekday and day number do not match",
});
}
}
Ok(epoch + tz)
}
}
impl fmt::Debug for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "EpochFormat:`")?;
for maybe_item in self.items.iter().take(self.num_items) {
let item = maybe_item.as_ref().unwrap();
write!(f, "{:?}", item.token)?;
if let Some(char) = item.sep_char {
write!(f, "{char}")?;
}
if let Some(char) = item.second_sep_char {
write!(f, "{char}")?;
}
if item.optional {
write!(f, "?")?;
}
}
write!(f, "`")?;
Ok(())
}
}
impl FromStr for Format {
type Err = ParsingError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut me = Format::default();
for token in s.split('%') {
if me.num_items == MAX_TOKENS && token.chars().next().is_some() {
return Err(ParsingError::UnknownFormat);
}
match token.chars().next() {
Some(char) => match char {
'Y' => {
me.items[me.num_items] = Some(Item::new(
Token::Year,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'y' => {
me.items[me.num_items] = Some(Item::new(
Token::YearShort,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'm' => {
me.items[me.num_items] = Some(Item::new(
Token::Month,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'b' => {
me.items[me.num_items] = Some(Item::new(
Token::MonthNameShort,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'B' => {
me.items[me.num_items] = Some(Item::new(
Token::MonthName,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'd' => {
me.items[me.num_items] = Some(Item::new(
Token::Day,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'j' => {
me.items[me.num_items] = Some(Item::new(
Token::DayOfYearInteger,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'J' => {
me.items[me.num_items] = Some(Item::new(
Token::DayOfYear,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'A' => {
me.items[me.num_items] = Some(Item::new(
Token::Weekday,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'a' => {
me.items[me.num_items] = Some(Item::new(
Token::WeekdayShort,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'H' => {
me.items[me.num_items] = Some(Item::new(
Token::Hour,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'M' => {
me.items[me.num_items] = Some(Item::new(
Token::Minute,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'S' => {
me.items[me.num_items] = Some(Item::new(
Token::Second,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'f' => {
me.items[me.num_items] = Some(Item::new(
Token::Subsecond,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'T' => {
me.items[me.num_items] = Some(Item::new(
Token::Timescale,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'w' => {
me.items[me.num_items] = Some(Item::new(
Token::WeekdayDecimal,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
'z' => {
me.items[me.num_items] = Some(Item::new(
Token::OffsetHours,
token.chars().nth(1),
token.chars().nth(2),
));
me.num_items += 1;
}
_ => return Err(ParsingError::UnknownToken { token: char }),
},
None => continue, }
}
Ok(me)
}
}
#[test]
fn epoch_format_from_str() {
let fmt = Format::from_str("%Y-%m-%d").unwrap();
assert_eq!(fmt, crate::efmt::consts::ISO8601_DATE);
let fmt = Format::from_str("%Y-%m-%dT%H:%M:%S.%f %T").unwrap();
assert_eq!(fmt, crate::efmt::consts::ISO8601);
let fmt = Format::from_str("%Y-%m-%dT%H:%M:%S.%f? %T?").unwrap();
assert_eq!(fmt, crate::efmt::consts::ISO8601_FLEX);
let fmt = Format::from_str("%Y-%j").unwrap();
assert_eq!(fmt, crate::efmt::consts::ISO8601_ORDINAL);
let fmt = Format::from_str("%A, %d %B %Y %H:%M:%S").unwrap();
assert_eq!(fmt, crate::efmt::consts::RFC2822_LONG);
let fmt = Format::from_str("%a, %d %b %Y %H:%M:%S").unwrap();
assert_eq!(fmt, crate::efmt::consts::RFC2822);
}
#[cfg(feature = "std")]
#[test]
fn gh_248_regression() {
let e = Epoch::from_format_str("2023-117T12:55:26", "%Y-%jT%H:%M:%S").unwrap();
assert_eq!(format!("{e}"), "2023-04-27T12:55:26 UTC");
}
#[cfg(feature = "std")]
#[test]
fn test_strptime_no_separators() {
let fmt = Format::from_str("%Y%m%d%H%M%S").unwrap();
let e = fmt.parse("20270216143714").unwrap();
assert_eq!(format!("{e}"), "2027-02-16T14:37:14 UTC");
let fmt = Format::from_str("%Y%m%d").unwrap();
let e = fmt.parse("20230102").unwrap();
assert_eq!(format!("{e}"), "2023-01-02T00:00:00 UTC");
}