Skip to main content

sel/
selector.rs

1//! Selector parsing and representation.
2
3use crate::error::{Result, SelError};
4use std::collections::BTreeSet;
5
6/// A selector specifying which lines/positions to extract.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum Selector {
9    /// Select all lines (no selector provided).
10    All,
11
12    /// Select specific line numbers and/or ranges.
13    LineNumbers(Vec<LineSpec>),
14
15    /// Select specific positions (line:column).
16    Positions(Vec<Position>),
17}
18
19/// Merge overlapping or adjacent ranges.
20fn merge_ranges(mut ranges: Vec<(usize, usize)>) -> Vec<(usize, usize)> {
21    if ranges.is_empty() {
22        return ranges;
23    }
24
25    ranges.sort_by_key(|&(start, _)| start);
26    let mut merged = Vec::new();
27    let mut current_start = ranges[0].0;
28    let mut current_end = ranges[0].1;
29
30    for &(start, end) in &ranges[1..] {
31        if start <= current_end + 1 {
32            // Overlapping or adjacent ranges - merge them
33            current_end = current_end.max(end);
34        } else {
35            merged.push((current_start, current_end));
36            current_start = start;
37            current_end = end;
38        }
39    }
40    merged.push((current_start, current_end));
41
42    merged
43}
44
45/// Merge single lines into ranges.
46///
47/// Converts single lines into Range specs where they are contiguous
48/// or adjacent to existing ranges.
49fn merge_lines_with_ranges(lines: Vec<usize>, ranges: Vec<(usize, usize)>) -> Vec<LineSpec> {
50    // Collect all "events": start and end of ranges, plus single lines
51    // For a single line n, we treat it as a range (n, n)
52    let mut all_ranges: Vec<(usize, usize)> = ranges;
53
54    // Add single lines as ranges of length 1
55    for line in lines {
56        all_ranges.push((line, line));
57    }
58
59    // Merge all ranges
60    let merged = merge_ranges(all_ranges);
61
62    // Convert back to LineSpec
63    merged
64        .into_iter()
65        .map(|(start, end)| {
66            if start == end {
67                LineSpec::Single(start)
68            } else {
69                LineSpec::Range(start, end)
70            }
71        })
72        .collect()
73}
74
75impl Selector {
76    /// Parse a selector string.
77    ///
78    /// # Examples
79    /// ```
80    /// use sel::selector::Selector;
81    ///
82    /// let sel = Selector::parse("42").unwrap();
83    /// let sel = Selector::parse("10-20").unwrap();
84    /// let sel = Selector::parse("1,5,10-15").unwrap();
85    /// let sel = Selector::parse("23:260").unwrap();
86    /// ```
87    pub fn parse(s: &str) -> Result<Self> {
88        if s.is_empty() {
89            return Ok(Selector::All);
90        }
91
92        // Check if any element contains ':' (positional selector)
93        let has_position = s.contains(':');
94
95        let parts: Vec<&str> = s.split(',').collect();
96
97        if has_position {
98            // All elements must be positional
99            let mut positions = Vec::new();
100            for part in parts {
101                let pos = Position::parse(part)?;
102                positions.push(pos);
103            }
104            Ok(Selector::Positions(positions))
105        } else {
106            // All elements are line numbers or ranges
107            let mut specs = Vec::new();
108            for part in parts {
109                let spec = LineSpec::parse(part)?;
110                specs.push(spec);
111            }
112            Ok(Selector::LineNumbers(specs))
113        }
114    }
115
116    /// Normalize the selector by merging adjacent/range specs.
117    pub fn normalize(&self) -> Self {
118        match self {
119            Selector::All => Selector::All,
120            Selector::Positions(p) => {
121                // Remove duplicates and sort
122                let unique: BTreeSet<_> = p.iter().collect();
123                Selector::Positions(unique.into_iter().cloned().collect())
124            }
125            Selector::LineNumbers(specs) => {
126                // Sort and merge overlapping ranges
127                let mut lines: Vec<usize> = Vec::new();
128                let mut ranges: Vec<(usize, usize)> = Vec::new();
129
130                for spec in specs {
131                    match spec {
132                        LineSpec::Single(n) => lines.push(*n),
133                        LineSpec::Range(start, end) => ranges.push((*start, *end)),
134                    }
135                }
136
137                lines.sort();
138                lines.dedup();
139                ranges.sort();
140
141                // Merge overlapping ranges
142                let merged_ranges = merge_ranges(ranges);
143
144                // Convert single lines to ranges and merge with existing ranges
145                let result = merge_lines_with_ranges(lines, merged_ranges);
146
147                Selector::LineNumbers(result)
148            }
149        }
150    }
151
152    /// Returns true if this is a positional selector.
153    pub fn is_positional(&self) -> bool {
154        matches!(self, Selector::Positions(_))
155    }
156}
157
158/// A line specification: either a single line or a range.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum LineSpec {
161    /// Single line number.
162    Single(usize),
163
164    /// Range of lines (inclusive).
165    Range(usize, usize),
166}
167
168impl LineSpec {
169    /// Parse a line spec string.
170    ///
171    /// # Examples
172    /// ```
173    /// # use sel::selector::LineSpec;
174    /// let spec = LineSpec::parse("42").unwrap();
175    /// let spec = LineSpec::parse("10-20").unwrap();
176    /// ```
177    pub fn parse(s: &str) -> Result<Self> {
178        if let Some((start, end)) = s.split_once('-') {
179            let start = start.parse::<usize>().map_err(|_| {
180                SelError::InvalidSelector(format!("Invalid range start: '{}'", start))
181            })?;
182            let end = end
183                .parse::<usize>()
184                .map_err(|_| SelError::InvalidSelector(format!("Invalid range end: '{}'", end)))?;
185
186            if start == 0 || end == 0 {
187                return Err(SelError::InvalidSelector(
188                    "Line numbers must be >= 1".to_string(),
189                ));
190            }
191
192            if start > end {
193                return Err(SelError::InvalidSelector(format!(
194                    "Range start ({}) > end ({})",
195                    start, end
196                )));
197            }
198
199            Ok(LineSpec::Range(start, end))
200        } else {
201            let n = s
202                .parse::<usize>()
203                .map_err(|_| SelError::InvalidSelector(format!("Invalid line number: '{}'", s)))?;
204
205            if n == 0 {
206                return Err(SelError::InvalidSelector(
207                    "Line number must be >= 1".to_string(),
208                ));
209            }
210
211            Ok(LineSpec::Single(n))
212        }
213    }
214
215    /// Check if this spec contains the given line number.
216    pub fn contains(&self, line: usize) -> bool {
217        match self {
218            LineSpec::Single(n) => *n == line,
219            LineSpec::Range(start, end) => line >= *start && line <= *end,
220        }
221    }
222
223    /// Get the starting line number.
224    pub fn start(&self) -> usize {
225        match self {
226            LineSpec::Single(n) => *n,
227            LineSpec::Range(start, _) => *start,
228        }
229    }
230}
231
232/// A position in a file (line:column).
233#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
234pub struct Position {
235    /// Line number (1-indexed).
236    pub line: usize,
237
238    /// Column number in bytes (1-indexed).
239    pub column: usize,
240}
241
242impl Position {
243    /// Create a new position.
244    pub fn new(line: usize, column: usize) -> Self {
245        Self { line, column }
246    }
247
248    /// Parse a position string.
249    ///
250    /// # Examples
251    /// ```
252    /// # use sel::selector::Position;
253    /// let pos = Position::parse("23:260").unwrap();
254    /// assert_eq!(pos.line, 23);
255    /// assert_eq!(pos.column, 260);
256    /// ```
257    pub fn parse(s: &str) -> Result<Self> {
258        let (line_str, col_str) = s.split_once(':').ok_or_else(|| {
259            SelError::InvalidSelector(format!("Invalid position format: '{}'", s))
260        })?;
261
262        let line = line_str.parse::<usize>().map_err(|_| {
263            SelError::InvalidSelector(format!("Invalid line number in position: '{}'", line_str))
264        })?;
265
266        let column = col_str.parse::<usize>().map_err(|_| {
267            SelError::InvalidSelector(format!("Invalid column number in position: '{}'", col_str))
268        })?;
269
270        if line == 0 || column == 0 {
271            return Err(SelError::InvalidSelector(
272                "Line and column numbers must be >= 1".to_string(),
273            ));
274        }
275
276        Ok(Position { line, column })
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn parse_single_line() {
286        let sel = Selector::parse("42").unwrap();
287        assert_eq!(sel, Selector::LineNumbers(vec![LineSpec::Single(42)]));
288    }
289
290    #[test]
291    fn parse_range() {
292        let sel = Selector::parse("10-20").unwrap();
293        assert_eq!(sel, Selector::LineNumbers(vec![LineSpec::Range(10, 20)]));
294    }
295
296    #[test]
297    fn parse_multiple_specs() {
298        let sel = Selector::parse("1,5,10-15,20").unwrap();
299        assert_eq!(
300            sel,
301            Selector::LineNumbers(vec![
302                LineSpec::Single(1),
303                LineSpec::Single(5),
304                LineSpec::Range(10, 15),
305                LineSpec::Single(20),
306            ])
307        );
308    }
309
310    #[test]
311    fn parse_position() {
312        let sel = Selector::parse("23:260").unwrap();
313        assert_eq!(sel, Selector::Positions(vec![Position::new(23, 260)]));
314    }
315
316    #[test]
317    fn parse_multiple_positions() {
318        let sel = Selector::parse("15:30,23:260").unwrap();
319        assert_eq!(
320            sel,
321            Selector::Positions(vec![Position::new(15, 30), Position::new(23, 260),])
322        );
323    }
324
325    #[test]
326    fn reject_mixed_selector() {
327        // This test verifies that mixing positional and non-positional is rejected
328        // However, our current implementation checks for ':' globally, so "1,23:260"
329        // would be treated as positional and fail when parsing "1"
330        let result = Selector::parse("1,23:260");
331        assert!(result.is_err());
332    }
333
334    #[test]
335    fn parse_empty_selector() {
336        let sel = Selector::parse("").unwrap();
337        assert_eq!(sel, Selector::All);
338    }
339
340    #[test]
341    fn reject_zero_line() {
342        assert!(Selector::parse("0").is_err());
343        assert!(Selector::parse("10-0").is_err());
344    }
345
346    #[test]
347    fn reject_invalid_range() {
348        assert!(Selector::parse("20-10").is_err());
349    }
350
351    #[test]
352    fn line_spec_contains() {
353        let spec = LineSpec::Range(10, 20);
354        assert!(!spec.contains(9));
355        assert!(spec.contains(10));
356        assert!(spec.contains(15));
357        assert!(spec.contains(20));
358        assert!(!spec.contains(21));
359    }
360
361    #[test]
362    fn normalize_merges_overlapping_ranges() {
363        let sel = Selector::LineNumbers(vec![LineSpec::Range(1, 5), LineSpec::Range(3, 10)]);
364        let normalized = sel.normalize();
365        assert_eq!(
366            normalized,
367            Selector::LineNumbers(vec![LineSpec::Range(1, 10)])
368        );
369    }
370
371    #[test]
372    fn normalize_merges_adjacent_ranges() {
373        let sel = Selector::LineNumbers(vec![LineSpec::Range(1, 5), LineSpec::Range(6, 10)]);
374        let normalized = sel.normalize();
375        assert_eq!(
376            normalized,
377            Selector::LineNumbers(vec![LineSpec::Range(1, 10)])
378        );
379    }
380
381    #[test]
382    fn normalize_merges_single_lines_into_ranges() {
383        let sel = Selector::LineNumbers(vec![
384            LineSpec::Single(1),
385            LineSpec::Single(2),
386            LineSpec::Single(3),
387            LineSpec::Single(10),
388        ]);
389        let normalized = sel.normalize();
390        assert_eq!(
391            normalized,
392            Selector::LineNumbers(vec![LineSpec::Range(1, 3), LineSpec::Single(10)])
393        );
394    }
395
396    #[test]
397    fn normalize_complex_merge() {
398        let sel = Selector::LineNumbers(vec![
399            LineSpec::Range(1, 5),
400            LineSpec::Single(6),
401            LineSpec::Range(7, 10),
402            LineSpec::Range(15, 20),
403            LineSpec::Single(21),
404        ]);
405        let normalized = sel.normalize();
406        assert_eq!(
407            normalized,
408            Selector::LineNumbers(vec![LineSpec::Range(1, 10), LineSpec::Range(15, 21)])
409        );
410    }
411
412    #[test]
413    fn normalize_keeps_non_adjacent_ranges_separate() {
414        let sel = Selector::LineNumbers(vec![LineSpec::Range(1, 5), LineSpec::Range(10, 15)]);
415        let normalized = sel.normalize();
416        assert_eq!(
417            normalized,
418            Selector::LineNumbers(vec![LineSpec::Range(1, 5), LineSpec::Range(10, 15)])
419        );
420    }
421
422    #[test]
423    fn normalize_removes_duplicate_lines() {
424        let sel = Selector::LineNumbers(vec![
425            LineSpec::Single(5),
426            LineSpec::Single(5),
427            LineSpec::Single(5),
428        ]);
429        let normalized = sel.normalize();
430        assert_eq!(normalized, Selector::LineNumbers(vec![LineSpec::Single(5)]));
431    }
432
433    #[test]
434    fn normalize_empty_selector() {
435        let sel = Selector::LineNumbers(vec![]);
436        let normalized = sel.normalize();
437        assert_eq!(normalized, Selector::LineNumbers(vec![]));
438    }
439}