Skip to main content

jobberdb/
range.rs

1//! Temporal or positional range of jobs within the database.
2
3use super::prelude::*;
4use regex::Regex;
5
6/// Descriptor of temporal or positional range of jobs within the database.
7#[derive(Debug, PartialEq, Clone)]
8pub enum Range {
9    /// None.
10    None,
11    /// All.
12    All,
13    /// Last N jobs .
14    Count(usize),
15    /// At given positions.
16    At(Vec<usize>),
17    /// From position to position
18    PositionRange(usize, usize),
19    /// From position to the end.
20    FromPosition(usize),
21    /// All at a specified day
22    Day(Date),
23    /// All jobs overlapping the given time range.
24    TimeRange(DateTime, DateTime),
25    /// All jobs which overlap the time since a specified time.
26    Since(DateTime),
27}
28
29impl Range {
30    /// Parse a range from a string like told in the manual.
31    pub fn parse(list: Option<String>, context: &Context) -> Result<Self, Error> {
32        if let Some(list) = list {
33            match Self::parse_count(&list).or(Self::parse_at(&list).or(Self::parse_position_range(
34                &list,
35            )
36            .or(
37                Self::parse_time_range(&list, context).or(Self::parse_day(&list, context)
38                    .or(Self::parse_from_position(&list).or(Self::parse_since(&list, context)))),
39            ))) {
40                Range::None => Err(Error::RangeFormat(list)),
41                range => Ok(range),
42            }
43        } else {
44            Ok(Range::All)
45        }
46    }
47    /// Return self or another.
48    fn or(self, other: Self) -> Self {
49        match self {
50            Self::None => other,
51            _ => self,
52        }
53    }
54    /// Parse `Count`.
55    fn parse_count(list: &str) -> Range {
56        let re = Regex::new(r"^~(\d+)$").unwrap();
57
58        if let Some(cap) = re.captures_iter(list).next() {
59            return Self::Count(cap[1].parse::<usize>().unwrap());
60        }
61        Self::None
62    }
63    /// Parse `At`.
64    fn parse_at(list: &str) -> Range {
65        let re = Regex::new(r"^([\d,]+)$").unwrap();
66        if let Some(cap) = re.captures_iter(list).next() {
67            return Self::At(
68                cap[1]
69                    .split(',')
70                    .map(|c| c.parse::<usize>().unwrap() - 1)
71                    .collect(),
72            );
73        }
74        Self::None
75    }
76    /// Parse `PositionRange`.
77    fn parse_position_range(list: &str) -> Range {
78        let re = Regex::new(r"^(\d+)-(\d+)$").unwrap();
79        if let Some(cap) = re.captures_iter(list).next() {
80            return Self::PositionRange(
81                cap[1].parse::<usize>().unwrap() - 1,
82                cap[2].parse::<usize>().unwrap() - 1,
83            );
84        }
85        Self::None
86    }
87    /// Parse `FromPosition`.
88    fn parse_from_position(list: &str) -> Range {
89        let re = Regex::new(r"^(\d+)-$").unwrap();
90        if let Some(cap) = re.captures_iter(list).next() {
91            return Self::FromPosition(cap[1].parse::<usize>().unwrap() - 1);
92        }
93        Self::None
94    }
95    /// Parse `Day`.
96    fn parse_day(list: &str, context: &Context) -> Range {
97        let pt = PartialDateTime::parse_opt(Some(list.to_string()));
98        match pt {
99            PartialDateTime::None => Self::None,
100            _ => Range::Day(Date::from(pt.into(context.time()))),
101        }
102    }
103    /// Parse `TimeRange`.
104    fn parse_time_range(list: &str, context: &Context) -> Range {
105        let list: Vec<String> = if list.contains("...") {
106            let list: Vec<&str> = list.split("...").collect();
107            vec![list[0].to_string() + ".", list[1].to_string()]
108        } else {
109            let list: Vec<&str> = list.split("..").collect();
110            match list.len() {
111                1 => vec![list[0].to_string()],
112                2 => vec![list[0].to_string(), list[1].to_string()],
113                _ => return Range::None,
114            }
115        };
116        if list.len() == 2 {
117            let from = PartialDateTime::parse_opt(Some(list[0].to_string()));
118            let to = PartialDateTime::parse_opt(Some(list[1].to_string()));
119            match (from, to) {
120                (PartialDateTime::None, PartialDateTime::None) => Self::None,
121                (from, PartialDateTime::None) => {
122                    Self::TimeRange(from.into(context.time()), context.time())
123                }
124                (PartialDateTime::None, to) => {
125                    use chrono::{TimeZone, Utc};
126                    Self::TimeRange(
127                        Utc.with_ymd_and_hms(1900, 1, 1, 0, 0, 0).unwrap().into(),
128                        to.into(context.time()) + Duration::days(1),
129                    )
130                }
131                (from, to) => {
132                    let from = from.into(context.time());
133                    Self::TimeRange(from, to.into(from) + Duration::days(1))
134                }
135            }
136        } else {
137            Self::None
138        }
139    }
140    /// Parse `Since`.
141    fn parse_since(list: &str, context: &Context) -> Range {
142        let re = Regex::new(r"^(.+)\.\.$").unwrap();
143        if let Some(cap) = re.captures_iter(list).next() {
144            let pt = PartialDateTime::parse_opt(Some(cap[1].to_string()));
145            return match pt {
146                PartialDateTime::None => Self::None,
147                _ => Range::Since(pt.into(context.time())),
148            };
149        }
150        Self::None
151    }
152}
153
154impl std::fmt::Display for Range {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::None => write!(f, "none"),
158            Self::All => write!(f, "all job(s)"),
159            Self::Count(count) => write!(f, "last {count} job(s)"),
160            Self::At(pos) => write!(
161                f,
162                "job at position(s) {pos}",
163                pos = {
164                    let v: Vec<String> = pos.iter().map(|p| p.to_string()).collect();
165                    v.join(",")
166                }
167            ),
168            Self::PositionRange(from, to) => write!(
169                f,
170                "job(s) from position {from} to {to}",
171                from = from + 1,
172                to = to + 1
173            ),
174            Self::FromPosition(from) => write!(f, "job(s) from position {from}", from = from + 1),
175            Self::Day(day) => write!(f, "job(s) at {day}"),
176            Self::TimeRange(since, until) => write!(f, "job(s) since {since} until {until}"),
177            Self::Since(since) => write!(f, "job(s) since {since}"),
178        }
179    }
180}
181
182#[cfg(test)]
183fn new_job(start: &str, end: Option<&str>, message: Option<&str>, tags: Option<&str>) -> Job {
184    Job::new(
185        start.into(),
186        end.map(|end| end.into()),
187        message.map(|message| message.to_string()),
188        tags.map(|tags| tags.into()),
189    )
190    .expect("can't parse new job")
191}
192
193/// Test several range parsings.
194#[test]
195fn test_parse_time_range() {
196    // include start and end days in time range
197    let context = Context::new_test("2023-2-1 12:00");
198
199    // prepare some jobs around 2023-1-1
200    let mut jobs = Jobs::new();
201    jobs._push(new_job(
202        "2022-12-31 18:00",
203        Some("2022-12-31 23:59"),
204        None,
205        None,
206    ));
207    jobs._push(new_job(
208        "2023-01-01 00:00",
209        Some("2023-01-01 00:10"),
210        None,
211        None,
212    ));
213
214    //
215    let january = Range::parse(Some("1.1...31.1.".into()), &context).unwrap();
216
217    assert!(jobs._filter(&january, &TagSet::new()).unwrap().len() == 1);
218
219    assert_eq!(
220        january,
221        Range::TimeRange("2023-1-1 00:00".into(), "2023-2-1 00:00".into())
222    );
223}
224
225/// Test several failing range parsings.
226#[test]
227fn test_parse_fails() {
228    // include start and end days in time range
229    let context = Context::new_test("2023-2-1 12:00");
230
231    assert!(Range::parse(Some("1.1.-".into()), &context).is_err());
232}