use std::error::Error;
use std::fmt;
use std::str::FromStr;
use chrono::{DateTime, Duration, Local, NaiveDateTime, Utc};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TimeOp {
Gt,
Ge,
Lt,
Le,
Eq,
}
impl TimeOp {
pub const ALL: [TimeOp; 5] = [TimeOp::Gt, TimeOp::Ge, TimeOp::Lt, TimeOp::Le, TimeOp::Eq];
#[inline]
#[must_use]
pub fn applies(self, value: DateTime<Utc>, threshold: DateTime<Utc>) -> bool {
match self {
TimeOp::Gt => value > threshold,
TimeOp::Ge => value >= threshold,
TimeOp::Lt => value < threshold,
TimeOp::Le => value <= threshold,
TimeOp::Eq => value == threshold,
}
}
}
impl fmt::Display for TimeOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
TimeOp::Gt => ">",
TimeOp::Ge => ">=",
TimeOp::Lt => "<",
TimeOp::Le => "<=",
TimeOp::Eq => "=",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TimeFilter {
op: TimeOp,
time: DateTime<Utc>,
}
impl TimeFilter {
#[inline]
#[must_use]
pub const fn new(op: TimeOp, time: DateTime<Utc>) -> Self {
TimeFilter { op, time }
}
#[inline]
#[must_use]
pub const fn op(self) -> TimeOp {
self.op
}
#[inline]
#[must_use]
pub fn time(self) -> DateTime<Utc> {
self.time
}
#[inline]
#[must_use]
pub const fn gt(time: DateTime<Utc>) -> Self {
TimeFilter {
op: TimeOp::Gt,
time,
}
}
#[inline]
#[must_use]
pub const fn ge(time: DateTime<Utc>) -> Self {
TimeFilter {
op: TimeOp::Ge,
time,
}
}
#[inline]
#[must_use]
pub const fn lt(time: DateTime<Utc>) -> Self {
TimeFilter {
op: TimeOp::Lt,
time,
}
}
#[inline]
#[must_use]
pub const fn le(time: DateTime<Utc>) -> Self {
TimeFilter {
op: TimeOp::Le,
time,
}
}
#[inline]
#[must_use]
pub const fn eq(time: DateTime<Utc>) -> Self {
TimeFilter {
op: TimeOp::Eq,
time,
}
}
#[inline]
#[must_use]
pub fn matches(self, value: DateTime<Utc>) -> bool {
self.op.applies(value, self.time)
}
}
impl fmt::Display for TimeFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.op, format_datetime(&self.time))
}
}
impl FromStr for TimeFilter {
type Err = TimeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_time_filter(s)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for TimeFilter {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(&format!(
"{}{}",
self.op,
self.time.format("%Y-%m-%d %H:%M:%S")
))
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for TimeFilter {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimeError {
MissingOperator,
EmptyInput,
UnknownSuffix,
InvalidNumber,
InvalidDate,
}
impl fmt::Display for TimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
TimeError::MissingOperator => {
"time filter must start with an operator (>=, >, <=, <, =)"
}
TimeError::EmptyInput => "empty input",
TimeError::UnknownSuffix => "unknown time suffix",
TimeError::InvalidNumber => "failed to parse number",
TimeError::InvalidDate => "failed to parse date/time",
})
}
}
impl Error for TimeError {}
pub type TimeResult<T> = Result<T, TimeError>;
pub fn parse_time_filter(s: &str) -> TimeResult<TimeFilter> {
let s = s.trim();
let prefixes: &[(&str, TimeOp)] = &[
(">=", TimeOp::Ge),
("<=", TimeOp::Le),
(">", TimeOp::Gt),
("<", TimeOp::Lt),
("=", TimeOp::Eq),
];
let (op, rest) = prefixes
.iter()
.find_map(|&(prefix, op)| s.strip_prefix(prefix).map(|r| (op, r)))
.ok_or(TimeError::MissingOperator)?;
let time = parse_time(rest)?;
Ok(TimeFilter { op, time })
}
pub fn parse_time(time_str: &str) -> TimeResult<DateTime<Utc>> {
let s = time_str.trim();
if s.is_empty() {
return Err(TimeError::EmptyInput);
}
if let Some(time) = try_parse_relative(s) {
return Ok(time);
}
try_parse_absolute(s)
}
fn try_parse_relative(s: &str) -> Option<DateTime<Utc>> {
let duration = parse_duration_inner(s)?;
Some(Utc::now() - duration)
}
pub fn parse_duration(s: &str) -> TimeResult<Duration> {
let s = s.trim();
if s.is_empty() {
return Err(TimeError::EmptyInput);
}
if s.starts_with('P') || s.starts_with('p') {
return parse_iso8601_duration(s);
}
parse_duration_inner(s).ok_or(TimeError::UnknownSuffix)
}
fn parse_duration_inner(s: &str) -> Option<Duration> {
let alpha_pos = s.find(|c: char| c.is_ascii_alphabetic())?;
let (num_str, suffix) = s.split_at(alpha_pos);
let num: i64 = num_str.trim().parse().ok()?;
let suf = suffix.trim();
if suf.eq_ignore_ascii_case("d")
|| suf.eq_ignore_ascii_case("day")
|| suf.eq_ignore_ascii_case("days")
{
Some(Duration::days(num))
} else if suf.eq_ignore_ascii_case("h")
|| suf.eq_ignore_ascii_case("hr")
|| suf.eq_ignore_ascii_case("hour")
|| suf.eq_ignore_ascii_case("hours")
{
Some(Duration::hours(num))
} else if suf.eq_ignore_ascii_case("m")
|| suf.eq_ignore_ascii_case("min")
|| suf.eq_ignore_ascii_case("minute")
|| suf.eq_ignore_ascii_case("minutes")
{
Some(Duration::minutes(num))
} else if suf.eq_ignore_ascii_case("s")
|| suf.eq_ignore_ascii_case("sec")
|| suf.eq_ignore_ascii_case("second")
|| suf.eq_ignore_ascii_case("seconds")
{
Some(Duration::seconds(num))
} else {
None
}
}
fn parse_iso8601_duration(s: &str) -> TimeResult<Duration> {
let s = if s.as_bytes().first() == Some(&b'P') || s.as_bytes().first() == Some(&b'p') {
&s[1..]
} else {
return Err(TimeError::UnknownSuffix);
};
if s.is_empty() {
return Err(TimeError::InvalidNumber);
}
let mut total_seconds = 0i64;
let mut num_start: Option<usize> = None;
let mut in_time = false;
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
match b {
b'T' | b't' => {
in_time = true;
if num_start.is_some() {
return Err(TimeError::InvalidNumber);
}
}
b'0'..=b'9' => {
if num_start.is_none() {
num_start = Some(i);
}
}
b'D' | b'd' => {
let Some(start) = num_start.take() else {
return Err(TimeError::InvalidNumber);
};
let days: i64 = s[start..i].parse().map_err(|_| TimeError::InvalidNumber)?;
total_seconds += days * 86400;
}
b'H' | b'h' => {
let Some(start) = num_start.take() else {
return Err(TimeError::InvalidNumber);
};
if !in_time {
return Err(TimeError::InvalidNumber);
}
let hours: i64 = s[start..i].parse().map_err(|_| TimeError::InvalidNumber)?;
total_seconds += hours * 3600;
}
b'M' | b'm' => {
let Some(start) = num_start.take() else {
return Err(TimeError::InvalidNumber);
};
if !in_time {
return Err(TimeError::InvalidNumber);
}
let minutes: i64 = s[start..i].parse().map_err(|_| TimeError::InvalidNumber)?;
total_seconds += minutes * 60;
}
b'S' | b's' => {
let Some(start) = num_start.take() else {
return Err(TimeError::InvalidNumber);
};
if !in_time {
return Err(TimeError::InvalidNumber);
}
let seconds: i64 = s[start..i].parse().map_err(|_| TimeError::InvalidNumber)?;
total_seconds += seconds;
}
_ => {
return Err(TimeError::UnknownSuffix);
}
}
}
if num_start.is_some() {
return Err(TimeError::InvalidNumber);
}
if total_seconds == 0 {
return Err(TimeError::InvalidNumber);
}
Ok(Duration::seconds(total_seconds))
}
fn try_parse_absolute(s: &str) -> TimeResult<DateTime<Utc>> {
if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
return Ok(DateTime::from_naive_utc_and_offset(naive, Utc));
}
if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M") {
return Ok(DateTime::from_naive_utc_and_offset(naive, Utc));
}
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
let Some(naive) = naive_date.and_hms_opt(0, 0, 0) else {
return Err(TimeError::InvalidDate);
};
return Ok(DateTime::from_naive_utc_and_offset(naive, Utc));
}
Err(TimeError::InvalidDate)
}
#[must_use]
pub fn format_datetime(dt: &DateTime<Utc>) -> String {
dt.with_timezone(&Local)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
}