Skip to main content

feagi_brain_development/connectivity/rules/
patterns.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4/*!
5Pattern-based connectivity - wildcard matching and transformations.
6
7Supports absolute patterns (*, ?, !, int, N..M) and source-relative directional
8patterns (?+, ?-, ?+=, ?-=, ?+N, ?-N, ?-N:?+M) for spatial connectivity.
9*/
10
11use crate::types::Position;
12
13type Dimensions = (usize, usize, usize);
14
15/// Direction along an axis relative to the source coordinate.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Direction {
18    Positive,
19    Negative,
20}
21
22/// Pattern element types for specifying connectivity rules per axis.
23#[derive(Debug, Clone, PartialEq)]
24pub enum PatternElement {
25    /// `"*"` - matches any coordinate on this axis
26    Wildcard,
27    /// `"?"` - pass through source coordinate (dst = src)
28    Skip,
29    /// `"!"` - exclude source coordinate (all except src)
30    Exclude,
31    /// Absolute coordinate value
32    Exact(i32),
33    /// `"?+"` or `"?-"` - all coordinates strictly above/below src
34    DirectionExclusive(Direction),
35    /// `"?+="` or `"?-="` - all coordinates at or above/below src
36    DirectionInclusive(Direction),
37    /// `"?+N"` or `"?-N"` - single coordinate offset from src
38    Offset(i32),
39    /// `"?-A:?+B"` - inclusive range [src + lo, src + hi]
40    Range(i32, i32),
41    /// `"N..M"` - inclusive absolute range [N, M], independent of source
42    AbsoluteRange(i32, i32),
43}
44
45impl PatternElement {
46    /// Parse a pattern element from a string value.
47    pub fn from_value(value: &str) -> Self {
48        match value {
49            "*" => PatternElement::Wildcard,
50            "?" => PatternElement::Skip,
51            "!" => PatternElement::Exclude,
52            "?+" => PatternElement::DirectionExclusive(Direction::Positive),
53            "?-" => PatternElement::DirectionExclusive(Direction::Negative),
54            "?+=" => PatternElement::DirectionInclusive(Direction::Positive),
55            "?-=" => PatternElement::DirectionInclusive(Direction::Negative),
56            _ => {
57                if let Some(range_str) = Self::try_parse_range(value) {
58                    return range_str;
59                }
60                if let Some(abs_range) = Self::try_parse_absolute_range(value) {
61                    return abs_range;
62                }
63                if let Some(offset) = Self::try_parse_offset(value) {
64                    return offset;
65                }
66                if let Ok(num) = value.parse::<i32>() {
67                    PatternElement::Exact(num)
68                } else {
69                    PatternElement::Wildcard
70                }
71            }
72        }
73    }
74
75    /// Parse FFI integer encoding into a PatternElement.
76    pub fn from_int(value: i32) -> Self {
77        match value {
78            -1 => PatternElement::Wildcard,
79            -2 => PatternElement::Skip,
80            -3 => PatternElement::Exclude,
81            -10 => PatternElement::DirectionExclusive(Direction::Positive),
82            -11 => PatternElement::DirectionExclusive(Direction::Negative),
83            -12 => PatternElement::DirectionInclusive(Direction::Positive),
84            -13 => PatternElement::DirectionInclusive(Direction::Negative),
85            _ => PatternElement::Exact(value),
86        }
87    }
88
89    /// Attempt to parse a relative range pattern like "?-1:?+1" or "?+2:?+5".
90    /// Format: "?<sign><int>:?<sign><int>" where both bounds are relative to src.
91    fn try_parse_range(value: &str) -> Option<PatternElement> {
92        let parts: Vec<&str> = value.split(':').collect();
93        if parts.len() != 2 {
94            return None;
95        }
96        let lo = Self::extract_relative_offset(parts[0])?;
97        let hi = Self::extract_relative_offset(parts[1])?;
98        Some(PatternElement::Range(lo, hi))
99    }
100
101    /// Attempt to parse an absolute inclusive range `"N..M"`.
102    fn try_parse_absolute_range(value: &str) -> Option<PatternElement> {
103        let idx = value.find("..")?;
104        if value[idx + 2..].contains("..") {
105            return None;
106        }
107        let lo = value[..idx].parse::<i32>().ok()?;
108        let hi = value[idx + 2..].parse::<i32>().ok()?;
109        Some(PatternElement::AbsoluteRange(lo, hi))
110    }
111
112    /// Attempt to parse a single offset pattern like "?+3" or "?-2".
113    fn try_parse_offset(value: &str) -> Option<PatternElement> {
114        let offset = Self::extract_relative_offset(value)?;
115        Some(PatternElement::Offset(offset))
116    }
117
118    /// Extract a numeric offset from a "?+N" or "?-N" string.
119    fn extract_relative_offset(s: &str) -> Option<i32> {
120        if !s.starts_with('?') {
121            return None;
122        }
123        let rest = &s[1..];
124        if rest.is_empty() {
125            return None;
126        }
127        if rest == "+" || rest == "-" || rest == "+=" || rest == "-=" {
128            return None;
129        }
130        rest.parse::<i32>().ok()
131    }
132}
133
134/// 3D pattern (x, y, z)
135pub type Pattern3D = (PatternElement, PatternElement, PatternElement);
136
137/// Match a coordinate against a pattern element (point-wise check).
138pub fn match_pattern_element(element: &PatternElement, coordinate: i32, src_coord: i32) -> bool {
139    match element {
140        PatternElement::Wildcard => true,
141        PatternElement::Skip => coordinate == src_coord,
142        PatternElement::Exclude => coordinate != src_coord,
143        PatternElement::Exact(val) => coordinate == *val,
144        PatternElement::DirectionExclusive(Direction::Positive) => coordinate > src_coord,
145        PatternElement::DirectionExclusive(Direction::Negative) => coordinate < src_coord,
146        PatternElement::DirectionInclusive(Direction::Positive) => coordinate >= src_coord,
147        PatternElement::DirectionInclusive(Direction::Negative) => coordinate <= src_coord,
148        PatternElement::Offset(off) => coordinate == src_coord + off,
149        PatternElement::Range(lo, hi) => {
150            coordinate >= src_coord + lo && coordinate <= src_coord + hi
151        }
152        PatternElement::AbsoluteRange(lo, hi) => coordinate >= *lo && coordinate <= *hi,
153    }
154}
155
156/// Expand a single axis pattern element into a set of destination coordinates.
157fn expand_axis(element: &PatternElement, src_coord: u32, dim: usize) -> Vec<u32> {
158    match element {
159        PatternElement::Wildcard => (0..dim as u32).collect(),
160        PatternElement::Skip => {
161            if (src_coord as usize) < dim {
162                vec![src_coord]
163            } else {
164                vec![]
165            }
166        }
167        PatternElement::Exclude => (0..dim as u32).filter(|&c| c != src_coord).collect(),
168        PatternElement::Exact(val) => {
169            if *val >= 0 && (*val as usize) < dim {
170                vec![*val as u32]
171            } else {
172                vec![]
173            }
174        }
175        PatternElement::DirectionExclusive(Direction::Positive) => {
176            ((src_coord + 1)..dim as u32).collect()
177        }
178        PatternElement::DirectionExclusive(Direction::Negative) => (0..src_coord).collect(),
179        PatternElement::DirectionInclusive(Direction::Positive) => {
180            (src_coord..dim as u32).collect()
181        }
182        PatternElement::DirectionInclusive(Direction::Negative) => {
183            (0..=src_coord).filter(|&c| (c as usize) < dim).collect()
184        }
185        PatternElement::Offset(off) => {
186            let target = src_coord as i32 + off;
187            if target >= 0 && (target as usize) < dim {
188                vec![target as u32]
189            } else {
190                vec![]
191            }
192        }
193        PatternElement::Range(lo, hi) => {
194            let start = (src_coord as i32 + lo).max(0) as u32;
195            let end_exclusive = ((src_coord as i32 + hi) + 1).min(dim as i32) as u32;
196            if start >= end_exclusive {
197                vec![]
198            } else {
199                (start..end_exclusive).collect()
200            }
201        }
202        PatternElement::AbsoluteRange(lo, hi) => expand_absolute_range(*lo, *hi, dim),
203    }
204}
205
206/// Inclusive absolute range `[lo, hi]` clamped to `[0, dim)`.
207fn expand_absolute_range(lo: i32, hi: i32, dim: usize) -> Vec<u32> {
208    if lo > hi {
209        return vec![];
210    }
211    let start = lo.max(0) as u32;
212    let end_exclusive = (hi + 1).min(dim as i32).max(0) as u32;
213    if start >= end_exclusive {
214        vec![]
215    } else {
216        (start..end_exclusive).collect()
217    }
218}
219
220/// Source-side expansion: `*`, exact N, and `N..M` filter; other tokens are wildcards.
221fn expand_source_axis(element: &PatternElement, dim: usize) -> Vec<u32> {
222    match element {
223        PatternElement::Wildcard => (0..dim as u32).collect(),
224        PatternElement::Exact(val) => {
225            if *val >= 0 && (*val as usize) < dim {
226                vec![*val as u32]
227            } else {
228                vec![]
229            }
230        }
231        PatternElement::AbsoluteRange(lo, hi) => expand_absolute_range(*lo, *hi, dim),
232        _ => (0..dim as u32).collect(),
233    }
234}
235
236/// Source-side match for one axis of a live neuron coordinate.
237fn source_axis_matches(element: &PatternElement, coord: u32) -> bool {
238    match element {
239        PatternElement::Wildcard => true,
240        PatternElement::Exact(val) => *val >= 0 && coord == (*val as u32),
241        PatternElement::AbsoluteRange(lo, hi) => {
242            let value = coord as i32;
243            value >= *lo && value <= *hi
244        }
245        _ => true,
246    }
247}
248
249/// Generate destination coordinates from pattern matching.
250pub fn find_destination_coordinates(
251    dst_dimensions: Dimensions,
252    src_coordinate: Position,
253    _src_pattern: &Pattern3D,
254    dst_pattern: &Pattern3D,
255) -> Vec<Position> {
256    let (dst_width, dst_height, dst_depth) = dst_dimensions;
257    let (src_x, src_y, src_z) = src_coordinate;
258
259    let x_range = expand_axis(&dst_pattern.0, src_x, dst_width);
260    let y_range = expand_axis(&dst_pattern.1, src_y, dst_height);
261    let z_range = expand_axis(&dst_pattern.2, src_z, dst_depth);
262
263    let mut results = Vec::with_capacity(x_range.len() * y_range.len() * z_range.len());
264    for x in &x_range {
265        for y in &y_range {
266            for z in &z_range {
267                results.push((*x, *y, *z));
268            }
269        }
270    }
271
272    results
273}
274
275/// Find source coordinates that match a pattern.
276/// Directional/relative patterns are treated as wildcard on source side since
277/// they require a specific source coordinate to resolve against.
278pub fn find_source_coordinates(
279    src_pattern: &Pattern3D,
280    src_dimensions: Dimensions,
281) -> Vec<Position> {
282    let (src_width, src_height, src_depth) = src_dimensions;
283
284    let x_range = expand_source_axis(&src_pattern.0, src_width);
285    let y_range = expand_source_axis(&src_pattern.1, src_height);
286    let z_range = expand_source_axis(&src_pattern.2, src_depth);
287
288    let mut results = Vec::with_capacity(x_range.len() * y_range.len() * z_range.len());
289    for x in &x_range {
290        for y in &y_range {
291            for z in &z_range {
292                results.push((*x, *y, *z));
293            }
294        }
295    }
296
297    results
298}
299
300/// Batch process pattern matching for multiple patterns.
301pub fn match_patterns_batch(
302    src_coordinate: Position,
303    patterns: &[(Pattern3D, Pattern3D)],
304    _src_dimensions: Dimensions,
305    dst_dimensions: Dimensions,
306) -> Vec<Position> {
307    let mut all_results = Vec::new();
308
309    for (src_pattern, dst_pattern) in patterns {
310        let (src_x, src_y, src_z) = src_coordinate;
311
312        let x_match = source_axis_matches(&src_pattern.0, src_x);
313        let y_match = source_axis_matches(&src_pattern.1, src_y);
314        let z_match = source_axis_matches(&src_pattern.2, src_z);
315
316        if x_match && y_match && z_match {
317            let mut results = find_destination_coordinates(
318                dst_dimensions,
319                src_coordinate,
320                src_pattern,
321                dst_pattern,
322            );
323            all_results.append(&mut results);
324        }
325    }
326
327    all_results.sort_unstable();
328    all_results.dedup();
329
330    all_results
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_wildcard_pattern() {
339        let src_pattern = (
340            PatternElement::Wildcard,
341            PatternElement::Wildcard,
342            PatternElement::Exact(0),
343        );
344        let dst_pattern = (
345            PatternElement::Skip,
346            PatternElement::Skip,
347            PatternElement::Exact(1),
348        );
349
350        let results =
351            find_destination_coordinates((10, 10, 10), (5, 5, 0), &src_pattern, &dst_pattern);
352
353        assert_eq!(results.len(), 1);
354        assert_eq!(results[0], (5, 5, 1));
355    }
356
357    #[test]
358    fn test_exact_pattern() {
359        let src_pattern = (
360            PatternElement::Exact(0),
361            PatternElement::Exact(0),
362            PatternElement::Exact(0),
363        );
364        let dst_pattern = (
365            PatternElement::Exact(1),
366            PatternElement::Exact(2),
367            PatternElement::Exact(3),
368        );
369
370        let results =
371            find_destination_coordinates((10, 10, 10), (0, 0, 0), &src_pattern, &dst_pattern);
372
373        assert_eq!(results.len(), 1);
374        assert_eq!(results[0], (1, 2, 3));
375    }
376
377    #[test]
378    fn test_exclude_pattern() {
379        let src_pattern = (
380            PatternElement::Wildcard,
381            PatternElement::Wildcard,
382            PatternElement::Wildcard,
383        );
384        let dst_pattern = (
385            PatternElement::Exclude,
386            PatternElement::Exact(0),
387            PatternElement::Exact(0),
388        );
389
390        let results =
391            find_destination_coordinates((3, 1, 1), (1, 0, 0), &src_pattern, &dst_pattern);
392
393        assert_eq!(results.len(), 2);
394        assert!(results.contains(&(0, 0, 0)));
395        assert!(results.contains(&(2, 0, 0)));
396    }
397
398    #[test]
399    fn test_direction_positive_exclusive() {
400        let src_pattern = (
401            PatternElement::Wildcard,
402            PatternElement::Wildcard,
403            PatternElement::Wildcard,
404        );
405        let dst_pattern = (
406            PatternElement::DirectionExclusive(Direction::Positive),
407            PatternElement::Skip,
408            PatternElement::Skip,
409        );
410
411        let results =
412            find_destination_coordinates((8, 4, 2), (3, 1, 0), &src_pattern, &dst_pattern);
413
414        assert_eq!(results, vec![(4, 1, 0), (5, 1, 0), (6, 1, 0), (7, 1, 0)]);
415    }
416
417    #[test]
418    fn test_direction_negative_exclusive() {
419        let src_pattern = (
420            PatternElement::Wildcard,
421            PatternElement::Wildcard,
422            PatternElement::Wildcard,
423        );
424        let dst_pattern = (
425            PatternElement::DirectionExclusive(Direction::Negative),
426            PatternElement::Skip,
427            PatternElement::Skip,
428        );
429
430        let results =
431            find_destination_coordinates((8, 4, 2), (3, 1, 0), &src_pattern, &dst_pattern);
432
433        assert_eq!(results, vec![(0, 1, 0), (1, 1, 0), (2, 1, 0)]);
434    }
435
436    #[test]
437    fn test_direction_positive_inclusive() {
438        let dst_pattern = (
439            PatternElement::DirectionInclusive(Direction::Positive),
440            PatternElement::Skip,
441            PatternElement::Skip,
442        );
443        let src_pattern = (
444            PatternElement::Wildcard,
445            PatternElement::Wildcard,
446            PatternElement::Wildcard,
447        );
448
449        let results =
450            find_destination_coordinates((6, 3, 1), (2, 1, 0), &src_pattern, &dst_pattern);
451
452        assert_eq!(results, vec![(2, 1, 0), (3, 1, 0), (4, 1, 0), (5, 1, 0)]);
453    }
454
455    #[test]
456    fn test_direction_negative_inclusive() {
457        let dst_pattern = (
458            PatternElement::DirectionInclusive(Direction::Negative),
459            PatternElement::Skip,
460            PatternElement::Skip,
461        );
462        let src_pattern = (
463            PatternElement::Wildcard,
464            PatternElement::Wildcard,
465            PatternElement::Wildcard,
466        );
467
468        let results =
469            find_destination_coordinates((6, 3, 1), (2, 1, 0), &src_pattern, &dst_pattern);
470
471        assert_eq!(results, vec![(0, 1, 0), (1, 1, 0), (2, 1, 0)]);
472    }
473
474    #[test]
475    fn test_offset_positive() {
476        let dst_pattern = (
477            PatternElement::Offset(2),
478            PatternElement::Skip,
479            PatternElement::Skip,
480        );
481        let src_pattern = (
482            PatternElement::Wildcard,
483            PatternElement::Wildcard,
484            PatternElement::Wildcard,
485        );
486
487        let results =
488            find_destination_coordinates((10, 10, 10), (3, 5, 7), &src_pattern, &dst_pattern);
489
490        assert_eq!(results, vec![(5, 5, 7)]);
491    }
492
493    #[test]
494    fn test_offset_negative() {
495        let dst_pattern = (
496            PatternElement::Offset(-2),
497            PatternElement::Skip,
498            PatternElement::Skip,
499        );
500        let src_pattern = (
501            PatternElement::Wildcard,
502            PatternElement::Wildcard,
503            PatternElement::Wildcard,
504        );
505
506        let results =
507            find_destination_coordinates((10, 10, 10), (3, 5, 7), &src_pattern, &dst_pattern);
508
509        assert_eq!(results, vec![(1, 5, 7)]);
510    }
511
512    #[test]
513    fn test_offset_out_of_bounds() {
514        let dst_pattern = (
515            PatternElement::Offset(5),
516            PatternElement::Skip,
517            PatternElement::Skip,
518        );
519        let src_pattern = (
520            PatternElement::Wildcard,
521            PatternElement::Wildcard,
522            PatternElement::Wildcard,
523        );
524
525        let results =
526            find_destination_coordinates((6, 5, 5), (4, 2, 2), &src_pattern, &dst_pattern);
527
528        assert!(results.is_empty());
529    }
530
531    #[test]
532    fn test_range_symmetric() {
533        let dst_pattern = (
534            PatternElement::Range(-1, 1),
535            PatternElement::Range(-1, 1),
536            PatternElement::Skip,
537        );
538        let src_pattern = (
539            PatternElement::Wildcard,
540            PatternElement::Wildcard,
541            PatternElement::Wildcard,
542        );
543
544        let results =
545            find_destination_coordinates((10, 10, 5), (5, 5, 2), &src_pattern, &dst_pattern);
546
547        assert_eq!(results.len(), 9); // 3x3 grid
548        assert!(results.contains(&(4, 4, 2)));
549        assert!(results.contains(&(5, 5, 2)));
550        assert!(results.contains(&(6, 6, 2)));
551    }
552
553    #[test]
554    fn test_range_clamped_at_boundary() {
555        let dst_pattern = (
556            PatternElement::Range(-3, 3),
557            PatternElement::Skip,
558            PatternElement::Skip,
559        );
560        let src_pattern = (
561            PatternElement::Wildcard,
562            PatternElement::Wildcard,
563            PatternElement::Wildcard,
564        );
565
566        // src_x=1, range would be -2..4, clamped to 0..4
567        let results =
568            find_destination_coordinates((8, 1, 1), (1, 0, 0), &src_pattern, &dst_pattern);
569
570        assert_eq!(
571            results,
572            vec![(0, 0, 0), (1, 0, 0), (2, 0, 0), (3, 0, 0), (4, 0, 0)]
573        );
574    }
575
576    #[test]
577    fn test_range_forward_only() {
578        let dst_pattern = (
579            PatternElement::Range(1, 3),
580            PatternElement::Skip,
581            PatternElement::Skip,
582        );
583        let src_pattern = (
584            PatternElement::Wildcard,
585            PatternElement::Wildcard,
586            PatternElement::Wildcard,
587        );
588
589        let results =
590            find_destination_coordinates((10, 1, 1), (2, 0, 0), &src_pattern, &dst_pattern);
591
592        assert_eq!(results, vec![(3, 0, 0), (4, 0, 0), (5, 0, 0)]);
593    }
594
595    #[test]
596    fn test_direction_at_edge() {
597        let dst_pattern = (
598            PatternElement::DirectionExclusive(Direction::Negative),
599            PatternElement::Skip,
600            PatternElement::Skip,
601        );
602        let src_pattern = (
603            PatternElement::Wildcard,
604            PatternElement::Wildcard,
605            PatternElement::Wildcard,
606        );
607
608        // src_x=0: nothing to the left
609        let results =
610            find_destination_coordinates((10, 1, 1), (0, 0, 0), &src_pattern, &dst_pattern);
611
612        assert!(results.is_empty());
613    }
614
615    #[test]
616    fn test_from_value_new_patterns() {
617        assert_eq!(
618            PatternElement::from_value("?+"),
619            PatternElement::DirectionExclusive(Direction::Positive)
620        );
621        assert_eq!(
622            PatternElement::from_value("?-"),
623            PatternElement::DirectionExclusive(Direction::Negative)
624        );
625        assert_eq!(
626            PatternElement::from_value("?+="),
627            PatternElement::DirectionInclusive(Direction::Positive)
628        );
629        assert_eq!(
630            PatternElement::from_value("?-="),
631            PatternElement::DirectionInclusive(Direction::Negative)
632        );
633        assert_eq!(PatternElement::from_value("?+3"), PatternElement::Offset(3));
634        assert_eq!(
635            PatternElement::from_value("?-2"),
636            PatternElement::Offset(-2)
637        );
638        assert_eq!(
639            PatternElement::from_value("?-1:?+1"),
640            PatternElement::Range(-1, 1)
641        );
642        assert_eq!(
643            PatternElement::from_value("?+2:?+5"),
644            PatternElement::Range(2, 5)
645        );
646        assert_eq!(
647            PatternElement::from_value("1..98"),
648            PatternElement::AbsoluteRange(1, 98)
649        );
650    }
651
652    #[test]
653    fn test_from_value_backward_compat() {
654        assert_eq!(PatternElement::from_value("*"), PatternElement::Wildcard);
655        assert_eq!(PatternElement::from_value("?"), PatternElement::Skip);
656        assert_eq!(PatternElement::from_value("!"), PatternElement::Exclude);
657        assert_eq!(PatternElement::from_value("7"), PatternElement::Exact(7));
658    }
659
660    #[test]
661    fn test_from_int_new_encodings() {
662        assert_eq!(
663            PatternElement::from_int(-10),
664            PatternElement::DirectionExclusive(Direction::Positive)
665        );
666        assert_eq!(
667            PatternElement::from_int(-11),
668            PatternElement::DirectionExclusive(Direction::Negative)
669        );
670        assert_eq!(
671            PatternElement::from_int(-12),
672            PatternElement::DirectionInclusive(Direction::Positive)
673        );
674        assert_eq!(
675            PatternElement::from_int(-13),
676            PatternElement::DirectionInclusive(Direction::Negative)
677        );
678    }
679
680    #[test]
681    fn test_batch_with_directional() {
682        let patterns = vec![(
683            (
684                PatternElement::Wildcard,
685                PatternElement::Wildcard,
686                PatternElement::Wildcard,
687            ),
688            (
689                PatternElement::DirectionExclusive(Direction::Positive),
690                PatternElement::Skip,
691                PatternElement::Skip,
692            ),
693        )];
694
695        let results = match_patterns_batch((3, 0, 0), &patterns, (8, 1, 1), (8, 1, 1));
696
697        assert_eq!(results, vec![(4, 0, 0), (5, 0, 0), (6, 0, 0), (7, 0, 0)]);
698    }
699
700    #[test]
701    fn test_absolute_range_filters_source_x() {
702        let src_pattern = (
703            PatternElement::from_value("1..3"),
704            PatternElement::Wildcard,
705            PatternElement::Wildcard,
706        );
707        let sources = find_source_coordinates(&src_pattern, (8, 1, 1));
708        assert_eq!(sources, vec![(1, 0, 0), (2, 0, 0), (3, 0, 0)]);
709    }
710
711    #[test]
712    fn test_absolute_range_destination_expand() {
713        let src_pattern = (
714            PatternElement::Wildcard,
715            PatternElement::Wildcard,
716            PatternElement::Wildcard,
717        );
718        let dst_pattern = (
719            PatternElement::AbsoluteRange(1, 3),
720            PatternElement::Exact(0),
721            PatternElement::Exact(0),
722        );
723        let results =
724            find_destination_coordinates((10, 1, 1), (7, 0, 0), &src_pattern, &dst_pattern);
725        assert_eq!(results, vec![(1, 0, 0), (2, 0, 0), (3, 0, 0)]);
726    }
727
728    #[test]
729    fn test_absolute_range_batch_skips_outside_source() {
730        let patterns = [(
731            (
732                PatternElement::AbsoluteRange(1, 3),
733                PatternElement::Wildcard,
734                PatternElement::Wildcard,
735            ),
736            (
737                PatternElement::Skip,
738                PatternElement::Skip,
739                PatternElement::Exact(0),
740            ),
741        )];
742        let inside = match_patterns_batch((2, 0, 0), &patterns, (8, 1, 1), (8, 1, 1));
743        assert_eq!(inside, vec![(2, 0, 0)]);
744        let outside = match_patterns_batch((5, 0, 0), &patterns, (8, 1, 1), (8, 1, 1));
745        assert!(outside.is_empty());
746    }
747
748    #[test]
749    fn test_absolute_range_inverted_is_empty() {
750        let results = expand_absolute_range(5, 1, 10);
751        assert!(results.is_empty());
752    }
753}