Skip to main content

simulator_client/
ranges.rs

1use std::{error::Error, str::FromStr};
2
3use chrono::{DateTime, NaiveDate, TimeZone, Utc};
4use simulator_api::AvailableRange;
5
6/// A slot number or a UTC timestamp, used to filter available ranges.
7#[derive(Debug, Clone)]
8pub enum RangeBound {
9    Slot(u64),
10    Time(DateTime<Utc>),
11}
12
13#[derive(Debug)]
14pub struct ParseRangeBoundError(String);
15
16impl std::fmt::Display for ParseRangeBoundError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        f.write_str(&self.0)
19    }
20}
21
22impl Error for ParseRangeBoundError {}
23
24impl FromStr for RangeBound {
25    type Err = ParseRangeBoundError;
26
27    /// Parse from a string: tries a raw slot number first, then RFC 3339,
28    /// then date-only "YYYY-MM-DD" (interpreted as midnight UTC).
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        if let Ok(slot) = s.parse::<u64>() {
31            return Ok(Self::Slot(slot));
32        }
33        if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
34            return Ok(Self::Time(dt.with_timezone(&Utc)));
35        }
36        if let Ok(date) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
37            let dt = Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0).unwrap());
38            return Ok(Self::Time(dt));
39        }
40        Err(ParseRangeBoundError(format!(
41            "could not parse {s:?} as a slot number, RFC 3339 timestamp, or YYYY-MM-DD date"
42        )))
43    }
44}
45
46/// Filter `ranges` to those bookended by `[after, before]`:
47/// - `after`: keep only ranges whose start is at or after this bound
48/// - `before`: keep only ranges whose end is at or before this bound
49///
50/// When a bound is a [`RangeBound::Time`], the range's UTC timestamp fields are used;
51/// ranges that have no UTC timestamp for the relevant boundary are kept (conservative).
52/// When a bound is a [`RangeBound::Slot`], slot fields are used directly.
53pub fn filter_ranges(
54    ranges: &[AvailableRange],
55    after: Option<&RangeBound>,
56    before: Option<&RangeBound>,
57) -> Vec<AvailableRange> {
58    let mut out = ranges.to_vec();
59
60    if let Some(bound) = after {
61        out.retain(|r| starts_after(r, bound));
62    }
63    if let Some(bound) = before {
64        out.retain(|r| ends_before(r, bound));
65    }
66
67    out
68}
69
70fn ends_before(r: &AvailableRange, bound: &RangeBound) -> bool {
71    match bound {
72        RangeBound::Slot(slot) => r.max_bundle_end_slot.is_none_or(|end| end <= *slot),
73        RangeBound::Time(time) => match &r.max_bundle_end_slot_utc {
74            None => true, // unbounded or unknown — keep it
75            Some(utc_str) => parse_utc(utc_str).is_none_or(|end| end <= *time),
76        },
77    }
78}
79
80fn starts_after(r: &AvailableRange, bound: &RangeBound) -> bool {
81    match bound {
82        RangeBound::Slot(slot) => r.bundle_start_slot >= *slot,
83        RangeBound::Time(time) => match &r.bundle_start_slot_utc {
84            None => true, // unknown — keep it
85            Some(utc_str) => parse_utc(utc_str).is_none_or(|start| start >= *time),
86        },
87    }
88}
89
90fn parse_utc(s: &str) -> Option<DateTime<Utc>> {
91    DateTime::parse_from_rfc3339(s)
92        .ok()
93        .map(|dt| dt.with_timezone(&Utc))
94}