Skip to main content

consortium/
range_set.rs

1//! Numeric range sets with folding and autostep support.
2//!
3//! This module is the Rust implementation of `ClusterShell.RangeSet`.
4//!
5//! A [`RangeSet`] manages a set of non-negative integers as a union of
6//! string-represented values, with optional step detection (autostep) for
7//! compact string representation (e.g. `"0-8/2"`).
8//!
9//! Internally, like Python's ClusterShell, the set stores string representations
10//! of integers, preserving zero-padding per element (e.g. "001", "02", "3").
11
12use std::collections::BTreeSet;
13use std::fmt;
14use thiserror::Error;
15
16/// Special constant used to disable autostep feature.
17pub const AUTOSTEP_DISABLED: f64 = 1e100;
18
19// ---------------------------------------------------------------------------
20// Errors
21// ---------------------------------------------------------------------------
22
23#[derive(Debug, Error)]
24pub enum RangeSetError {
25    #[error("parse error: {msg} (in \"{part}\")")]
26    ParseError { part: String, msg: String },
27
28    #[error("padding mismatch: {msg} (in \"{part}\")")]
29    PaddingError { part: String, msg: String },
30}
31
32pub type Result<T> = std::result::Result<T, RangeSetError>;
33
34// ---------------------------------------------------------------------------
35// Sorting key
36// ---------------------------------------------------------------------------
37
38/// Sort key matching Python's RangeSet._sorted():
39/// - Negative numbers (starting with '-'): sort by (-len, int_value)
40/// - Non-negative: sort by (len, string_value)
41fn sort_key(s: &str) -> (i64, i64, String) {
42    if s.starts_with('-') {
43        let ival: i64 = s.parse().unwrap_or(0);
44        (-(s.len() as i64), ival, String::new())
45    } else {
46        (s.len() as i64, 0, s.to_string())
47    }
48}
49
50fn sorted_elements(elements: &BTreeSet<String>) -> Vec<String> {
51    let mut v: Vec<String> = elements.iter().cloned().collect();
52    v.sort_by(|a, b| sort_key(a).cmp(&sort_key(b)));
53    v
54}
55
56/// Normalize `start`/`stop` search-window bounds like Python's
57/// `_normalized_index_bounds()` (list.index() semantics: negative values
58/// count from the end).
59fn normalized_index_bounds(length: usize, start: isize, stop: Option<isize>) -> (usize, usize) {
60    let start = if start < 0 {
61        (length as isize + start).max(0) as usize
62    } else {
63        start as usize
64    };
65    let stop = match stop {
66        None => length,
67        Some(s) if s < 0 => (length as isize + s).max(0) as usize,
68        Some(s) => s as usize,
69    };
70    (start, stop)
71}
72
73// ---------------------------------------------------------------------------
74// Slice representation (used internally for display)
75// ---------------------------------------------------------------------------
76
77#[derive(Debug, Clone)]
78struct RangeSlice {
79    start: i64,
80    stop: i64, // exclusive
81    step: i64,
82    pad: usize,
83}
84
85// ---------------------------------------------------------------------------
86// RangeSet
87// ---------------------------------------------------------------------------
88
89/// An ordered set of integer string representations, supporting cluster
90/// range notation like `"1-10,15,20-30/2"`.
91#[derive(Debug, Clone)]
92pub struct RangeSet {
93    /// Internal storage: string representations of integers, preserving padding.
94    elements: BTreeSet<String>,
95    /// Autostep threshold (internal representation = user_value - 1).
96    /// When >= AUTOSTEP_DISABLED, stepping is off.
97    _autostep: f64,
98}
99
100impl RangeSet {
101    /// Create an empty `RangeSet`.
102    pub fn new() -> Self {
103        Self {
104            elements: BTreeSet::new(),
105            _autostep: AUTOSTEP_DISABLED,
106        }
107    }
108
109    /// Parse a range set from a string pattern like `"1-4,6,8-12/2"`.
110    ///
111    /// `autostep` is the user-facing autostep threshold (minimum element count
112    /// to trigger step folding). Pass `None` to disable.
113    pub fn parse(pattern: &str, autostep: Option<u32>) -> Result<Self> {
114        let mut rs = Self::new();
115        rs.set_autostep(autostep);
116        rs._parse(pattern)?;
117        Ok(rs)
118    }
119
120    /// Construct from an optional pattern, mirroring Python's
121    /// `RangeSet(pattern_or_None)`: `None` yields an empty set, while
122    /// `Some("")` is an error by design, exactly like `RangeSet("")`.
123    ///
124    /// This is the round-trip-safe reconstruction path for the empty set,
125    /// porting the upstream pickling fix (ClusterShell commit 9df4c5a):
126    /// Python's `RangeSet.__reduce__` stores `None` instead of `""` when the
127    /// set is empty, so unpickling an empty RangeSet works again. In Rust
128    /// terms, serializing an empty set must produce `None` (not
129    /// `Some(String::new())`), and deserializing through this constructor
130    /// round-trips correctly.
131    pub fn from_optional_pattern(pattern: Option<&str>, autostep: Option<u32>) -> Result<Self> {
132        match pattern {
133            None => {
134                let mut rs = Self::new();
135                rs.set_autostep(autostep);
136                Ok(rs)
137            }
138            Some(p) => Self::parse(p, autostep),
139        }
140    }
141
142    /// Internal parse: comma-separated subranges.
143    fn _parse(&mut self, pattern: &str) -> Result<()> {
144        for subrange in pattern.split(',') {
145            let subrange = subrange.trim();
146
147            let (baserange, step) = if subrange.contains('/') {
148                let parts: Vec<&str> = subrange.splitn(2, '/').collect();
149                let step_str = parts[1];
150                let step: i64 = step_str.parse().map_err(|_| RangeSetError::ParseError {
151                    part: subrange.to_string(),
152                    msg: "cannot convert string to integer".to_string(),
153                })?;
154                (parts[0], step)
155            } else {
156                (subrange, 1i64)
157            };
158
159            // Parse begin and end
160            let (begin_str, end_str, _begin_sign, _end_sign) =
161                self.parse_range_part(baserange, subrange, step)?;
162
163            // Compute padding
164            let (start, stop, pad) = self.compute_padding(&begin_str, &end_str, subrange)?;
165
166            // Validate
167            if stop > 1e100 as i64 || start > stop || step < 1 {
168                return Err(RangeSetError::ParseError {
169                    part: subrange.to_string(),
170                    msg: "invalid values in range".to_string(),
171                });
172            }
173
174            self.add_range(start, stop + 1, step, pad as u32);
175        }
176        Ok(())
177    }
178
179    /// Parse the begin-end part of a range, handling negative numbers.
180    fn parse_range_part<'a>(
181        &self,
182        baserange: &'a str,
183        subrange: &str,
184        step: i64,
185    ) -> Result<(String, String, i64, i64)> {
186        if !baserange.contains('-') {
187            // Single value
188            if step != 1 {
189                return Err(RangeSetError::ParseError {
190                    part: subrange.to_string(),
191                    msg: "invalid step usage".to_string(),
192                });
193            }
194            return Ok((baserange.to_string(), baserange.to_string(), 1, 1));
195        }
196
197        // Has a dash — could be a range or a negative number
198        let parts: Vec<&str> = baserange.split('-').collect();
199        match parts.len() {
200            2 => {
201                // "begin-end"
202                let begin = parts[0].trim().to_string();
203                let end = parts[1].trim().to_string();
204                if begin.is_empty() {
205                    // "-5" → single negative number
206                    if end.is_empty() {
207                        return Err(RangeSetError::ParseError {
208                            part: subrange.to_string(),
209                            msg: "cannot convert string to integer".to_string(),
210                        });
211                    }
212                    return Ok((end.clone(), end, -1, -1));
213                }
214                Ok((begin, end, 1, 1))
215            }
216            3 => {
217                // "-begin-end" (negative start)
218                let begin = parts[1].trim().to_string();
219                let end = parts[2].trim().to_string();
220                Ok((begin, end, -1, 1))
221            }
222            4 => {
223                // "-begin--end" (both negative)
224                let begin = parts[1].trim().to_string();
225                let end = parts[3].trim().to_string();
226                Ok((begin, end, -1, -1))
227            }
228            _ => Err(RangeSetError::ParseError {
229                part: subrange.to_string(),
230                msg: "cannot convert string to integer".to_string(),
231            }),
232        }
233    }
234
235    /// Compute padding and numeric start/stop from begin/end strings.
236    fn compute_padding(&self, begin: &str, end: &str, subrange: &str) -> Result<(i64, i64, usize)> {
237        let mut pad: usize = 0;
238
239        // Parse begin
240        let begin_int: i64 = begin.parse().map_err(|_| {
241            if subrange.is_empty() {
242                RangeSetError::ParseError {
243                    part: subrange.to_string(),
244                    msg: "empty range".to_string(),
245                }
246            } else {
247                RangeSetError::ParseError {
248                    part: subrange.to_string(),
249                    msg: "cannot convert string to integer".to_string(),
250                }
251            }
252        })?;
253
254        if begin_int != 0 {
255            let begins = begin.trim_start_matches('0');
256            if begin.len() - begins.len() > 0 {
257                pad = begin.len();
258            }
259        } else if begin.len() > 1 {
260            pad = begin.len();
261        }
262
263        // Parse end
264        let end_trimmed = if end.parse::<i64>().unwrap_or(1) != 0 {
265            end.trim_start_matches('0')
266        } else {
267            end
268        };
269
270        let mut endpad: usize = 0;
271        if end.len() - end_trimmed.len() > 0 {
272            endpad = end.len();
273        }
274
275        // Check padding consistency
276        if (pad > 0 || endpad > 0) && begin.len() != end.len() {
277            return Err(RangeSetError::ParseError {
278                part: subrange.to_string(),
279                msg: "padding length mismatch".to_string(),
280            });
281        }
282
283        let end_int: i64 = end_trimmed.parse().map_err(|_| RangeSetError::ParseError {
284            part: subrange.to_string(),
285            msg: "cannot convert string to integer".to_string(),
286        })?;
287
288        Ok((begin_int, end_int, pad))
289    }
290
291    /// Add a range of integers [start, stop) with given step and padding.
292    pub fn add_range(&mut self, start: i64, stop: i64, step: i64, pad: u32) {
293        assert!(start < stop, "please provide ordered node index ranges");
294        assert!(step > 0);
295        assert!(stop - start < 1_000_000_000, "range too large");
296
297        let mut i = start;
298        while i < stop {
299            if pad == 0 {
300                self.elements.insert(format!("{}", i));
301            } else {
302                self.elements
303                    .insert(format!("{:0>width$}", i, width = pad as usize));
304            }
305            i += step;
306        }
307    }
308
309    /// Number of individual values in the set.
310    pub fn len(&self) -> usize {
311        self.elements.len()
312    }
313
314    /// Whether the set is empty.
315    pub fn is_empty(&self) -> bool {
316        self.elements.is_empty()
317    }
318
319    /// Add a string element directly.
320    pub fn add_str(&mut self, s: &str) {
321        self.elements.insert(s.to_string());
322    }
323
324    /// Add an integer element with optional zero-padding.
325    pub fn add_int(&mut self, value: i64, pad: u32) {
326        if pad == 0 {
327            self.elements.insert(format!("{}", value));
328        } else {
329            self.elements
330                .insert(format!("{:0>width$}", value, width = pad as usize));
331        }
332    }
333
334    /// Remove an element by string. Panics if not present.
335    pub fn remove_str(&mut self, s: &str) {
336        if !self.elements.remove(s) {
337            panic!("element '{}' not found in RangeSet", s);
338        }
339    }
340
341    /// Remove an element by string if present (no error).
342    pub fn discard_str(&mut self, s: &str) {
343        self.elements.remove(s);
344    }
345
346    /// Check if a string element is in the set.
347    pub fn contains_str(&self, s: &str) -> bool {
348        self.elements.contains(s)
349    }
350
351    /// Check if an integer (unpadded) is in the set.
352    pub fn contains_int(&self, value: i64) -> bool {
353        self.elements.contains(&format!("{}", value))
354    }
355
356    /// Return the zero-based position of `value` in sorted order, or `None`
357    /// if the element is absent.
358    ///
359    /// Ports Python's `RangeSet.index()` (ClusterShell commit 484d230), the
360    /// reverse operation of `RangeSet.__getitem__`, behaving like
361    /// `list.index()` (absent element -> `None` here, `ValueError` there).
362    ///
363    /// Like the Python integer form, the value is looked up as its plain
364    /// decimal string, so zero-padding is significant: `9` is not a member
365    /// of `RangeSet("08-10")` and yields `None`. Negative values cannot be
366    /// expressed through this `u64` signature.
367    pub fn index(&self, value: u64) -> Option<usize> {
368        self.index_str(&value.to_string())
369    }
370
371    /// String form of [`RangeSet::index`], matching Python's string
372    /// argument form where zero-padding is significant.
373    pub fn index_str(&self, key: &str) -> Option<usize> {
374        self.index_str_within(key, 0, None)
375    }
376
377    /// [`RangeSet::index_str`] restricted to a search window, mirroring
378    /// Python's optional `start`/`stop` arguments of `RangeSet.index()`
379    /// (list.index() semantics; negative bounds count from the end).
380    /// Returns `None` when the element is absent or its position falls
381    /// outside the window.
382    pub fn index_str_within(&self, key: &str, start: isize, stop: Option<isize>) -> Option<usize> {
383        // Set membership is fast and padding-aware.
384        if !self.elements.contains(key) {
385            return None;
386        }
387        // Compute the rank in a single O(n) pass (no full sort) by counting
388        // the elements that sort before `key`, like the Python oracle.
389        let tkey = sort_key(key);
390        let found = self
391            .elements
392            .iter()
393            .filter(|elem| sort_key(elem) < tkey)
394            .count();
395        if start != 0 || stop.is_some() {
396            let (start, stop) = normalized_index_bounds(self.len(), start, stop);
397            if !(start <= found && found < stop) {
398                return None;
399            }
400        }
401        Some(found)
402    }
403
404    /// Get sorted elements as owned strings.
405    pub fn sorted(&self) -> Vec<String> {
406        sorted_elements(&self.elements)
407    }
408
409    /// Iterate as strings (owned).
410    pub fn striter(&self) -> impl Iterator<Item = String> {
411        sorted_elements(&self.elements).into_iter()
412    }
413
414    /// Iterate as integer values.
415    pub fn intiter(&self) -> impl Iterator<Item = i64> {
416        sorted_elements(&self.elements)
417            .into_iter()
418            .map(|s| s.parse::<i64>().unwrap_or(0))
419    }
420
421    // -----------------------------------------------------------------------
422    // Set operations
423    // -----------------------------------------------------------------------
424
425    /// Return the union of two range sets.
426    pub fn union(&self, other: &RangeSet) -> RangeSet {
427        let mut result = self.clone();
428        result.update(other);
429        result
430    }
431
432    /// Return the intersection of two range sets.
433    pub fn intersection(&self, other: &RangeSet) -> RangeSet {
434        let mut result = self.clone();
435        result.intersection_update(other);
436        result
437    }
438
439    /// Return the difference (self - other).
440    pub fn difference(&self, other: &RangeSet) -> RangeSet {
441        let mut result = self.clone();
442        result.difference_update(other);
443        result
444    }
445
446    /// Return the symmetric difference.
447    pub fn symmetric_difference(&self, other: &RangeSet) -> RangeSet {
448        let mut result = self.clone();
449        result.symmetric_difference_update(other);
450        result
451    }
452
453    /// Update self to be the union with other.
454    pub fn update(&mut self, other: &RangeSet) {
455        for elem in &other.elements {
456            self.elements.insert(elem.clone());
457        }
458    }
459
460    /// Update self to be the intersection with other.
461    pub fn intersection_update(&mut self, other: &RangeSet) {
462        self.elements = self
463            .elements
464            .intersection(&other.elements)
465            .cloned()
466            .collect();
467    }
468
469    /// Update self to be self - other.
470    pub fn difference_update(&mut self, other: &RangeSet) {
471        self.elements = self.elements.difference(&other.elements).cloned().collect();
472    }
473
474    /// Update self to be the symmetric difference with other.
475    pub fn symmetric_difference_update(&mut self, other: &RangeSet) {
476        self.elements = self
477            .elements
478            .symmetric_difference(&other.elements)
479            .cloned()
480            .collect();
481    }
482
483    // -----------------------------------------------------------------------
484    // Autostep
485    // -----------------------------------------------------------------------
486
487    /// Get autostep value (user-facing: minimum node count, or None if disabled).
488    pub fn autostep(&self) -> Option<u32> {
489        if self._autostep >= AUTOSTEP_DISABLED {
490            None
491        } else {
492            Some((self._autostep as u32) + 1)
493        }
494    }
495
496    /// Set autostep value (user-facing: minimum node count, or None to disable).
497    pub fn set_autostep(&mut self, val: Option<u32>) {
498        match val {
499            None => self._autostep = AUTOSTEP_DISABLED,
500            Some(v) => self._autostep = (v as f64) - 1.0,
501        }
502    }
503
504    // -----------------------------------------------------------------------
505    // Display internals: _slices_padding, _strslices
506    // -----------------------------------------------------------------------
507
508    /// Iterator over (RangeSlice, padding) pairs, implementing Python's
509    /// `_slices_padding(autostep)`.
510    fn slices_padding(&self, autostep: f64) -> Vec<RangeSlice> {
511        let sorted = sorted_elements(&self.elements);
512        if sorted.is_empty() {
513            return Vec::new();
514        }
515
516        let mut result: Vec<RangeSlice> = Vec::new();
517
518        let mut cur_pad: usize = 0;
519        let mut cur_padded: bool = false;
520        let mut cur_start: Option<i64> = None;
521        let mut cur_step: Option<i64> = None;
522        let mut last_idx: i64 = 0;
523
524        for si in &sorted {
525            let idx: i64 = si.parse().unwrap_or(0);
526            let digitlen = si.len();
527            let padded = digitlen > 1 && si.starts_with('0');
528
529            if let Some(cs) = cur_start {
530                // Check for padding mismatch
531                let padding_mismatch = if cur_padded {
532                    digitlen != cur_pad
533                } else {
534                    padded
535                };
536
537                // Check for step mismatch
538                let step_mismatch = if let Some(cstep) = cur_step {
539                    cstep != idx - last_idx
540                } else {
541                    false
542                };
543
544                if padding_mismatch || step_mismatch {
545                    let (stepped, step) = if let Some(cstep) = cur_step {
546                        let stepped =
547                            (cstep == 1) || ((last_idx - cs) as f64 >= autostep * cstep as f64);
548                        (stepped, cstep)
549                    } else {
550                        (true, 1i64)
551                    };
552
553                    if stepped {
554                        result.push(RangeSlice {
555                            start: cs,
556                            stop: last_idx + 1,
557                            step,
558                            pad: if cur_padded { cur_pad } else { 0 },
559                        });
560                        cur_start = Some(idx);
561                        cur_padded = padded;
562                        cur_pad = digitlen;
563                    } else {
564                        if padding_mismatch {
565                            let stop = last_idx + 1;
566                            let mut j = cs;
567                            while j < stop {
568                                result.push(RangeSlice {
569                                    start: j,
570                                    stop: j + 1,
571                                    step: 1,
572                                    pad: if cur_padded { cur_pad } else { 0 },
573                                });
574                                j += step;
575                            }
576                            cur_start = Some(idx);
577                            cur_padded = padded;
578                            cur_pad = digitlen;
579                        } else {
580                            let stop = last_idx - step + 1;
581                            let mut j = cs;
582                            while j < stop {
583                                result.push(RangeSlice {
584                                    start: j,
585                                    stop: j + 1,
586                                    step: 1,
587                                    pad: if cur_padded { cur_pad } else { 0 },
588                                });
589                                j += step;
590                            }
591                            cur_start = Some(last_idx);
592                        }
593                    }
594
595                    cur_step = if step_mismatch {
596                        Some(idx - last_idx)
597                    } else {
598                        None
599                    };
600                    last_idx = idx;
601                    continue;
602                }
603            } else {
604                // First element
605                cur_padded = padded;
606                cur_pad = digitlen;
607                cur_start = Some(idx);
608                cur_step = None;
609                last_idx = idx;
610                continue;
611            }
612
613            cur_step = Some(idx - last_idx);
614            last_idx = idx;
615        }
616
617        // Flush remaining
618        if let Some(cs) = cur_start {
619            if let Some(cstep) = cur_step {
620                let stepped = (last_idx - cs) as f64 >= self._autostep * cstep as f64;
621                if stepped || cstep == 1 {
622                    result.push(RangeSlice {
623                        start: cs,
624                        stop: last_idx + 1,
625                        step: cstep,
626                        pad: if cur_padded { cur_pad } else { 0 },
627                    });
628                } else {
629                    let mut j = cs;
630                    while j <= last_idx {
631                        result.push(RangeSlice {
632                            start: j,
633                            stop: j + 1,
634                            step: 1,
635                            pad: if cur_padded { cur_pad } else { 0 },
636                        });
637                        j += cstep;
638                    }
639                }
640            } else {
641                // Single element
642                result.push(RangeSlice {
643                    start: cs,
644                    stop: last_idx + 1,
645                    step: 1,
646                    pad: if cur_padded { cur_pad } else { 0 },
647                });
648            }
649        }
650
651        result
652    }
653
654    /// Folded slices using the instance's autostep setting.
655    fn folded_slices(&self) -> Vec<RangeSlice> {
656        self.slices_padding(self._autostep)
657    }
658
659    /// Format slices as strings.
660    fn strslices(&self) -> Vec<String> {
661        self.folded_slices()
662            .iter()
663            .map(|sli| {
664                if sli.start + 1 == sli.stop {
665                    // Single element
666                    if sli.pad > 0 {
667                        format!("{:0>width$}", sli.start, width = sli.pad)
668                    } else {
669                        format!("{}", sli.start)
670                    }
671                } else if sli.step == 1 {
672                    // Contiguous range
673                    if sli.pad > 0 {
674                        format!(
675                            "{:0>width$}-{:0>width$}",
676                            sli.start,
677                            sli.stop - 1,
678                            width = sli.pad
679                        )
680                    } else {
681                        format!("{}-{}", sli.start, sli.stop - 1)
682                    }
683                } else {
684                    // Stepped range
685                    if sli.pad > 0 {
686                        format!(
687                            "{:0>width$}-{:0>width$}/{}",
688                            sli.start,
689                            sli.stop - 1,
690                            sli.step,
691                            width = sli.pad
692                        )
693                    } else {
694                        format!("{}-{}/{}", sli.start, sli.stop - 1, sli.step)
695                    }
696                }
697            })
698            .collect()
699    }
700
701    /// Get the maximum padding value in the set.
702    pub fn padding(&self) -> Option<usize> {
703        let mut result = None;
704        for si in sorted_elements(&self.elements) {
705            let digitlen = si.len();
706            if digitlen > 1 && si.starts_with('0') {
707                result = Some(digitlen);
708            }
709        }
710        result
711    }
712}
713
714impl Default for RangeSet {
715    fn default() -> Self {
716        Self::new()
717    }
718}
719
720impl PartialEq for RangeSet {
721    /// Equality follows Python's set semantics (used by `RangeSet.__eq__`):
722    /// only the contained elements are compared, not the autostep setting.
723    fn eq(&self, other: &Self) -> bool {
724        self.elements == other.elements
725    }
726}
727
728impl fmt::Display for RangeSet {
729    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
730        write!(f, "{}", self.strslices().join(","))
731    }
732}
733
734// ---------------------------------------------------------------------------
735// RangeSetND (N-dimensional) — partial port (no automatic nD folding yet)
736// ---------------------------------------------------------------------------
737
738/// N-dimensional range set, used internally by NodeSet for multi-range patterns.
739///
740/// NOTE: unlike Python's `RangeSetND`, no automatic n-dimensional folding
741/// (merging/sorting of overlapping vectors, i.e. `precond_fold`) is performed
742/// yet. Iteration and [`RangeSetND::index`] follow the stored vector order,
743/// so callers wanting Python-identical positions must store vectors already
744/// disjoint, merged, and ordered the way Python's fold would leave them.
745#[derive(Debug, Clone)]
746pub struct RangeSetND {
747    pub vecrangesets: Vec<Vec<RangeSet>>,
748    _autostep: f64,
749}
750
751/// Lazy Cartesian-product iterator over the sorted elements of each
752/// dimension of one RangeSet vector. The last dimension varies fastest,
753/// matching Python's `itertools.product`.
754struct VecProductIter {
755    dims: Vec<Vec<String>>,
756    counter: Vec<usize>,
757    done: bool,
758}
759
760impl VecProductIter {
761    fn new(dims: Vec<Vec<String>>) -> Self {
762        let done = dims.iter().any(|d| d.is_empty());
763        Self {
764            counter: vec![0; dims.len()],
765            dims,
766            done,
767        }
768    }
769}
770
771impl Iterator for VecProductIter {
772    type Item = Vec<String>;
773
774    fn next(&mut self) -> Option<Self::Item> {
775        if self.done {
776            return None;
777        }
778        let item: Vec<String> = self
779            .dims
780            .iter()
781            .zip(&self.counter)
782            .map(|(d, &i)| d[i].clone())
783            .collect();
784        // Advance the mixed-radix counter, last dimension fastest.
785        let mut pos = self.counter.len();
786        loop {
787            if pos == 0 {
788                self.done = true;
789                break;
790            }
791            pos -= 1;
792            self.counter[pos] += 1;
793            if self.counter[pos] < self.dims[pos].len() {
794                break;
795            }
796            self.counter[pos] = 0;
797        }
798        Some(item)
799    }
800}
801
802impl RangeSetND {
803    pub fn new(autostep: Option<u32>) -> Self {
804        Self {
805            vecrangesets: Vec::new(),
806            _autostep: autostep.map(|v| v as f64).unwrap_or(AUTOSTEP_DISABLED),
807        }
808    }
809
810    /// Build from a vector of RangeSet vectors, mirroring Python's
811    /// `RangeSetND([[rs1, rs2, ...], ...])`.
812    pub fn from_rangesets(vecrangesets: Vec<Vec<RangeSet>>, autostep: Option<u32>) -> Self {
813        Self {
814            vecrangesets,
815            _autostep: autostep.map(|v| v as f64).unwrap_or(AUTOSTEP_DISABLED),
816        }
817    }
818
819    /// Count elements as the sum of each vector's Cartesian-product size.
820    ///
821    /// Without nD folding this counts duplicate index vectors across
822    /// overlapping stored vectors multiple times; Python's `__len__` folds
823    /// first and counts unique vectors only.
824    pub fn len(&self) -> usize {
825        self.vecrangesets
826            .iter()
827            .map(|rgvec| rgvec.iter().map(RangeSet::len).product::<usize>())
828            .sum()
829    }
830
831    /// Whether the set contains no element.
832    pub fn is_empty(&self) -> bool {
833        self.len() == 0
834    }
835
836    /// Iterate over all index vectors (as zero-padded strings) in iteration
837    /// order: stored vectors in order, and within each vector the Cartesian
838    /// product of its sorted RangeSets with the last dimension varying
839    /// fastest (Python's `RangeSetND._iter()`).
840    pub fn iter(&self) -> impl Iterator<Item = Vec<String>> + '_ {
841        self.vecrangesets
842            .iter()
843            .flat_map(|rgvec| VecProductIter::new(rgvec.iter().map(RangeSet::sorted).collect()))
844    }
845
846    /// Return the zero-based position of an index vector in iteration order,
847    /// or `None` if absent (Python raises `ValueError`).
848    ///
849    /// Ports Python's `RangeSetND.index()` (ClusterShell commit 484d230),
850    /// the reverse operation of `RangeSetND.__getitem__`. Elements may be
851    /// strings (zero-padding is then significant) or integers; anything
852    /// `ToString` covers mirrors Python's `"%s" % e` conversion. Python's
853    /// `TypeError` for bare strings/scalars is a compile-time impossibility
854    /// here since a slice of indexes is required.
855    pub fn index<T: ToString>(&self, elem: &[T]) -> Option<usize> {
856        self.index_within(elem, 0, None)
857    }
858
859    /// [`RangeSetND::index`] restricted to a search window, mirroring
860    /// Python's optional `start`/`stop` arguments (list.index() semantics;
861    /// negative bounds count from the end).
862    pub fn index_within<T: ToString>(
863        &self,
864        elem: &[T],
865        start: isize,
866        stop: Option<isize>,
867    ) -> Option<usize> {
868        let target: Vec<String> = elem.iter().map(|e| e.to_string()).collect();
869        for (pos, ivec) in self.iter().enumerate() {
870            if ivec == target {
871                if start != 0 || stop.is_some() {
872                    let (start, stop) = normalized_index_bounds(self.len(), start, stop);
873                    if !(start <= pos && pos < stop) {
874                        // First occurrence is outside the window: Python
875                        // breaks out of the scan and raises ValueError.
876                        break;
877                    }
878                }
879                return Some(pos);
880            }
881        }
882        None
883    }
884}
885
886// ---------------------------------------------------------------------------
887// Tests
888// ---------------------------------------------------------------------------
889
890#[cfg(test)]
891mod tests {
892    use super::*;
893
894    fn test_rs(input: &str, expected: &str, expected_len: usize) {
895        let rs = RangeSet::parse(input, Some(3)).unwrap();
896        assert_eq!(
897            rs.to_string(),
898            expected,
899            "parse '{}' display mismatch",
900            input
901        );
902        assert_eq!(rs.len(), expected_len, "parse '{}' len mismatch", input);
903    }
904
905    #[test]
906    fn test_simple() {
907        test_rs("0", "0", 1);
908        test_rs("1", "1", 1);
909        test_rs("0-2", "0-2", 3);
910        test_rs("1-3", "1-3", 3);
911        test_rs("1-3,4-6", "1-6", 6);
912        test_rs("1-3,4-6,7-10", "1-10", 10);
913    }
914
915    #[test]
916    fn test_step_simple() {
917        test_rs("0-4/2", "0-4/2", 3);
918        test_rs("1-4/2", "1,3", 2);
919        test_rs("1-4/3", "1,4", 2);
920        test_rs("1-4/4", "1", 1);
921    }
922
923    #[test]
924    fn test_step_advanced() {
925        test_rs("1-4/4,2-6/2", "1-2,4,6", 4);
926        test_rs("6-24/6,9-21/6", "6-24/3", 7);
927        test_rs("0-24/2,9-21/2", "0-8/2,9-22,24", 20);
928        test_rs("0-24/2,9-21/2,100", "0-8/2,9-22,24,100", 21);
929        test_rs("3-21/9,6-24/9,9-27/9", "3-27/3", 9);
930        test_rs("1-17/2,2-18/2", "1-18", 18);
931        test_rs("1-17/01", "1-17", 17);
932        test_rs("1-17/02", "1-17/2", 9);
933        test_rs("1-17/03", "1-16/3", 6);
934    }
935
936    #[test]
937    fn test_step_advanced_more() {
938        test_rs("0-24/2,9-21/2,100-101", "0-8/2,9-22,24,100-101", 22);
939        test_rs("101-121/4,1-225/112", "1,101-121/4,225", 8);
940        test_rs("1-32/3,13-28/9", "1-31/3", 11);
941        test_rs("1-32/3,13-22/9", "1-31/3", 11);
942        test_rs("1-32/3,13-31/9", "1-31/3", 11);
943        test_rs("1-32/3,13-40/9", "1-31/3,40", 12);
944    }
945
946    #[test]
947    fn test_step_more_complex() {
948        test_rs("1-16/3,13-28/6", "1-19/3,25", 8);
949        test_rs("1-16/3,1-16/6", "1-16/3", 6);
950        test_rs("1-16/6,1-16/3", "1-16/3", 6);
951        test_rs("1-17/2,33-41/2,2-18/2", "1-18,33-41/2", 23);
952        test_rs("1-17/2,33-41/2,2-20/2", "1-18,20,33-41/2", 24);
953        test_rs("1-17/2,33-41/2,2-19/2", "1-18,33-41/2", 23);
954    }
955
956    #[test]
957    fn test_bad_syntax() {
958        assert!(RangeSet::parse("", None).is_err());
959        assert!(RangeSet::parse("-", None).is_err());
960        assert!(RangeSet::parse("A", None).is_err());
961        assert!(RangeSet::parse("2-5/a", None).is_err());
962        assert!(RangeSet::parse("3/2", None).is_err());
963        assert!(RangeSet::parse("3-/2", None).is_err());
964        assert!(RangeSet::parse("-/2", None).is_err());
965        assert!(RangeSet::parse("4-a/2", None).is_err());
966        assert!(RangeSet::parse("4-3/2", None).is_err());
967        assert!(RangeSet::parse("4-5/-2", None).is_err());
968        assert!(RangeSet::parse("004-002", None).is_err());
969    }
970
971    #[test]
972    fn test_intersection() {
973        let mut r1 = RangeSet::parse("4-34", None).unwrap();
974        let r2 = RangeSet::parse("27-42", None).unwrap();
975        r1.intersection_update(&r2);
976        assert_eq!(r1.to_string(), "27-34");
977        assert_eq!(r1.len(), 8);
978    }
979
980    #[test]
981    fn test_intersection_complex() {
982        let mut r1 = RangeSet::parse("2-450,654-700,800", None).unwrap();
983        let r2 = RangeSet::parse("500-502,690-820,830-840,900", None).unwrap();
984        r1.intersection_update(&r2);
985        assert_eq!(r1.to_string(), "690-700,800");
986        assert_eq!(r1.len(), 12);
987    }
988
989    #[test]
990    fn test_intersection_step() {
991        let mut r1 = RangeSet::parse("4-34/2", None).unwrap();
992        let r2 = RangeSet::parse("28-42/2", None).unwrap();
993        r1.intersection_update(&r2);
994        assert_eq!(r1.to_string(), "28,30,32,34");
995        assert_eq!(r1.len(), 4);
996
997        let mut r1 = RangeSet::parse("4-34/2", None).unwrap();
998        let r2 = RangeSet::parse("27-42/2", None).unwrap();
999        r1.intersection_update(&r2);
1000        assert_eq!(r1.to_string(), "");
1001        assert_eq!(r1.len(), 0);
1002
1003        let mut r1 = RangeSet::parse("2-60/3", Some(3)).unwrap();
1004        let r2 = RangeSet::parse("3-50/2", Some(3)).unwrap();
1005        r1.intersection_update(&r2);
1006        assert_eq!(r1.to_string(), "5-47/6");
1007        assert_eq!(r1.len(), 8);
1008    }
1009
1010    #[test]
1011    fn test_difference() {
1012        let mut r1 = RangeSet::parse("4,7-33", None).unwrap();
1013        let r2 = RangeSet::parse("8-33", None).unwrap();
1014        r1.difference_update(&r2);
1015        assert_eq!(r1.to_string(), "4,7");
1016        assert_eq!(r1.len(), 2);
1017    }
1018
1019    #[test]
1020    fn test_difference_bounds() {
1021        let mut r1 = RangeSet::parse("1-10,39-41,50-60", None).unwrap();
1022        let r2 = RangeSet::parse("1-10,38-39,50-60", None).unwrap();
1023        r1.difference_update(&r2);
1024        assert_eq!(r1.len(), 2);
1025        assert_eq!(r1.to_string(), "40-41");
1026    }
1027
1028    #[test]
1029    fn test_symmetric_difference() {
1030        let mut r1 = RangeSet::parse("4,7-33", None).unwrap();
1031        let r2 = RangeSet::parse("8-34", None).unwrap();
1032        r1.symmetric_difference_update(&r2);
1033        assert_eq!(r1.to_string(), "4,7,34");
1034        assert_eq!(r1.len(), 3);
1035    }
1036
1037    #[test]
1038    fn test_symmetric_difference_complex() {
1039        let mut r1 = RangeSet::parse("5,7,10-12,33-50", None).unwrap();
1040        let r2 = RangeSet::parse("8-34", None).unwrap();
1041        r1.symmetric_difference_update(&r2);
1042        assert_eq!(r1.to_string(), "5,7-9,13-32,35-50");
1043        assert_eq!(r1.len(), 40);
1044
1045        let mut r1 = RangeSet::parse("8-30", None).unwrap();
1046        let r2 = RangeSet::parse("31-40", None).unwrap();
1047        r1.symmetric_difference_update(&r2);
1048        assert_eq!(r1.to_string(), "8-40");
1049        assert_eq!(r1.len(), 33);
1050
1051        let mut r1 = RangeSet::parse("8-30", None).unwrap();
1052        let r2 = RangeSet::parse("8-30", None).unwrap();
1053        r1.symmetric_difference_update(&r2);
1054        assert_eq!(r1.to_string(), "");
1055        assert_eq!(r1.len(), 0);
1056    }
1057
1058    #[test]
1059    fn test_union() {
1060        let r1 = RangeSet::parse("1-10", None).unwrap();
1061        let r2 = RangeSet::parse("8-20", None).unwrap();
1062        let r3 = r1.union(&r2);
1063        assert_eq!(r3.to_string(), "1-20");
1064        assert_eq!(r3.len(), 20);
1065    }
1066
1067    #[test]
1068    fn test_contains() {
1069        let r1 = RangeSet::parse("1-100,102,105-242,800", None).unwrap();
1070        assert_eq!(r1.len(), 240);
1071        assert!(r1.contains_str("99"));
1072        assert!(!r1.contains_str("099"));
1073        assert!(!r1.contains_str("101"));
1074    }
1075
1076    #[test]
1077    fn test_padding() {
1078        let r1 = RangeSet::parse("001-010", None).unwrap();
1079        assert_eq!(r1.len(), 10);
1080        assert!(r1.contains_str("001"));
1081        assert!(r1.contains_str("010"));
1082        assert!(!r1.contains_str("1"));
1083        assert!(!r1.contains_str("10"));
1084
1085        let r2 = RangeSet::parse("00-02", None).unwrap();
1086        assert!(r2.contains_str("00"));
1087        assert!(!r2.contains_str("0"));
1088        assert!(r2.contains_str("01"));
1089        assert!(!r2.contains_str("1"));
1090
1091        let r3 = RangeSet::parse("0003-0143,0360-1000", None).unwrap();
1092        assert!(!r3.contains_str("360"));
1093        assert!(r3.contains_str("0360"));
1094    }
1095
1096    #[test]
1097    fn test_padding_display() {
1098        test_rs("001-010", "001-010", 10);
1099        test_rs("00-02", "00-02", 3);
1100    }
1101
1102    #[test]
1103    fn test_empty() {
1104        let rs = RangeSet::new();
1105        assert!(rs.is_empty());
1106        assert_eq!(rs.len(), 0);
1107        assert_eq!(rs.to_string(), "");
1108    }
1109
1110    #[test]
1111    fn test_add_remove() {
1112        let mut rs = RangeSet::new();
1113        rs.add_int(5, 0);
1114        rs.add_int(3, 0);
1115        rs.add_int(1, 0);
1116        assert_eq!(rs.len(), 3);
1117        assert_eq!(rs.to_string(), "1,3,5");
1118        rs.discard_str("3");
1119        assert_eq!(rs.len(), 2);
1120        assert_eq!(rs.to_string(), "1,5");
1121    }
1122
1123    #[test]
1124    fn test_intiter() {
1125        let rs = RangeSet::parse("3,1,5,10", None).unwrap();
1126        let vals: Vec<i64> = rs.intiter().collect();
1127        assert_eq!(vals, vec![1, 3, 5, 10]);
1128    }
1129
1130    #[test]
1131    fn test_large_range() {
1132        let rs = RangeSet::parse("1-1000", None).unwrap();
1133        assert_eq!(rs.len(), 1000);
1134        assert_eq!(rs.to_string(), "1-1000");
1135    }
1136
1137    #[test]
1138    fn test_diff_step() {
1139        let mut r1 = RangeSet::parse("4-34/2", Some(3)).unwrap();
1140        let r2 = RangeSet::parse("3-33/2", Some(3)).unwrap();
1141        r1.difference_update(&r2);
1142        assert_eq!(r1.to_string(), "4-34/2");
1143        assert_eq!(r1.len(), 16);
1144
1145        let mut r1 = RangeSet::parse("4-34/2", Some(3)).unwrap();
1146        let r2 = RangeSet::parse("2-14/2", Some(3)).unwrap();
1147        r1.difference_update(&r2);
1148        assert_eq!(r1.to_string(), "16-34/2");
1149        assert_eq!(r1.len(), 10);
1150
1151        let mut r1 = RangeSet::parse("4-34/2", Some(3)).unwrap();
1152        let r2 = RangeSet::parse("28-52/2", Some(3)).unwrap();
1153        r1.difference_update(&r2);
1154        assert_eq!(r1.to_string(), "4-26/2");
1155        assert_eq!(r1.len(), 12);
1156
1157        let mut r1 = RangeSet::parse("4-34/2", Some(3)).unwrap();
1158        let r2 = RangeSet::parse("12-18/2", Some(3)).unwrap();
1159        r1.difference_update(&r2);
1160        assert_eq!(r1.to_string(), "4-10/2,20-34/2");
1161        assert_eq!(r1.len(), 12);
1162    }
1163
1164    #[test]
1165    fn test_diff_step_complex() {
1166        let mut r1 = RangeSet::parse("1-100", Some(3)).unwrap();
1167        let r2 = RangeSet::parse("2-98/2", Some(3)).unwrap();
1168        r1.difference_update(&r2);
1169        assert_eq!(r1.to_string(), "1-99/2,100");
1170        assert_eq!(r1.len(), 51);
1171
1172        let mut r1 = RangeSet::parse("1-1000", Some(3)).unwrap();
1173        let r2 = RangeSet::parse("2-999/2", Some(3)).unwrap();
1174        r1.difference_update(&r2);
1175        assert_eq!(r1.to_string(), "1-999/2,1000");
1176        assert_eq!(r1.len(), 501);
1177    }
1178
1179    #[test]
1180    fn test_folding() {
1181        // Python: testFolding
1182        let r1 = RangeSet::parse(
1183            "112,114-117,119,121,130,132,134,136,138,139-141,144,147-148",
1184            Some(6),
1185        )
1186        .unwrap();
1187        assert_eq!(
1188            r1.to_string(),
1189            "112,114-117,119,121,130,132,134,136,138-141,144,147-148"
1190        );
1191
1192        let r1 = RangeSet::parse(
1193            "112,114-117,119,121,130,132,134,136,138,139-141,144,147-148",
1194            Some(5),
1195        )
1196        .unwrap();
1197        assert_eq!(
1198            r1.to_string(),
1199            "112,114-117,119,121,130-138/2,139-141,144,147-148"
1200        );
1201
1202        let r1 = RangeSet::parse("1,3-4,6,8", None).unwrap();
1203        assert_eq!(r1.to_string(), "1,3-4,6,8");
1204
1205        let r1 = RangeSet::parse("1,3-4,6,8", Some(4)).unwrap();
1206        assert_eq!(r1.to_string(), "1,3-4,6,8");
1207
1208        let r1 = RangeSet::parse("1,3-4,6,8", Some(2)).unwrap();
1209        assert_eq!(r1.to_string(), "1-3/2,4,6-8/2");
1210
1211        let r1 = RangeSet::parse("1,3-4,6,8", Some(3)).unwrap();
1212        assert_eq!(r1.to_string(), "1,3-4,6,8");
1213    }
1214
1215    #[test]
1216    fn test_mixed_padding() {
1217        // Python: test_mixed_padding
1218        let r0 = RangeSet::parse("030-031,032-100/2,101-489", Some(3)).unwrap();
1219        assert_eq!(r0.to_string(), "030-032,034-100/2,101-489");
1220
1221        let r1 = RangeSet::parse("030-032,033-100/3,102", Some(3)).unwrap();
1222        assert_eq!(r1.to_string(), "030-033,036-102/3");
1223
1224        let r2 = RangeSet::parse("030-032,033-100/3,101", Some(3)).unwrap();
1225        assert_eq!(r2.to_string(), "030-033,036-099/3,101");
1226
1227        let r3 = RangeSet::parse("030-032,033-100/3,100", Some(3)).unwrap();
1228        assert_eq!(r3.to_string(), "030-033,036-099/3,100");
1229    }
1230
1231    #[test]
1232    fn test_mixed_padding_with_different_widths() {
1233        // Python: test_mixed_padding r5 case
1234        let r5 = RangeSet::parse("030-032,033-100/3,99-105/3,0001", Some(3)).unwrap();
1235        assert_eq!(r5.to_string(), "99,030-033,036-105/3,0001");
1236    }
1237
1238    #[test]
1239    fn test_padding_mismatch_errors() {
1240        // Python: test_mixed_padding_mismatch
1241        assert!(RangeSet::parse("1-044", None).is_err());
1242        assert!(RangeSet::parse("01-044", None).is_err());
1243        assert!(RangeSet::parse("001-44", None).is_err());
1244        assert!(RangeSet::parse("0-9,1-044", None).is_err());
1245        assert!(RangeSet::parse("0-9,01-044", None).is_err());
1246        assert!(RangeSet::parse("0-9,001-44", None).is_err());
1247        assert!(RangeSet::parse("030-032,033-99/3,100", None).is_err());
1248    }
1249
1250    #[test]
1251    fn test_update_complex() {
1252        // Python: testUpdate
1253        let mut r1 = RangeSet::parse("1-100,102,105-242,800", None).unwrap();
1254        assert_eq!(r1.len(), 240);
1255        let r2 = RangeSet::parse("243-799,1924-1984", None).unwrap();
1256        assert_eq!(r2.len(), 618);
1257        r1.update(&r2);
1258        assert_eq!(r1.len(), 240 + 618);
1259        assert_eq!(r1.to_string(), "1-100,102,105-800,1924-1984");
1260    }
1261
1262    #[test]
1263    fn test_union_complex() {
1264        // Python: testUnion
1265        let r1 = RangeSet::parse("1-100,102,105-242,800", None).unwrap();
1266        let r2 = RangeSet::parse("243-799,1924-1984", None).unwrap();
1267        let r3 = r1.union(&r2);
1268        assert_eq!(r3.len(), 858);
1269        assert_eq!(r3.to_string(), "1-100,102,105-800,1924-1984");
1270
1271        // with overlap
1272        let r2 = RangeSet::parse("200-799", None).unwrap();
1273        let r3 = r1.union(&r2);
1274        assert_eq!(r3.len(), 797);
1275        assert_eq!(r3.to_string(), "1-100,102,105-800");
1276    }
1277
1278    #[test]
1279    fn test_intersection_length() {
1280        // Python: testIntersectionLength
1281        let r1 = RangeSet::parse("115-117,130,166-170,4780-4999", None).unwrap();
1282        assert_eq!(r1.len(), 229);
1283        let r2 = RangeSet::parse("116-117,130,4781-4999", None).unwrap();
1284        assert_eq!(r2.len(), 222);
1285        let res = r1.intersection(&r2);
1286        assert_eq!(res.len(), 222);
1287
1288        let r1 = RangeSet::parse("115-200", None).unwrap();
1289        assert_eq!(r1.len(), 86);
1290        let r2 = RangeSet::parse("116-117,119,123-131,133,149,199", None).unwrap();
1291        assert_eq!(r2.len(), 15);
1292        let res = r1.intersection(&r2);
1293        assert_eq!(res.len(), 15);
1294
1295        // StopIteration test
1296        let r1 = RangeSet::parse("115-117,130,166-170,4780-4999,5003", None).unwrap();
1297        assert_eq!(r1.len(), 230);
1298        let r2 = RangeSet::parse("116-117,130,4781-4999", None).unwrap();
1299        assert_eq!(r2.len(), 222);
1300        let res = r1.intersection(&r2);
1301        assert_eq!(res.len(), 222);
1302
1303        // StopIteration test2
1304        let r1 = RangeSet::parse("130,166-170,4780-4999", None).unwrap();
1305        assert_eq!(r1.len(), 226);
1306        let r2 = RangeSet::parse("116-117", None).unwrap();
1307        assert_eq!(r2.len(), 2);
1308        let res = r1.intersection(&r2);
1309        assert_eq!(res.len(), 0);
1310    }
1311
1312    #[test]
1313    fn test_ior() {
1314        // Python: test_ior
1315        let mut r1 = RangeSet::parse("1,3-9,14-21,30-39,42", None).unwrap();
1316        let r2 = RangeSet::parse("2-5,10-32,35,40-41", None).unwrap();
1317        r1.update(&r2);
1318        assert_eq!(r1.len(), 42);
1319        assert_eq!(r1.to_string(), "1-42");
1320    }
1321
1322    #[test]
1323    fn test_iand() {
1324        // Python: test_iand
1325        let mut r1 = RangeSet::parse("1,3-9,14-21,30-39,42", None).unwrap();
1326        let r2 = RangeSet::parse("2-5,10-32,35,40-41", None).unwrap();
1327        r1.intersection_update(&r2);
1328        assert_eq!(r1.len(), 15);
1329        assert_eq!(r1.to_string(), "3-5,14-21,30-32,35");
1330    }
1331
1332    #[test]
1333    fn test_ixor() {
1334        // Python: test_ixor
1335        let mut r1 = RangeSet::parse("1,3-9,14-21,30-39,42", None).unwrap();
1336        let r2 = RangeSet::parse("2-5,10-32,35,40-41", None).unwrap();
1337        r1.symmetric_difference_update(&r2);
1338        assert_eq!(r1.len(), 27);
1339        assert_eq!(r1.to_string(), "1-2,6-13,22-29,33-34,36-42");
1340    }
1341
1342    #[test]
1343    fn test_isub() {
1344        // Python: test_isub
1345        let mut r1 = RangeSet::parse("1,3-9,14-21,30-39,42", None).unwrap();
1346        let r2 = RangeSet::parse("2-5,10-32,35,40-41", None).unwrap();
1347        r1.difference_update(&r2);
1348        assert_eq!(r1.len(), 12);
1349        assert_eq!(r1.to_string(), "1,6-9,33-34,36-39,42");
1350    }
1351
1352    #[test]
1353    fn test_add_range_api() {
1354        // Python: testAddRange
1355        let mut r1 = RangeSet::new();
1356        r1.add_range(1, 100, 1, 0);
1357        assert_eq!(r1.len(), 99);
1358        assert_eq!(r1.to_string(), "1-99");
1359        r1.add_range(40, 101, 1, 0);
1360        assert_eq!(r1.len(), 100);
1361        assert_eq!(r1.to_string(), "1-100");
1362        r1.add_range(399, 423, 2, 0);
1363        assert_eq!(r1.len(), 112);
1364        assert_eq!(
1365            r1.to_string(),
1366            "1-100,399,401,403,405,407,409,411,413,415,417,419,421"
1367        );
1368
1369        // With autostep
1370        let mut r1 = RangeSet::new();
1371        r1.set_autostep(Some(3));
1372        r1.add_range(1, 100, 1, 0);
1373        assert_eq!(r1.autostep(), Some(3));
1374        assert_eq!(r1.len(), 99);
1375        assert_eq!(r1.to_string(), "1-99");
1376        r1.add_range(40, 101, 1, 0);
1377        assert_eq!(r1.len(), 100);
1378        assert_eq!(r1.to_string(), "1-100");
1379        r1.add_range(399, 423, 2, 0);
1380        assert_eq!(r1.len(), 112);
1381        assert_eq!(r1.to_string(), "1-100,399-421/2");
1382    }
1383
1384    #[test]
1385    fn test_add_range_bounds() {
1386        let mut r1 = RangeSet::parse("1-30", Some(2)).unwrap();
1387        assert_eq!(r1.len(), 30);
1388        assert_eq!(r1.to_string(), "1-30");
1389        r1.add_range(32, 35, 1, 0);
1390        assert_eq!(r1.len(), 33);
1391        assert_eq!(r1.to_string(), "1-30,32-34");
1392        r1.add_range(31, 32, 1, 0);
1393        assert_eq!(r1.len(), 34);
1394        assert_eq!(r1.to_string(), "1-34");
1395
1396        let mut r1 = RangeSet::parse("1-30", Some(3)).unwrap();
1397        r1.add_range(40, 65, 10, 0);
1398        assert_eq!(r1.autostep(), Some(3));
1399        assert_eq!(r1.len(), 33);
1400        assert_eq!(r1.to_string(), "1-30,40-60/10");
1401
1402        // One
1403        r1.add_range(103, 104, 1, 0);
1404        assert_eq!(r1.len(), 34);
1405        assert_eq!(r1.to_string(), "1-30,40-60/10,103");
1406    }
1407
1408    #[test]
1409    fn test_autostep_property() {
1410        let mut rs = RangeSet::new();
1411        assert_eq!(rs.autostep(), None);
1412        rs.set_autostep(Some(3));
1413        assert_eq!(rs.autostep(), Some(3));
1414        rs.set_autostep(None);
1415        assert_eq!(rs.autostep(), None);
1416    }
1417
1418    #[test]
1419    fn test_padding_property() {
1420        let r1 = RangeSet::parse("001-010", None).unwrap();
1421        assert_eq!(r1.padding(), Some(3));
1422
1423        let r2 = RangeSet::parse("1-10", None).unwrap();
1424        assert_eq!(r2.padding(), None);
1425
1426        let r3 = RangeSet::parse("0003-0143,0360-1000", None).unwrap();
1427        assert_eq!(r3.padding(), Some(4));
1428    }
1429
1430    #[test]
1431    fn test_copy() {
1432        // Python: testCopy
1433        let rangeset = RangeSet::parse("115-117,130,166-170,4780-4999", None).unwrap();
1434        assert_eq!(rangeset.len(), 229);
1435        assert_eq!(rangeset.to_string(), "115-117,130,166-170,4780-4999");
1436        let mut r1 = rangeset.clone();
1437        let mut r2 = rangeset.clone();
1438        r1.remove_str("166");
1439        assert_eq!(rangeset.len(), r1.len() + 1);
1440        assert_eq!(rangeset.to_string(), "115-117,130,166-170,4780-4999");
1441        assert_eq!(r1.to_string(), "115-117,130,167-170,4780-4999");
1442        r2.update(&RangeSet::parse("118", None).unwrap());
1443        assert_eq!(rangeset.len() + 1, r2.len());
1444        assert_eq!(r2.to_string(), "115-118,130,166-170,4780-4999");
1445    }
1446
1447    #[test]
1448    fn test_remove_and_discard() {
1449        // Python: testRemove / testDiscard
1450        let mut r1 = RangeSet::parse("1-100,102,105-242,800", None).unwrap();
1451        assert_eq!(r1.len(), 240);
1452        r1.remove_str("100");
1453        assert_eq!(r1.len(), 239);
1454        assert_eq!(r1.to_string(), "1-99,102,105-242,800");
1455
1456        r1.discard_str("101"); // should not panic
1457        r1.discard_str("105");
1458        assert_eq!(r1.len(), 238);
1459        assert_eq!(r1.to_string(), "1-99,102,106-242,800");
1460    }
1461
1462    #[test]
1463    #[should_panic(expected = "not found")]
1464    fn test_remove_missing_panics() {
1465        let mut r1 = RangeSet::parse("1-100", None).unwrap();
1466        r1.remove_str("101");
1467    }
1468
1469    #[test]
1470    fn test_index() {
1471        // Python: testIndex (ClusterShell commit 484d230)
1472        let r1 = RangeSet::parse("1-100,102,105-242,800", None).unwrap();
1473        // index() is the reverse of __getitem__()
1474        let sorted = r1.sorted();
1475        for (i, elem) in sorted.iter().enumerate() {
1476            assert_eq!(r1.index_str(elem), Some(i));
1477        }
1478        // accept both string and integer arguments
1479        assert_eq!(r1.index_str("1"), Some(0));
1480        assert_eq!(r1.index(1), Some(0));
1481        assert_eq!(r1.index_str("102"), Some(100));
1482        assert_eq!(r1.index(105), Some(101));
1483        assert_eq!(r1.index_str("800"), Some(239));
1484
1485        // zero-padding is significant
1486        let r2 = RangeSet::parse("08-10", None).unwrap();
1487        assert_eq!(r2.index_str("08"), Some(0));
1488        assert_eq!(r2.index_str("09"), Some(1));
1489        assert_eq!(r2.index_str("10"), Some(2));
1490        assert_eq!(r2.index_str("9"), None);
1491        assert_eq!(r2.index(9), None);
1492    }
1493
1494    #[test]
1495    fn test_index_missing() {
1496        // Python: testIndex — missing element raises ValueError there,
1497        // returns None here (like list.index()).
1498        let r1 = RangeSet::parse("1-100,102,105-242,800", None).unwrap();
1499        assert_eq!(r1.index_str("101"), None);
1500        assert_eq!(r1.index(101), None);
1501        assert_eq!(r1.index(900), None);
1502        // empty set: nothing is found
1503        let r0 = RangeSet::new();
1504        assert_eq!(r0.index(0), None);
1505        assert_eq!(r0.index_str("0"), None);
1506    }
1507
1508    #[test]
1509    fn test_index_window() {
1510        // Python: testIndex — optional start/end search window
1511        // (list.index() semantics).
1512        let r3 = RangeSet::parse("0-9", None).unwrap();
1513        assert_eq!(r3.index_str_within("5", 3, None), Some(5));
1514        assert_eq!(r3.index_str_within("5", 5, None), Some(5));
1515        assert_eq!(r3.index_str_within("5", 6, None), None);
1516        assert_eq!(r3.index_str_within("8", -3, None), Some(8));
1517        assert_eq!(r3.index_str_within("5", 0, Some(6)), Some(5));
1518        assert_eq!(r3.index_str_within("5", 0, Some(5)), None);
1519        assert_eq!(r3.index_str_within("9", 0, Some(-1)), None);
1520    }
1521
1522    #[test]
1523    fn test_pickle_empty() {
1524        // Python: test_pickle_empty (ClusterShell commit 9df4c5a).
1525        // Pickling an empty RangeSet stores None (not "", which raises by
1526        // design on reconstruction); the round trip preserves autostep.
1527        let rs = RangeSet::from_optional_pattern(None, Some(3)).unwrap();
1528        assert_eq!(rs, RangeSet::new());
1529        assert_eq!(rs.to_string(), "");
1530        assert_eq!(rs.len(), 0);
1531        assert_eq!(rs.autostep(), Some(3));
1532        // The serialized external representation of an empty set is the
1533        // "no pattern" form, never Some("") which parse() rejects.
1534        let external = if rs.is_empty() {
1535            None
1536        } else {
1537            Some(rs.to_string())
1538        };
1539        let restored = RangeSet::from_optional_pattern(external.as_deref(), rs.autostep()).unwrap();
1540        assert_eq!(restored, rs);
1541        assert_eq!(restored.autostep(), Some(3));
1542        // Some("") remains an error by design, like RangeSet("").
1543        assert!(RangeSet::from_optional_pattern(Some(""), None).is_err());
1544    }
1545
1546    #[test]
1547    fn test_deepcopy_empty() {
1548        // Python: test_deepcopy_empty — Rust Clone is the deepcopy
1549        // equivalent for this value type.
1550        let rs = RangeSet::new().clone();
1551        assert_eq!(rs, RangeSet::new());
1552        assert_eq!(rs.to_string(), "");
1553        assert_eq!(rs.len(), 0);
1554    }
1555
1556    #[test]
1557    fn test_nd_index() {
1558        // Python: RangeSetNDTest.test_index (ClusterShell commit 484d230).
1559        // NOTE: the Python test builds RangeSetND([["10", "10-13"],
1560        // ["0-3", "1-2"]]) and relies on automatic nD folding to merge and
1561        // reorder the vectors. Folding is not implemented in Rust yet, so
1562        // the vectors are stored here in Python's post-fold order, which
1563        // yields identical iteration positions.
1564        let rn1 = RangeSetND::from_rangesets(
1565            vec![
1566                vec![
1567                    RangeSet::parse("0-3", None).unwrap(),
1568                    RangeSet::parse("1-2", None).unwrap(),
1569                ],
1570                vec![
1571                    RangeSet::parse("10", None).unwrap(),
1572                    RangeSet::parse("10-13", None).unwrap(),
1573                ],
1574            ],
1575            None,
1576        );
1577        assert_eq!(rn1.len(), 12);
1578        // index() is the reverse of __getitem__()
1579        for (i, ivec) in rn1.iter().enumerate() {
1580            assert_eq!(rn1.index(&ivec), Some(i));
1581        }
1582        assert_eq!(rn1.index(&["0", "1"]), Some(0));
1583        assert_eq!(rn1.index(&["3", "2"]), Some(7));
1584        assert_eq!(rn1.index(&["10", "13"]), Some(11));
1585        // integer vectors are also accepted (ToString mirrors "%s" % e)
1586        assert_eq!(rn1.index(&[0, 1]), Some(0));
1587        assert_eq!(rn1.index(&[10, 13]), Some(11));
1588        // missing vector raises ValueError in Python, None here
1589        assert_eq!(rn1.index(&["10", "14"]), None);
1590        assert_eq!(rn1.index(&["5", "1"]), None);
1591        // A bare string or scalar is not a valid index vector: Python raises
1592        // TypeError; in Rust this is rejected at compile time since a slice
1593        // of indexes is required (not testable at runtime).
1594        //
1595        // optional start/end search window (list.index() semantics)
1596        assert_eq!(rn1.index_within(&["3", "2"], 7, None), Some(7));
1597        assert_eq!(rn1.index_within(&["3", "2"], 8, None), None);
1598        assert_eq!(rn1.index_within(&["10", "13"], -1, None), Some(11));
1599        assert_eq!(rn1.index_within(&["10", "13"], 0, Some(12)), Some(11));
1600        assert_eq!(rn1.index_within(&["10", "13"], 0, Some(-1)), None);
1601    }
1602}