use std::{error::Error, str::FromStr};
use chrono::{DateTime, NaiveDate, TimeZone, Utc};
use simulator_api::AvailableRange;
#[derive(Debug, Clone)]
pub enum RangeBound {
Slot(u64),
Time(DateTime<Utc>),
}
#[derive(Debug)]
pub struct ParseRangeBoundError(String);
impl std::fmt::Display for ParseRangeBoundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl Error for ParseRangeBoundError {}
impl FromStr for RangeBound {
type Err = ParseRangeBoundError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(slot) = s.parse::<u64>() {
return Ok(Self::Slot(slot));
}
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Ok(Self::Time(dt.with_timezone(&Utc)));
}
if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
let dt = Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap());
return Ok(Self::Time(dt));
}
Err(ParseRangeBoundError(format!(
"could not parse {s:?} as a slot number, RFC 3339 timestamp, or YYYY-MM-DD date"
)))
}
}
pub fn filter_ranges(
ranges: &[AvailableRange],
after: Option<&RangeBound>,
before: Option<&RangeBound>,
) -> Vec<AvailableRange> {
let mut out = ranges.to_vec();
if let Some(bound) = after {
out.retain(|r| starts_after(r, bound));
}
if let Some(bound) = before {
out.retain(|r| ends_before(r, bound));
}
out
}
fn ends_before(r: &AvailableRange, bound: &RangeBound) -> bool {
match bound {
RangeBound::Slot(slot) => r.max_bundle_end_slot.is_none_or(|end| end <= *slot),
RangeBound::Time(time) => match &r.max_bundle_end_slot_utc {
None => true, Some(utc_str) => parse_utc(utc_str).is_none_or(|end| end <= *time),
},
}
}
fn starts_after(r: &AvailableRange, bound: &RangeBound) -> bool {
match bound {
RangeBound::Slot(slot) => r.bundle_start_slot >= *slot,
RangeBound::Time(time) => match &r.bundle_start_slot_utc {
None => true, Some(utc_str) => parse_utc(utc_str).is_none_or(|start| start >= *time),
},
}
}
fn parse_utc(s: &str) -> Option<DateTime<Utc>> {
DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.with_timezone(&Utc))
}