minspan/
lib.rs

1// We want to make sure we are getting the shortest match possible
2// without getting tripped up by pathological cases.
3pub mod minspan {
4
5    pub fn span<A>(query: &Vec<A>, history: &Vec<A>) -> Option<(usize, usize)>
6    where
7        A: PartialEq,
8    {
9        // If history is empty, we cannot find any span with valid indices.
10        if history.is_empty() {
11            return None;
12        }
13        // If query is empty, it's a subsequence starting at index 0.
14        // The current implementation returns (0, 0), representing history[0..=0].
15        // This is valid since history is non-empty here.
16        if query.is_empty() {
17            return Some((0, 0));
18        }
19
20        // Initialize state for the main algorithm
21        let mut starting_at: Vec<Option<(usize, usize)>> = query.iter().map(|_| None).collect();
22        let mut best_complete_solution: Option<(usize, usize)> = None;
23
24        // Main loop: requires non-empty query and history
25        for (bodyindex, bodychr) in history.iter().enumerate() {
26            for (keyindex, keychr) in query.iter().enumerate().rev() {
27                if keychr == bodychr {
28                    // we have a match, therefore record it: it ends at bodyindex,
29                    // and by construction, starts at starting_at[0]
30                    starting_at[keyindex] = if keyindex == 0 {
31                        // we got nothing yet! set to beginning
32                        Some((bodyindex, bodyindex))
33                    } else {
34                        starting_at[keyindex - 1].map(|(start, _end)| (start, bodyindex))
35                    };
36                    // are we finished?
37                    if (keyindex + 1) == query.len() {
38                        if let Some((from, to)) = starting_at[keyindex] {
39                            best_complete_solution = match best_complete_solution {
40                                None => Some((from, to)), // 1+to - from),
41                                Some((currfrom, currto)) => {
42                                    Some(if to - from < currto - currfrom {
43                                        (from, to)
44                                    } else {
45                                        (currfrom, currto)
46                                    })
47                                }
48                            }
49                        }
50                    }
51                }
52            }
53        }
54        best_complete_solution
55    }
56}
57
58// Add proptest imports for property-based testing
59#[cfg(test)]
60extern crate proptest;
61#[cfg(test)]
62use proptest::prelude::*;
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    // Keep existing tests
68    #[test]
69    fn test_minimal_match() {
70        // Helper to convert string slices to Vec<char> and call span
71        let run_span = |needle: &str, haystack: &str| {
72            let query: Vec<char> = needle.chars().collect();
73            let history: Vec<char> = haystack.chars().collect();
74            minspan::span(&query, &history)
75        };
76
77        // Helper to get span length or None
78        let get_span_len = |needle: &str, haystack: &str| {
79            run_span(needle, haystack).map(|(from, to)| 1 + to - from)
80        };
81
82        assert_eq!(get_span_len("ab", "ab"), Some(2));
83        assert_eq!(get_span_len("a", "ab"), Some(1));
84        assert_eq!(get_span_len("ab", "abc"), Some(2));
85        assert_eq!(get_span_len("abc", "abcd"), Some(3));
86        assert_eq!(get_span_len("curl", "curly"), Some(4));
87        assert_eq!(get_span_len("curl", "acccccurlycurrelly"), Some(4));
88        assert_eq!(get_span_len("z", "acccccurlycurrelly"), None);
89        assert_eq!(get_span_len("ssh", "testssh"), Some(3));
90        assert_eq!(get_span_len("aba", "abababa"), Some(3));
91        assert_eq!(run_span("", "abc"), Some((0, 0))); // Empty query
92        assert_eq!(run_span("a", ""), None); // Empty history
93        assert_eq!(run_span("", ""), None); // Both empty
94    }
95
96    #[test]
97    fn test_is_subsequence_consumes_main_seq() {
98        let sub: Vec<char> = vec!['a', 'a', 'a'];
99        let main_seq: Vec<char> = vec!['a', 'a'];
100        assert!(!is_subsequence(&sub, &main_seq), "is_subsequence should return false when main_seq is consumed before sub is fully matched");
101
102        let sub_ok: Vec<char> = vec!['a', 'a'];
103        let main_seq_ok: Vec<char> = vec!['a', 'a', 'a'];
104        assert!(
105            is_subsequence(&sub_ok, &main_seq_ok),
106            "is_subsequence should return true when sub is shorter or equal and matches"
107        );
108
109        let sub_diff: Vec<char> = vec!['a', 'b'];
110        let main_seq_diff: Vec<char> = vec!['a', 'a'];
111        assert!(
112            !is_subsequence(&sub_diff, &main_seq_diff),
113            "is_subsequence should return false for non-matching elements"
114        );
115    }
116
117    // Helper function to check if `sub` is a subsequence of `main_seq`
118    fn is_subsequence<A: PartialEq>(sub: &[A], main_seq: &[A]) -> bool {
119        let mut main_iter = main_seq.iter();
120        sub.iter()
121            .all(|sub_item| main_iter.any(|main_item| main_item == sub_item))
122    }
123
124    // Use proptest! with a custom config and closure syntax
125    proptest! {
126        #![proptest_config(ProptestConfig::with_cases(10000))]
127        #[test]
128        fn prop_span_finds_valid_subsequence(
129            // Query uses chars 'a'..'c' via regex strategy, length 0..10
130            query in proptest::collection::vec("[a-c]", 0..10),
131            // History uses chars 'a'..'d' via regex strategy, length 0..50
132            history in proptest::collection::vec("[a-d]", 0..50)
133        ) {
134            if let Some((from, to)) = minspan::span(&query, &history) {
135                // 1. Indices must be valid
136                prop_assert!(from <= to, "from index {} > to index {}", from, to);
137                prop_assert!(to < history.len(), "to index {} is out of bounds for history len {}", to, history.len());
138
139                // 2. The slice history[from..=to] must contain query as a subsequence
140                let history_slice = &history[from..=to];
141                prop_assert!(
142                    is_subsequence(&query, history_slice),
143                    "query {:?} is not a subsequence of history slice {:?} (from={}, to={})",
144                    query, history_slice, from, to
145                );
146
147                // 3. Check if a shorter span exists *before* the found one (minimality check part 1)
148                // This is tricky to fully verify without re-implementing the logic.
149                // We can check that no shorter valid subsequence exists ending at or before `to`.
150                // A simpler check: ensure the first element of query matches history[from]
151                // if query is not empty. This isn't always true for the *minimal* span,
152                // e.g., query="ab", history="axb", span=(1,2) 'xb', not (0,2) 'axb'.
153
154                // 3. Minimality Check: Ensure no shorter span contains the query.
155                // Iterate through all possible start (f) and end (t) indices.
156                for f in 0..history.len() {
157                    for t in f..history.len() {
158                        // Check only spans that are strictly shorter than the found span (to - from).
159                        if t - f < to - from {
160                            let shorter_slice = &history[f..=t];
161                            prop_assert!(
162                                !is_subsequence(&query, shorter_slice),
163                                "Found shorter span history[{}:{}] ({:?}) that contains query {:?}, but span returned ({}, {}) len {}",
164                                f, t, shorter_slice, query, from, to, to - from + 1
165                            );
166                        }
167                    }
168                }
169
170            } else {
171                // If span returns None, then query should not be a subsequence of history,
172                // unless the query itself is empty (as empty query is always a subsequence).
173                 prop_assert!(
174                    !is_subsequence(&query, &history) || query.is_empty(),
175                    "span returned None, but query {:?} IS a subsequence of history {:?}",
176                    query, history
177                );
178            }
179        }
180    }
181}