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 only those that overlap the `[after, before]` window.
47///
48/// When a bound is a [`RangeBound::Time`], the range's UTC timestamp fields are used;
49/// ranges that have no UTC timestamp for the relevant boundary are kept (conservative).
50/// When a bound is a [`RangeBound::Slot`], slot fields are used directly.
51pub fn filter_ranges(
52    ranges: &[AvailableRange],
53    after: Option<&RangeBound>,
54    before: Option<&RangeBound>,
55) -> Vec<AvailableRange> {
56    let mut out = ranges.to_vec();
57
58    if let Some(after) = after {
59        out.retain(|r| range_ends_after_or_is_unbounded(r, after));
60    }
61    if let Some(before) = before {
62        out.retain(|r| range_starts_before(r, before));
63    }
64
65    out
66}
67
68fn range_ends_after_or_is_unbounded(r: &AvailableRange, after: &RangeBound) -> bool {
69    match after {
70        RangeBound::Slot(slot) => r.max_bundle_end_slot.is_none_or(|end| end >= *slot),
71        RangeBound::Time(time) => match &r.max_bundle_end_slot_utc {
72            None => true, // unbounded or unknown — keep it
73            Some(utc_str) => parse_utc(utc_str).is_none_or(|end| end >= *time),
74        },
75    }
76}
77
78fn range_starts_before(r: &AvailableRange, before: &RangeBound) -> bool {
79    match before {
80        RangeBound::Slot(slot) => r.bundle_start_slot <= *slot,
81        RangeBound::Time(time) => match &r.bundle_start_slot_utc {
82            None => true, // unknown — keep it
83            Some(utc_str) => parse_utc(utc_str).is_none_or(|start| start <= *time),
84        },
85    }
86}
87
88fn parse_utc(s: &str) -> Option<DateTime<Utc>> {
89    DateTime::parse_from_rfc3339(s)
90        .ok()
91        .map(|dt| dt.with_timezone(&Utc))
92}