use regex::Error as RegexError;
use std::error::Error;
use std::fmt::{self, Display};
mod parse_relative_time;
use chrono::{DateTime, FixedOffset, Local, LocalResult, NaiveDateTime, TimeZone};
use parse_relative_time::parse_relative_time;
#[derive(Debug, PartialEq)]
pub enum ParseDateTimeError {
InvalidRegex(RegexError),
InvalidInput,
}
impl Display for ParseDateTimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRegex(err) => {
write!(f, "Invalid regex for time pattern: {err}")
}
Self::InvalidInput => {
write!(
f,
"Invalid input string: cannot be parsed as a relative time"
)
}
}
}
}
impl Error for ParseDateTimeError {}
impl From<RegexError> for ParseDateTimeError {
fn from(err: RegexError) -> Self {
Self::InvalidRegex(err)
}
}
mod format {
pub const ISO_8601: &str = "%Y-%m-%d";
pub const ISO_8601_NO_SEP: &str = "%Y%m%d";
pub const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
pub const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
pub const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f";
pub const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S";
pub const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M";
pub const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
pub const YYYYMMDDHHMM_OFFSET: &str = "%Y%m%d%H%M %z";
pub const YYYYMMDDHHMM_UTC_OFFSET: &str = "%Y%m%d%H%MUTC%z";
pub const YYYYMMDDHHMM_ZULU_OFFSET: &str = "%Y%m%d%H%MZ%z";
pub const YYYYMMDDHHMM_HYPHENATED_OFFSET: &str = "%Y-%m-%d %H:%M %z";
pub const YYYYMMDDHHMMS_T_SEP: &str = "%Y-%m-%dT%H:%M:%S";
pub const UTC_OFFSET: &str = "UTC%#z";
pub const ZULU_OFFSET: &str = "Z%#z";
}
pub fn parse_datetime<S: AsRef<str> + Clone>(
s: S,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
parse_datetime_at_date(Local::now(), s)
}
pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
date: DateTime<Local>,
s: S,
) -> Result<DateTime<FixedOffset>, ParseDateTimeError> {
for fmt in [
format::YYYYMMDDHHMM_OFFSET,
format::YYYYMMDDHHMM_HYPHENATED_OFFSET,
format::YYYYMMDDHHMM_UTC_OFFSET,
format::YYYYMMDDHHMM_ZULU_OFFSET,
] {
if let Ok(parsed) = DateTime::parse_from_str(s.as_ref(), fmt) {
return Ok(parsed);
}
}
for fmt in [
format::YYYYMMDDHHMMS_T_SEP,
format::YYYYMMDDHHMM,
format::YYYYMMDDHHMMS,
format::YYYYMMDDHHMMSS,
format::YYYY_MM_DD_HH_MM,
format::YYYYMMDDHHMM_DOT_SS,
format::POSIX_LOCALE,
] {
if let Ok(parsed) = NaiveDateTime::parse_from_str(s.as_ref(), fmt) {
if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
return Ok(dt);
}
}
}
if s.as_ref().bytes().next() == Some(b'@') {
if let Ok(parsed) = NaiveDateTime::parse_from_str(&s.as_ref()[1..], "%s") {
if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
return Ok(dt);
}
}
}
let ts = s.as_ref().to_owned() + "0000";
for fmt in [format::ISO_8601, format::ISO_8601_NO_SEP] {
let f = fmt.to_owned() + "%H%M";
if let Ok(parsed) = NaiveDateTime::parse_from_str(&ts, &f) {
if let Ok(dt) = naive_dt_to_fixed_offset(date, parsed) {
return Ok(dt);
}
}
}
let ts = format!("{}", date.format("%Y%m%d")) + "0000" + s.as_ref();
for fmt in [format::UTC_OFFSET, format::ZULU_OFFSET] {
let f = format::YYYYMMDDHHMM.to_owned() + fmt;
if let Ok(parsed) = DateTime::parse_from_str(&ts, &f) {
return Ok(parsed);
}
}
if let Ok(relative_time) = parse_relative_time(s.as_ref()) {
let current_time = DateTime::<FixedOffset>::from(date);
if let Some(date_time) = current_time.checked_add_signed(relative_time) {
return Ok(date_time);
}
}
s.as_ref()
.parse()
.map_err(|_| (ParseDateTimeError::InvalidInput))
}
fn naive_dt_to_fixed_offset(
local: DateTime<Local>,
dt: NaiveDateTime,
) -> Result<DateTime<FixedOffset>, ()> {
match local.offset().from_local_datetime(&dt) {
LocalResult::Single(dt) => Ok(dt),
_ => Err(()),
}
}
#[cfg(test)]
mod tests {
static TEST_TIME: i64 = 1613371067;
#[cfg(test)]
mod iso_8601 {
use std::env;
use crate::ParseDateTimeError;
use crate::{parse_datetime, tests::TEST_TIME};
#[test]
fn test_t_sep() {
env::set_var("TZ", "UTC");
let dt = "2021-02-15T06:37:47";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn test_space_sep() {
env::set_var("TZ", "UTC");
let dt = "2021-02-15 06:37:47";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn test_space_sep_offset() {
env::set_var("TZ", "UTC");
let dt = "2021-02-14 22:37:47 -0800";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn test_t_sep_offset() {
env::set_var("TZ", "UTC");
let dt = "2021-02-14T22:37:47 -0800";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
#[test]
fn invalid_formats() {
let invalid_dts = vec!["NotADate", "202104", "202104-12T22:37:47"];
for dt in invalid_dts {
assert_eq!(parse_datetime(dt), Err(ParseDateTimeError::InvalidInput));
}
}
#[test]
fn test_epoch_seconds() {
env::set_var("TZ", "UTC");
let dt = "@1613371067";
let actual = parse_datetime(dt);
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
}
}
#[cfg(test)]
mod offsets {
use chrono::Local;
use crate::parse_datetime;
use crate::ParseDateTimeError;
#[test]
fn test_positive_offsets() {
let offsets = vec![
"UTC+07:00",
"UTC+0700",
"UTC+07",
"Z+07:00",
"Z+0700",
"Z+07",
];
let expected = format!("{}{}", Local::now().format("%Y%m%d"), "0000+0700");
for offset in offsets {
let actual = parse_datetime(offset).unwrap();
assert_eq!(expected, format!("{}", actual.format("%Y%m%d%H%M%z")));
}
}
#[test]
fn test_partial_offset() {
let offsets = vec!["UTC+00:15", "UTC+0015", "Z+00:15", "Z+0015"];
let expected = format!("{}{}", Local::now().format("%Y%m%d"), "0000+0015");
for offset in offsets {
let actual = parse_datetime(offset).unwrap();
assert_eq!(expected, format!("{}", actual.format("%Y%m%d%H%M%z")));
}
}
#[test]
fn invalid_offset_format() {
let invalid_offsets = vec!["+0700", "UTC+2", "Z-1", "UTC+01005"];
for offset in invalid_offsets {
assert_eq!(
parse_datetime(offset),
Err(ParseDateTimeError::InvalidInput)
);
}
}
}
#[cfg(test)]
mod relative_time {
use crate::parse_datetime;
#[test]
fn test_positive_offsets() {
let relative_times = vec![
"today",
"yesterday",
"1 minute",
"3 hours",
"1 year 3 months",
];
for relative_time in relative_times {
assert_eq!(parse_datetime(relative_time).is_ok(), true);
}
}
}
#[cfg(test)]
mod timestamp {
use crate::parse_datetime;
use chrono::{TimeZone, Utc};
#[test]
fn test_positive_offsets() {
let offsets: Vec<i64> = vec![
0, 1, 2, 10, 100, 150, 2000, 1234400000, 1334400000, 1692582913, 2092582910,
];
for offset in offsets {
let time = Utc.timestamp_opt(offset, 0).unwrap();
let dt = parse_datetime(format!("@{}", offset));
assert_eq!(dt.unwrap(), time);
}
}
}
mod readme_test {
use crate::parse_datetime;
use chrono::{Local, TimeZone};
#[test]
fn test_readme_code() {
let dt = parse_datetime("2021-02-14 06:37:47");
assert_eq!(
dt.unwrap(),
Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap()
);
}
}
mod invalid_test {
use crate::parse_datetime;
use crate::ParseDateTimeError;
#[test]
fn test_invalid_input() {
let result = parse_datetime("foobar");
println!("{result:?}");
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
let result = parse_datetime("invalid 1");
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
}
}
}