Skip to main content

arrs/
indices.rs

1//! Parser and resolver for the `--indices` flag.
2//!
3//! Grammar:
4//! ```text
5//! indices := expr ("," expr)*
6//! expr    := int | range
7//! range   := int? ":" int?
8//! int     := "-"? [0-9]+
9//! ```
10//!
11//! Resolution against a dataset with `rowcount` rows:
12//! - Negative `i` means `rowcount + i` (errors if still < 0).
13//! - `a:b` is inclusive on both ends.
14//! - `a:` means `a..=rowcount-1`.
15//! - `:b` means `0..=b`.
16//! - `:` means the entire dataset.
17//! - Order is preserved and duplicates are not removed.
18
19use crate::Result;
20use crate::error::Error;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum Expr {
24    Single(i64),
25    Range(Option<i64>, Option<i64>),
26}
27
28fn parse_int(s: &str) -> std::result::Result<i64, String> {
29    s.parse::<i64>()
30        .map_err(|_| format!("invalid integer '{s}'"))
31}
32
33fn parse_expr(tok: &str) -> std::result::Result<Expr, String> {
34    let tok = tok.trim();
35    if tok.is_empty() {
36        return Err("empty index expression".to_string());
37    }
38    if let Some((lhs, rhs)) = tok.split_once(':') {
39        let start = if lhs.is_empty() {
40            None
41        } else {
42            Some(parse_int(lhs)?)
43        };
44        let end = if rhs.is_empty() {
45            None
46        } else {
47            Some(parse_int(rhs)?)
48        };
49        Ok(Expr::Range(start, end))
50    } else {
51        Ok(Expr::Single(parse_int(tok)?))
52    }
53}
54
55fn parse(raw: &str) -> std::result::Result<Vec<Expr>, String> {
56    if raw.trim().is_empty() {
57        return Err("--indices must not be empty".to_string());
58    }
59    raw.split(',').map(parse_expr).collect()
60}
61
62fn resolve_single(i: i64, rowcount: u64) -> Result<u64> {
63    let rc_i: i64 =
64        i64::try_from(rowcount).map_err(|_| Error::IndexOutOfRange { index: i, rowcount })?;
65    let resolved = if i < 0 { rc_i + i } else { i };
66    if resolved < 0 || resolved >= rc_i {
67        return Err(Error::IndexOutOfRange { index: i, rowcount });
68    }
69    Ok(resolved as u64)
70}
71
72/// Parse `--indices` and expand against the dataset row count.
73pub fn resolve(raw: &str, rowcount: u64) -> Result<Vec<u64>> {
74    let exprs = parse(raw).map_err(Error::IndexParse)?;
75    let rc_i: i64 =
76        i64::try_from(rowcount).map_err(|_| Error::IndexOutOfRange { index: 0, rowcount })?;
77    let mut out = Vec::new();
78    for expr in exprs {
79        match expr {
80            Expr::Single(i) => {
81                if rowcount == 0 {
82                    return Err(Error::IndexOutOfRange { index: i, rowcount });
83                }
84                out.push(resolve_single(i, rowcount)?);
85            }
86            Expr::Range(start, end) => {
87                if rowcount == 0 {
88                    return Err(Error::IndexOutOfRange {
89                        index: start.or(end).unwrap_or(0),
90                        rowcount,
91                    });
92                }
93                let start_raw = start.unwrap_or(0);
94                let end_raw = end.unwrap_or(rc_i - 1);
95                let s = resolve_single(start_raw, rowcount)?;
96                let e = resolve_single(end_raw, rowcount)?;
97                if s > e {
98                    return Err(Error::EmptyRange {
99                        start: start_raw,
100                        end: end_raw,
101                    });
102                }
103                out.extend(s..=e);
104            }
105        }
106    }
107    Ok(out)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn single_positive() {
116        assert_eq!(resolve("5", 10).unwrap(), vec![5]);
117    }
118
119    #[test]
120    fn single_negative() {
121        assert_eq!(resolve("-1", 10).unwrap(), vec![9]);
122        assert_eq!(resolve("-10", 10).unwrap(), vec![0]);
123    }
124
125    #[test]
126    fn range_closed() {
127        assert_eq!(resolve("2:5", 10).unwrap(), vec![2, 3, 4, 5]);
128    }
129
130    #[test]
131    fn range_open_start() {
132        assert_eq!(resolve(":3", 10).unwrap(), vec![0, 1, 2, 3]);
133    }
134
135    #[test]
136    fn range_open_end() {
137        assert_eq!(resolve("7:", 10).unwrap(), vec![7, 8, 9]);
138    }
139
140    #[test]
141    fn range_full() {
142        assert_eq!(resolve(":", 4).unwrap(), vec![0, 1, 2, 3]);
143    }
144
145    #[test]
146    fn range_negative_to_negative() {
147        // Last 5 of 10 rows = indices 5..=9
148        assert_eq!(resolve("-5:-1", 10).unwrap(), vec![5, 6, 7, 8, 9]);
149    }
150
151    #[test]
152    fn range_negative_to_positive() {
153        assert_eq!(resolve("-5:9", 10).unwrap(), vec![5, 6, 7, 8, 9]);
154    }
155
156    #[test]
157    fn order_preserved_and_dupes_kept() {
158        assert_eq!(resolve("3,1,1,0:2", 5).unwrap(), vec![3, 1, 1, 0, 1, 2]);
159    }
160
161    #[test]
162    fn out_of_range_positive() {
163        assert!(matches!(
164            resolve("10", 10),
165            Err(Error::IndexOutOfRange { .. })
166        ));
167    }
168
169    #[test]
170    fn out_of_range_negative() {
171        assert!(matches!(
172            resolve("-11", 10),
173            Err(Error::IndexOutOfRange { .. })
174        ));
175    }
176
177    #[test]
178    fn empty_range_error() {
179        assert!(matches!(resolve("5:2", 10), Err(Error::EmptyRange { .. })));
180    }
181
182    #[test]
183    fn invalid_int_error() {
184        assert!(matches!(resolve("abc", 10), Err(Error::IndexParse(_))));
185    }
186
187    #[test]
188    fn empty_input_error() {
189        assert!(matches!(resolve("", 10), Err(Error::IndexParse(_))));
190    }
191
192    #[test]
193    fn empty_dataset_single_error() {
194        assert!(matches!(
195            resolve("0", 0),
196            Err(Error::IndexOutOfRange { .. })
197        ));
198    }
199}