#![allow(deprecated)]
use crate::timezone;
use anyhow::{Result, anyhow};
use chrono::format::{Item, ParseResult, Parsed, parse as parse_items};
use chrono::prelude::*;
use regex::Regex;
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
RE.get_or_init(|| {
regex::RegexBuilder::new($re)
.unicode(false)
.build()
.expect("invalid regex literal")
})
}};
}
macro_rules! fmt_items {
($fmt:literal $(,)?) => {{
static ITEMS: std::sync::OnceLock<Vec<chrono::format::Item<'static>>> =
std::sync::OnceLock::new();
ITEMS
.get_or_init(|| {
chrono::format::StrftimeItems::new($fmt)
.parse()
.expect("invalid strftime literal")
})
.as_slice()
}};
}
const fn build_date_byte_table() -> [bool; 256] {
let mut table = [false; 256];
let mut i = 0usize;
while i < 256 {
let b = i as u8;
table[i] = b.is_ascii_alphanumeric()
|| matches!(b, b' ' | 0x09..=0x0D)
|| matches!(b, b'-' | b'+' | b'/' | b':' | b'.' | b',');
i += 1;
}
table
}
static DATE_BYTE: [bool; 256] = build_date_byte_table();
const fn build_ws_table() -> [bool; 256] {
let mut table = [false; 256];
let mut i = 0usize;
while i < 256 {
table[i] = matches!(i as u8, b' ' | 0x09..=0x0D);
i += 1;
}
table
}
static DATE_WS: [bool; 256] = build_ws_table();
#[inline]
fn cannot_be_date(input: &str) -> bool {
input.bytes().any(|b| !DATE_BYTE[b as usize])
}
#[inline]
fn tz_suffix(input: &str, prefix_end: usize, require_ws: bool) -> Option<&str> {
let bytes = input.as_bytes();
let rest = input.get(prefix_end..)?;
if require_ws {
let leading_ws = rest
.as_bytes()
.first()
.is_some_and(|&b| DATE_WS[b as usize]);
let trailing_ws = prefix_end
.checked_sub(1)
.is_some_and(|i| DATE_WS[bytes[i] as usize]);
if !leading_ws && !trailing_ws {
return None;
}
}
let token = rest.trim_start_matches(|c: char| (c as u32) < 128 && DATE_WS[c as usize]);
if !(3..=6).contains(&token.len()) {
return None;
}
token
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b':'))
.then_some(token)
}
const NORMALIZE_SCRATCH: usize = 64;
#[inline]
fn normalize_into<'b>(
input: &str,
buf: &'b mut [u8; NORMALIZE_SCRATCH],
strip: &[u8],
drop_at: bool,
) -> Option<&'b str> {
if input.len() > buf.len() {
return None;
}
let mut n = 0;
for &b in input.as_bytes() {
if !strip.contains(&b) {
buf[n] = b;
n += 1;
}
}
if drop_at && let Some(pos) = buf[..n].windows(2).position(|w| w == b"at") {
buf.copy_within(pos + 2..n, pos);
n -= 2;
}
std::str::from_utf8(&buf[..n]).ok()
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TimeShape {
Hm,
Hms,
HmsF,
ImP,
ImsP,
HmsFP,
}
#[inline]
fn time_shape(input: &str) -> TimeShape {
let bytes = input.as_bytes();
let ampm = bytes.len() >= 2 && {
let [.., ap, m] = bytes else { unreachable!() };
m | 32 == b'm' && matches!(ap | 32, b'a' | b'p')
};
let mut colons = 0_u8;
let mut fraction = false;
for &b in bytes {
match b {
b':' => colons += 1,
b'.' => fraction = true,
_ => {}
}
}
match (colons >= 2, fraction, ampm) {
(true, true, true) => TimeShape::HmsFP,
(true, true, false) => TimeShape::HmsF,
(true, false, true) => TimeShape::ImsP,
(true, false, false) => TimeShape::Hms,
(false, _, true) => TimeShape::ImP,
(false, _, false) => TimeShape::Hm,
}
}
#[inline]
fn slash_year_is_two_digits(bytes: &[u8]) -> bool {
let mut slashes = 0u8;
let mut year_len = 0usize;
for &b in bytes {
if b == b'/' {
slashes += 1;
} else if slashes == 2 {
if b.is_ascii_digit() {
year_len += 1;
} else {
break;
}
}
}
year_len == 2
}
pub struct Parse<'z, Tz2> {
tz: &'z Tz2,
default_time: NaiveTime,
prefer_dmy: bool,
}
impl<'z, Tz2> Parse<'z, Tz2>
where
Tz2: TimeZone,
{
pub const fn new(tz: &'z Tz2, default_time: NaiveTime) -> Self {
Self {
tz,
default_time,
prefer_dmy: false,
}
}
pub const fn prefer_dmy(&mut self, yes: bool) -> &Self {
self.prefer_dmy = yes;
self
}
pub const fn new_with_preference(
tz: &'z Tz2,
default_time: NaiveTime,
prefer_dmy: bool,
) -> Self {
Self {
tz,
default_time,
prefer_dmy,
}
}
#[inline]
fn dt_from_items(&self, input: &str, items: &[Item<'static>]) -> ParseResult<DateTime<Tz2>> {
let mut parsed = Parsed::new();
parse_items(&mut parsed, input, items.iter())?;
parsed.to_datetime_with_timezone(self.tz)
}
#[inline]
fn naive_dt_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDateTime> {
let mut parsed = Parsed::new();
parse_items(&mut parsed, input, items.iter())?;
parsed.to_naive_datetime_with_offset(0)
}
#[inline]
fn naive_date_from_items(input: &str, items: &[Item<'static>]) -> ParseResult<NaiveDate> {
let mut parsed = Parsed::new();
parse_items(&mut parsed, input, items.iter())?;
parsed.to_naive_date()
}
#[inline]
pub fn parse(&self, input: &str) -> Result<DateTime<Utc>> {
if cannot_be_date(input) {
return Err(anyhow!("{} did not match any formats.", input));
}
let Some(&first) = input.as_bytes().first() else {
return Err(anyhow!("{} did not match any formats.", input));
};
let parsed = if first.is_ascii_digit() {
self.slash_mdy_family(input)
.or_else(|| self.slash_ymd_family(input))
.or_else(|| self.ymd_family(input))
.or_else(|| self.month_ymd(input))
.or_else(|| self.month_dmy_family(input))
.or_else(|| self.unix_timestamp(input))
.or_else(|| self.rfc2822(input))
} else if first.is_ascii_alphabetic() {
self.month_mdy_family(input).or_else(|| self.rfc2822(input))
} else {
self.unix_timestamp(input).or_else(|| self.rfc2822(input))
};
parsed.unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input)))
}
#[inline]
fn ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {
r"^\d{4}-\d{2}"
};
if !re.is_match(input) {
return None;
}
self.rfc3339(input)
.or_else(|| self.ymd_hms(input))
.or_else(|| self.ymd_hms_z(input))
.or_else(|| self.ymd(input))
.or_else(|| self.ymd_z(input))
}
#[inline]
fn month_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {
r"^[a-zA-Z]{3,9}\.?\s+\d{1,2}"
};
if !re.is_match(input) {
return None;
}
self.month_mdy_hms(input)
.or_else(|| self.month_mdy_hms_z(input))
.or_else(|| self.month_mdy(input))
}
#[inline]
fn month_dmy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}"
};
if !re.is_match(input) {
return None;
}
self.month_dmy_hms(input).or_else(|| self.month_dmy(input))
}
#[inline]
fn slash_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}"
};
if !re.is_match(input) {
return None;
}
if self.prefer_dmy {
self.slash_dmy_hms(input)
.or_else(|| self.slash_dmy(input))
.or_else(|| self.slash_mdy_hms(input))
.or_else(|| self.slash_mdy(input))
} else {
self.slash_mdy_hms(input)
.or_else(|| self.slash_mdy(input))
.or_else(|| self.slash_dmy_hms(input))
.or_else(|| self.slash_dmy(input))
}
}
#[inline]
fn slash_ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}"};
if !re.is_match(input) {
return None;
}
self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input))
}
#[inline]
fn unix_timestamp(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let &b0 = input.as_bytes().first()?;
if !(b0.is_ascii_digit() || matches!(b0, b'+' | b'-' | b'.')) {
return None;
}
let ts_sec_val: f64 = if let Ok(val) = fast_float2::parse(input) {
val
} else {
return None;
};
if !ts_sec_val.is_finite() {
return None;
}
let ts_ns_val = ts_sec_val * 1_000_000_000_f64;
let result = Utc.timestamp_nanos(ts_ns_val as i64).with_timezone(&Utc);
Some(Ok(result))
}
#[inline]
fn rfc3339(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
DateTime::parse_from_rfc3339(input)
.ok()
.map(|parsed| parsed.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn rfc2822(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
if !input.as_bytes().contains(&b':') {
return None;
}
DateTime::parse_from_rfc2822(input)
.ok()
.map(|parsed| parsed.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {
r"^\d{4}-\d{2}-\d{2}[T\s]+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
};
if !re.is_match(input) {
return None;
}
let items = match (input.as_bytes()[10] == b'T', time_shape(input)) {
(true, TimeShape::Hms) => fmt_items!("%Y-%m-%dT%H:%M:%S"),
(true, TimeShape::Hm) => fmt_items!("%Y-%m-%dT%H:%M"),
(true, TimeShape::HmsF) => fmt_items!("%Y-%m-%dT%H:%M:%S%.f"),
(true, TimeShape::ImsP | TimeShape::HmsFP) => {
fmt_items!("%Y-%m-%dT%I:%M:%S%.f %P")
}
(true, TimeShape::ImP) => fmt_items!("%Y-%m-%dT%I:%M %P"),
(false, TimeShape::Hms) => fmt_items!("%Y-%m-%d %H:%M:%S"),
(false, TimeShape::Hm) => fmt_items!("%Y-%m-%d %H:%M"),
(false, TimeShape::HmsF) => fmt_items!("%Y-%m-%d %H:%M:%S%.f"),
(false, TimeShape::ImsP | TimeShape::HmsFP) => {
fmt_items!("%Y-%m-%d %I:%M:%S%.f %P")
}
(false, TimeShape::ImP) => fmt_items!("%Y-%m-%d %I:%M %P"),
};
self.dt_from_items(input, items)
.ok()
.map(|parsed| parsed.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn ymd_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
if input.len() < 17 || !input.as_bytes()[10].is_ascii_whitespace() {
return None;
}
let prefix: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?"};
let fast_tz = prefix
.find(input)
.and_then(|m| tz_suffix(input, m.end(), false));
let re: &Regex = regex! {
r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
};
if let Some(tz) = fast_tz.or_else(|| {
re.captures(input)
.and_then(|caps| caps.name("tz").map(|m| m.as_str().trim()))
}) {
let parse_from_str = Self::naive_dt_from_items;
return match timezone::parse(tz) {
Ok(offset) => parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S %Z"))
.or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M %Z")))
.or_else(|_| parse_from_str(input, fmt_items!("%Y-%m-%d %H:%M:%S%.f %Z")))
.ok()
.and_then(|parsed| offset.from_local_datetime(&parsed).single())
.map(|datetime| datetime.with_timezone(&Utc))
.map(Ok),
Err(err) => Some(Err(err)),
};
}
None
}
#[inline]
fn ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}$"
};
if !re.is_match(input) {
return None;
}
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(self.tz);
Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn ymd_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
if input.len() <= 10 {
return None;
}
let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
};
if let Some(caps) = re.captures(input)
&& let Some(matched_tz) = caps.name("tz")
{
return match timezone::parse(matched_tz.as_str().trim()) {
Ok(offset) => {
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(&offset);
Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d %Z"))
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| offset.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
Err(err) => Some(Err(err)),
};
}
None
}
#[inline]
fn month_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^\d{4}-\w{3,9}-\d{2}$"
};
if !re.is_match(input) {
return None;
}
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(self.tz);
Self::naive_date_from_items(input, fmt_items!("%Y-%m-%d"))
.or_else(|_| Self::naive_date_from_items(input, fmt_items!("%Y-%b-%d")))
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn month_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {
r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4},?\s+\d{1,2}:\d{2}(?::\d{2})?\s*(?:am|pm|AM|PM)?$"
};
if !re.is_match(input) {
return None;
}
let mut buf = [0_u8; NORMALIZE_SCRATCH];
let fallback;
let dt = match normalize_into(input, &mut buf, b",.", false) {
Some(s) => s,
None => {
fallback = input.replace([',', '.'], "");
fallback.as_str()
}
};
let items = match time_shape(dt) {
TimeShape::Hms => fmt_items!("%B %d %Y %H:%M:%S"),
TimeShape::Hm => fmt_items!("%B %d %Y %H:%M"),
TimeShape::ImsP => fmt_items!("%B %d %Y %I:%M:%S %P"),
TimeShape::ImP => fmt_items!("%B %d %Y %I:%M %P"),
TimeShape::HmsF | TimeShape::HmsFP => return None,
};
self.dt_from_items(dt, items)
.ok()
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn month_mdy_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
if input.len() < 20 {
return None;
}
let bytes = input.as_bytes();
let has_year = (0..bytes.len().saturating_sub(3)).any(|i| {
bytes[i..i + 4].iter().all(|b| b.is_ascii_digit())
&& (i == 0 || !bytes[i - 1].is_ascii_digit())
&& bytes.get(i + 4).is_none_or(|b| !b.is_ascii_digit())
});
if !has_year {
return None;
}
let prefix: &Regex = regex! {
r"^[a-zA-Z]{3,9}\s+\d{1,2},?\s+\d{4}\s*,?(?:at)?\s+\d{2}:\d{2}(?::\d{2})?\s*(?:am|pm|AM|PM)?"
};
let fast_tz = prefix
.find(input)
.and_then(|m| tz_suffix(input, m.end(), true));
let re: &Regex = regex! {
r"^[a-zA-Z]{3,9}\s+\d{1,2},?\s+\d{4}\s*,?(?:at)?\s+\d{2}:\d{2}(?::\d{2})?\s*(?:am|pm|AM|PM)?(?P<tz>\s+[+-:a-zA-Z0-9]{3,6})$",
};
if let Some(tz) = fast_tz.or_else(|| {
re.captures(input)
.and_then(|caps| caps.name("tz").map(|m| m.as_str().trim()))
}) {
let parse_from_str = Self::naive_dt_from_items;
return match timezone::parse(tz) {
Ok(offset) => {
let mut buf = [0_u8; NORMALIZE_SCRATCH];
let fallback;
let dt: &str = match normalize_into(input, &mut buf, b",", true) {
Some(s) => s,
None => {
let mut owned = input.replace(',', "");
if let Some(pos) = owned.find("at") {
owned.replace_range(pos..pos + 2, "");
}
fallback = owned;
fallback.as_str()
}
};
parse_from_str(dt, fmt_items!("%B %d %Y %H:%M:%S %Z"))
.or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %H:%M %Z")))
.or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %I:%M:%S %P %Z")))
.or_else(|_| parse_from_str(dt, fmt_items!("%B %d %Y %I:%M %P %Z")))
.ok()
.and_then(|parsed| offset.from_local_datetime(&parsed).single())
.map(|datetime| datetime.with_timezone(&Utc))
.map(Ok)
}
Err(err) => Some(Err(err)),
};
}
None
}
#[inline]
fn month_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4}$"
};
if !re.is_match(input) {
return None;
}
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(self.tz);
let mut buf = [0_u8; NORMALIZE_SCRATCH];
let fallback;
let dt = match normalize_into(input, &mut buf, b",.", false) {
Some(s) => s,
None => {
fallback = input.replace([',', '.'], "");
fallback.as_str()
}
};
Self::naive_date_from_items(dt, fmt_items!("%B %d %y"))
.or_else(|_| Self::naive_date_from_items(dt, fmt_items!("%B %d %Y")))
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn month_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
if !input.as_bytes().contains(&b':') {
return None;
}
let re: &Regex = regex! {
r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4},?\s+\d{1,2}:[0-9]{2}(?::[0-9]{2})?(?:\.[0-9]{1,9})?$"
};
if !re.is_match(input) {
return None;
}
let mut buf = [0_u8; NORMALIZE_SCRATCH];
let fallback;
let dt = match normalize_into(input, &mut buf, b",", false) {
Some(s) => s,
None => {
fallback = input.replace(',', "");
fallback.as_str()
}
};
let items = match time_shape(dt) {
TimeShape::Hms => fmt_items!("%d %B %Y %H:%M:%S"),
TimeShape::Hm => fmt_items!("%d %B %Y %H:%M"),
TimeShape::HmsF => fmt_items!("%d %B %Y %H:%M:%S%.f"),
TimeShape::ImP | TimeShape::ImsP | TimeShape::HmsFP => return None,
};
self.dt_from_items(dt, items)
.ok()
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn month_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4}$"
};
if !re.is_match(input) {
return None;
}
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(self.tz);
let bytes = input.as_bytes();
let len = bytes.len();
let four_digit_year = len >= 5
&& bytes[len - 4..].iter().all(|b| b.is_ascii_digit())
&& bytes[len - 5].is_ascii_whitespace();
let parsed = if four_digit_year {
Self::naive_date_from_items(input, fmt_items!("%d %B %Y"))
} else {
Self::naive_date_from_items(input, fmt_items!("%d %B %y"))
.or_else(|_| Self::naive_date_from_items(input, fmt_items!("%d %B %Y")))
};
parsed
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn slash_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {
r"^\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
};
if !re.is_match(input) {
return None;
}
let items = match (
slash_year_is_two_digits(input.as_bytes()),
time_shape(input),
) {
(true, TimeShape::Hms) => fmt_items!("%m/%d/%y %H:%M:%S"),
(true, TimeShape::Hm) => fmt_items!("%m/%d/%y %H:%M"),
(true, TimeShape::HmsF) => fmt_items!("%m/%d/%y %H:%M:%S%.f"),
(true, TimeShape::ImsP | TimeShape::HmsFP) => {
fmt_items!("%m/%d/%y %I:%M:%S%.f %P")
}
(true, TimeShape::ImP) => fmt_items!("%m/%d/%y %I:%M %P"),
(false, TimeShape::Hms) => fmt_items!("%m/%d/%Y %H:%M:%S"),
(false, TimeShape::Hm) => fmt_items!("%m/%d/%Y %H:%M"),
(false, TimeShape::HmsF) => fmt_items!("%m/%d/%Y %H:%M:%S%.f"),
(false, TimeShape::ImsP | TimeShape::HmsFP) => {
fmt_items!("%m/%d/%Y %I:%M:%S%.f %P")
}
(false, TimeShape::ImP) => fmt_items!("%m/%d/%Y %I:%M %P"),
};
self.dt_from_items(input, items)
.ok()
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn slash_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {
r"^\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2}(?::\d{2})?(?:\.\d{1,9})?\s*(?:am|pm|AM|PM)?$"
};
if !re.is_match(input) {
return None;
}
let items = match (
slash_year_is_two_digits(input.as_bytes()),
time_shape(input),
) {
(true, TimeShape::Hms) => fmt_items!("%d/%m/%y %H:%M:%S"),
(true, TimeShape::Hm) => fmt_items!("%d/%m/%y %H:%M"),
(true, TimeShape::HmsF) => fmt_items!("%d/%m/%y %H:%M:%S%.f"),
(true, TimeShape::ImsP | TimeShape::HmsFP) => {
fmt_items!("%d/%m/%y %I:%M:%S%.f %P")
}
(true, TimeShape::ImP) => fmt_items!("%d/%m/%y %I:%M %P"),
(false, TimeShape::Hms) => fmt_items!("%d/%m/%Y %H:%M:%S"),
(false, TimeShape::Hm) => fmt_items!("%d/%m/%Y %H:%M"),
(false, TimeShape::HmsF) => fmt_items!("%d/%m/%Y %H:%M:%S%.f"),
(false, TimeShape::ImsP | TimeShape::HmsFP) => {
fmt_items!("%d/%m/%Y %I:%M:%S%.f %P")
}
(false, TimeShape::ImP) => fmt_items!("%d/%m/%Y %I:%M %P"),
};
self.dt_from_items(input, items)
.ok()
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn slash_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}/\d{2,4}$"
};
if !re.is_match(input) {
return None;
}
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(self.tz);
let fmt = if slash_year_is_two_digits(input.as_bytes()) {
fmt_items!("%m/%d/%y")
} else {
fmt_items!("%m/%d/%Y")
};
Self::naive_date_from_items(input, fmt)
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn slash_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$"
};
if !re.is_match(input) {
return None;
}
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(self.tz);
let fmt = if slash_year_is_two_digits(input.as_bytes()) {
fmt_items!("%d/%m/%y")
} else {
fmt_items!("%d/%m/%Y")
};
Self::naive_date_from_items(input, fmt)
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn slash_ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {
r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}\s+[0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?(?:\.[0-9]{1,9})?\s*(?:am|pm|AM|PM)?$"
};
if !re.is_match(input) {
return None;
}
let items = match time_shape(input) {
TimeShape::Hms => fmt_items!("%Y/%m/%d %H:%M:%S"),
TimeShape::Hm => fmt_items!("%Y/%m/%d %H:%M"),
TimeShape::HmsF => fmt_items!("%Y/%m/%d %H:%M:%S%.f"),
TimeShape::ImsP | TimeShape::HmsFP => fmt_items!("%Y/%m/%d %I:%M:%S%.f %P"),
TimeShape::ImP => fmt_items!("%Y/%m/%d %I:%M %P"),
};
self.dt_from_items(input, items)
.ok()
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
#[inline]
fn slash_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$"
};
if !re.is_match(input) {
return None;
}
let now = Utc::now()
.date()
.and_time(self.default_time)?
.with_timezone(self.tz);
Self::naive_date_from_items(input, fmt_items!("%Y/%m/%d"))
.ok()
.map(|parsed| parsed.and_time(now.time()))
.and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
.map(|at_tz| at_tz.with_timezone(&Utc))
.map(Ok)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unix_timestamp() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = vec![
("0", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
("-770172300", Utc.ymd(1945, 8, 5).and_hms(23, 15, 0)),
(
"1671673426.123456789",
Utc.ymd(2022, 12, 22).and_hms_nano(1, 43, 46, 123456768),
),
("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)),
(
"1620036248.420",
Utc.ymd(2021, 5, 3).and_hms_milli(10, 4, 8, 420),
),
(
"1620036248.717915136",
Utc.ymd(2021, 5, 3).and_hms_nano(10, 4, 8, 717915136),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.unix_timestamp(input).unwrap().unwrap(),
want,
"unix_timestamp/{}",
input
)
}
assert!(parse.unix_timestamp("15116").is_some());
assert!(
parse
.unix_timestamp("16200248727179150001620024872717915000") .is_some()
);
assert!(parse.unix_timestamp("not-a-ts").is_none());
for input in [
"inf", "nan", "INF", "NaN", "infinity", "+inf", "-inf", "-nan",
] {
assert!(
parse.unix_timestamp(input).is_none(),
"unix_timestamp must reject non-finite {input}"
);
}
}
#[test]
fn rfc3339() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"2021-05-01T01:17:02.604456Z",
Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000),
),
(
"2017-11-25T22:34:50Z",
Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.rfc3339(input).unwrap().unwrap(),
want,
"rfc3339/{}",
input
)
}
assert!(parse.rfc3339("2017-11-25 22:34:50").is_none());
assert!(parse.rfc3339("not-date-time").is_none());
}
#[test]
fn rfc2822() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"Wed, 02 Jun 2021 06:31:39 GMT",
Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
),
(
"Wed, 02 Jun 2021 06:31:39 PDT",
Utc.ymd(2021, 6, 2).and_hms(13, 31, 39),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.rfc2822(input).unwrap().unwrap(),
want,
"rfc2822/{}",
input
)
}
assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none());
assert!(parse.rfc2822("not-date-time").is_none());
}
#[test]
fn ymd_hms() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)),
(
"2021-04-30 21:14:10",
Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
),
(
"2021-04-30 21:14:10.052282",
Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282),
),
(
"2014-04-26 05:24:37 PM",
Utc.ymd(2014, 4, 26).and_hms(17, 24, 37),
),
(
"2014-04-26 17:24:37.123",
Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123),
),
(
"2014-04-26 17:24:37.3186369",
Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900),
),
(
"2012-08-03 18:31:59.257000000",
Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
),
("2020-01-15T08:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
("2020-01-15T08:00:00", Utc.ymd(2020, 1, 15).and_hms(8, 0, 0)),
(
"2020-01-15T08:00:00.123",
Utc.ymd(2020, 1, 15).and_hms_milli(8, 0, 0, 123),
),
(
"2020-01-15T08:00:00.123456",
Utc.ymd(2020, 1, 15).and_hms_micro(8, 0, 0, 123456),
),
(
"2020-01-15T08:00:00.123456789",
Utc.ymd(2020, 1, 15).and_hms_nano(8, 0, 0, 123456789),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.ymd_hms(input).unwrap().unwrap(),
want,
"ymd_hms/{}",
input
)
}
assert!(parse.ymd_hms("not-date-time").is_none());
let t_form = parse.ymd_hms("2020-01-15T08:00:00").unwrap().unwrap();
let space_form = parse.ymd_hms("2020-01-15 08:00:00").unwrap().unwrap();
assert_eq!(t_form, space_form, "T-separator vs space disagree");
}
#[test]
fn ymd_hms_z() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"2017-11-25 13:31:15 PST",
Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
),
(
"2017-11-25 13:31 PST",
Utc.ymd(2017, 11, 25).and_hms(21, 31, 0),
),
(
"2014-12-16 06:20:00 UTC",
Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
),
(
"2014-12-16 06:20:00 GMT",
Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
),
(
"2014-04-26 13:13:43 +0800",
Utc.ymd(2014, 4, 26).and_hms(5, 13, 43),
),
(
"2014-04-26 13:13:44 +09:00",
Utc.ymd(2014, 4, 26).and_hms(4, 13, 44),
),
(
"2012-08-03 18:31:59.257000000 +0000",
Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
),
(
"2015-09-30 18:48:56.35272715 UTC",
Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.ymd_hms_z(input).unwrap().unwrap(),
want,
"ymd_hms_z/{}",
input
)
}
assert!(parse.ymd_hms_z("not-date-time").is_none());
assert!(parse.ymd_hms_z("2021-04-30 21:14").is_none()); assert!(parse.ymd_hms_z("2021-04-30X21:14Z").is_none()); assert!(parse.ymd_hms_z("2021-04-30 21:1XZ").is_none()); }
#[test]
fn ymd() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [(
"2021-02-21",
Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
)];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.ymd(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"ymd/{}",
input
)
}
assert!(parse.ymd("not-date-time").is_none());
}
#[test]
fn ymd_z() {
let parse = Parse::new(&Utc, Utc::now().time());
let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600));
let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600));
let test_cases = [
(
"2021-02-21 PST",
FixedOffset::west(8 * 3600)
.ymd(2021, 2, 21)
.and_time(now_at_pst.time())
.map(|dt| dt.with_timezone(&Utc)),
),
(
"2021-02-21 UTC",
FixedOffset::west(0)
.ymd(2021, 2, 21)
.and_time(Utc::now().time())
.map(|dt| dt.with_timezone(&Utc)),
),
(
"2020-07-20+08:00",
FixedOffset::east(8 * 3600)
.ymd(2020, 7, 20)
.and_time(now_at_cst.time())
.map(|dt| dt.with_timezone(&Utc)),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.ymd_z(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"ymd_z/{}",
input
)
}
assert!(parse.ymd_z("not-date-time").is_none());
assert!(parse.ymd_z("2021-02-21").is_none()); assert!(parse.ymd_z("2021-02-21X").is_none()); }
#[test]
fn month_ymd() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [(
"2021-Feb-21",
Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
)];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.month_ymd(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"month_ymd/{}",
input
)
}
assert!(parse.month_ymd("not-date-time").is_none());
}
#[test]
fn month_mdy_hms() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"May 8, 2009 5:57:51 PM",
Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
),
(
"September 17, 2012 10:09am",
Utc.ymd(2012, 9, 17).and_hms(10, 9, 0),
),
(
"September 17, 2012, 10:10:09",
Utc.ymd(2012, 9, 17).and_hms(10, 10, 9),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.month_mdy_hms(input).unwrap().unwrap(),
want,
"month_mdy_hms/{}",
input
)
}
assert!(parse.month_mdy_hms("not-date-time").is_none());
}
#[test]
fn month_mdy_hms_z() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"May 02, 2021 15:51:31 UTC",
Utc.ymd(2021, 5, 2).and_hms(15, 51, 31),
),
(
"May 02, 2021 15:51 UTC",
Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
),
(
"May 26, 2021, 12:49 AM PDT",
Utc.ymd(2021, 5, 26).and_hms(7, 49, 0),
),
(
"September 17, 2012 at 10:09am PST",
Utc.ymd(2012, 9, 17).and_hms(18, 9, 0),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.month_mdy_hms_z(input).unwrap().unwrap(),
want,
"month_mdy_hms_z/{}",
input
)
}
assert!(parse.month_mdy_hms_z("not-date-time").is_none());
assert!(parse.month_mdy_hms_z("May 27, 02:45:27 XX PST").is_none()); assert!(parse.month_mdy_hms_z("May 27 1234 something PST").is_none()); }
#[test]
fn month_mdy() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"May 25, 2021",
Utc.ymd(2021, 5, 25).and_time(Utc::now().time()),
),
(
"oct 7, 1970",
Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
),
(
"oct 7, 70",
Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
),
(
"oct. 7, 1970",
Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
),
(
"oct. 7, 70",
Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
),
(
"October 7, 1970",
Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.month_mdy(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"month_mdy/{}",
input
)
}
assert!(parse.month_mdy("not-date-time").is_none());
}
#[test]
fn month_dmy_hms() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"12 Feb 2006, 19:17",
Utc.ymd(2006, 2, 12).and_hms(19, 17, 0),
),
("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)),
(
"14 May 2019 19:11:40.164",
Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.month_dmy_hms(input).unwrap().unwrap(),
want,
"month_dmy_hms/{}",
input
)
}
assert!(parse.month_dmy_hms("not-date-time").is_none());
}
#[test]
fn month_dmy() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())),
(
"7 oct 1970",
Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
),
(
"03 February 2013",
Utc.ymd(2013, 2, 3).and_time(Utc::now().time()),
),
(
"1 July 2013",
Utc.ymd(2013, 7, 1).and_time(Utc::now().time()),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.month_dmy(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"month_dmy/{}",
input
)
}
assert!(parse.month_dmy("not-date-time").is_none());
}
#[test]
fn month_dmy_year_fast_path() {
let parse = Parse::new(&Utc, Utc::now().time());
let four_digit = parse.month_dmy("14 May 2019").unwrap().unwrap();
assert_eq!(four_digit.year(), 2019);
assert_eq!(four_digit.month(), 5);
assert_eq!(four_digit.day(), 14);
let two_digit = parse.month_dmy("14 May 19").unwrap().unwrap();
assert_eq!(two_digit.year(), 2019);
assert_eq!(two_digit.month(), 5);
assert_eq!(two_digit.day(), 14);
}
#[test]
fn slash_mdy_hms() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = vec![
("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
(
"8/8/1965 01:00:01 PM",
Utc.ymd(1965, 8, 8).and_hms(13, 0, 1),
),
("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
(
"03/19/2012 10:11:59",
Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
),
(
"03/19/2012 10:11:59.3186369",
Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.slash_mdy_hms(input).unwrap().unwrap(),
want,
"slash_mdy_hms/{}",
input
)
}
assert!(parse.slash_mdy_hms("not-date-time").is_none());
}
#[test]
fn slash_mdy() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"3/31/2014",
Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
),
(
"03/31/2014",
Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
),
("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.slash_mdy(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"slash_mdy/{}",
input
)
}
assert!(parse.slash_mdy("not-date-time").is_none());
}
#[test]
fn slash_dmy() {
let mut parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"31/3/2014",
Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
),
(
"13/11/2014",
Utc.ymd(2014, 11, 13).and_time(Utc::now().time()),
),
("21/08/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
("1/8/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.prefer_dmy(true)
.slash_dmy(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"slash_dmy/{}",
input
)
}
assert!(parse.slash_dmy("not-date-time").is_none());
}
#[test]
fn slash_ymd_hms() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
(
"2012/03/19 10:11:59",
Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
),
(
"2012/03/19 10:11:59.3186369",
Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse.slash_ymd_hms(input).unwrap().unwrap(),
want,
"slash_ymd_hms/{}",
input
)
}
assert!(parse.slash_ymd_hms("not-date-time").is_none());
}
#[test]
fn slash_ymd() {
let parse = Parse::new(&Utc, Utc::now().time());
let test_cases = [
(
"2014/3/31",
Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
),
(
"2014/03/31",
Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
),
];
for &(input, want) in test_cases.iter() {
assert_eq!(
parse
.slash_ymd(input)
.unwrap()
.unwrap()
.trunc_subsecs(0)
.with_second(0)
.unwrap(),
want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
"slash_ymd/{}",
input
)
}
assert!(parse.slash_ymd("not-date-time").is_none());
}
#[test]
fn time_shape_classification() {
use TimeShape::{Hm, Hms, HmsF, HmsFP, ImP, ImsP};
let cases = [
("2021-04-30 21:14", Hm),
("2021-04-30T21:14", Hm),
("2021-04-30 21:14:10", Hms),
("2021-04-30 21:14:10.052282", HmsF),
("8/8/1965 12:00 AM", ImP),
("8/8/1965 12:00am", ImP),
("8/8/1965 01:00:01 PM", ImsP),
("September 17 2012 10:09am", ImP),
("03/19/2012 10:11:59.318 PM", HmsFP),
("03/19/2012 10:11.123", Hm),
];
for (input, want) in cases {
assert!(
time_shape(input) == want,
"time_shape misclassified {input}"
);
}
}
#[test]
fn every_fmt_items_literal_is_valid() {
let src = include_str!("datetime.rs");
let call = Regex::new(r#"fmt_items!\("([^"]*)"\)"#).unwrap();
let mut checked = 0_usize;
for caps in call.captures_iter(src) {
let fmt = &caps[1];
assert!(
chrono::format::StrftimeItems::new(fmt).parse().is_ok(),
"invalid strftime literal: {fmt}"
);
checked += 1;
}
assert!(
checked >= 50,
"expected to check every fmt_items! literal, only found {checked}"
);
}
#[test]
fn fractional_seconds_with_ampm() {
let parse = Parse::new(&Utc, Utc::now().time());
let cases = [
(
"03/19/2012 10:11:59.318 PM",
Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
),
(
"3/19/2012 1:11:59.318 am",
Utc.ymd(2012, 3, 19).and_hms_milli(1, 11, 59, 318),
),
(
"03/19/12 10:11:59.318 PM",
Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
),
(
"2012/03/19 10:11:59.318 PM",
Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
),
(
"2012-03-19 10:11:59.318 PM",
Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
),
(
"2012-03-19T10:11:59.318 PM",
Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
),
(
"2012-03-19 10:11:59.3186369 PM",
Utc.ymd(2012, 3, 19).and_hms_nano(22, 11, 59, 318636900),
),
(
"03/19/2012 10:11:59 PM",
Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
),
(
"2012-03-19 10:11:59 PM",
Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
),
];
for (input, want) in cases {
assert_eq!(parse.parse(input).unwrap(), want, "parse/{input}");
}
let dmy = Parse::new_with_preference(&Utc, Utc::now().time(), true);
let dmy_cases = [
(
"19/03/2012 10:11:59.318 PM",
Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
),
(
"19/03/12 10:11:59.318 PM",
Utc.ymd(2012, 3, 19).and_hms_milli(22, 11, 59, 318),
),
(
"9/3/2012 1:11:59.318 am",
Utc.ymd(2012, 3, 9).and_hms_milli(1, 11, 59, 318),
),
(
"19/03/2012 10:11:59 PM",
Utc.ymd(2012, 3, 19).and_hms(22, 11, 59),
),
];
for (input, want) in dmy_cases {
assert_eq!(dmy.parse(input).unwrap(), want, "prefer_dmy/{input}");
}
}
#[test]
fn unsupported_shapes_still_fail() {
let parse = Parse::new(&Utc, Utc::now().time());
for input in [
"May 8, 2009 5:57:51.123 PM",
"14 May 2019 07:11:40 PM",
"14 May 2019 07:11:40.164 PM",
"03/19/2012 10:11.123",
"2021-04-30 21:14.052282",
"03/19/2012 10:11.123 PM",
"03/19/2012 22:11:59.318 PM",
] {
assert!(
parse.parse(input).is_err(),
"{input} is expected to remain unsupported"
);
}
}
}