seqex
Regex-like pattern matching for arbitrary sequences. Instead of matching characters against character classes, match elements of any type against predicate functions.
Built on an NFA engine (Thompson's construction) — O(n × m) guaranteed, no exponential backtracking.
Install
[]
= "0.1"
Quick start
use Pattern;
let is_even = ;
let is_odd = ;
// Build a pattern and compile it
let matcher = new
.followed_by
.followed_by
.compile;
// Find all non-overlapping matches
let results = matcher.find_all;
// → [{ start: 0, end: 2, data: [2, 3, 4] },
// { start: 3, end: 5, data: [6, 7, 8] }]
API
Building patterns
Start a pattern with Pattern::new() or Pattern::any(), then chain methods to describe the shape you're looking for.
Pattern::new(fn) — start with a predicate
new
Pattern::<T>::any() — start with a wildcard (matches any element)
any
.followed_by(fn | pattern) — append a predicate or sub-pattern
new.followed_by.followed_by
// Sub-patterns work too
let prefix = new.followed_by;
new.followed_by
.followed_by_any() — append a wildcard
new.followed_by_any.followed_by
Quantifiers
Quantifiers modify the last element in the pattern. Greedy variants (_greedy) accept an explicit boolean parameter.
| Method | Regex equivalent | Description |
|---|---|---|
.one_or_more() |
+ |
One or more |
.zero_or_more() |
* |
Zero or more |
.optional() |
? |
Zero or one |
.times(n) |
{n} |
Exactly n |
.between(min, max) |
{min,max} |
Between min and max |
.at_least(min) |
{min,} |
Min to unbounded |
// One or more even numbers followed by an odd
new.one_or_more.followed_by
// Exactly 3 positive numbers
new.times
// Between 2 and 5 elements
new.between
Greedy vs lazy
By default, quantifiers are greedy (match as many elements as possible). Use the _greedy(false) variants for lazy matching (match as few as possible).
// Greedy: consumes as many positives as possible
new.one_or_more.followed_by
// On [1, 2, 3] → matches [1, 2, 3]
// Lazy: consumes as few positives as possible
new.one_or_more_greedy.followed_by
// On [1, 2, 3] → matches [1, 2]
Alternation
.or(fn | pattern) — match this pattern or another
// Match a positive or negative number
new.or
// Alternation with complex sub-patterns
new.followed_by
.or
Pattern::one_of(alternatives) — multi-way alternation
Cleaner syntax for 3+ branches. Accepts any mix of Pat::new(fn) predicates and Pattern instances (via .into()).
use Pat;
one_of
// With sub-patterns
one_of
// Composable — chain quantifiers, followed_by, etc.
one_of
.one_or_more
.followed_by
Anchors
.at_start() — anchor to the beginning of the sequence
new
.at_start
.compile
.find_all // → [{ start: 0, end: 0, data: [2] }]
.find_all // → []
.at_end() — anchor to the end of the sequence
new
.at_end
.compile
.find_all // → [{ start: 2, end: 2, data: [4] }]
Compiling and matching
.compile() — compile the pattern into a Matcher
Patterns are consumed on compilation (Rust move semantics). Call .compile() to get a Matcher that can be used repeatedly against different sequences.
let matcher = new.compile;
matcher.find_all(slice) — find all non-overlapping matches
Returns a Vec<MatchResult<T>> with start, end, and data fields.
matcher.find_all
// → [{ start: 1, end: 1, data: [2] },
// { start: 3, end: 3, data: [4] },
// { start: 5, end: 5, data: [6] }]
matcher.find_all_iter(iter) — find all matches from an iterator
Accepts any IntoIterator<Item = T>. Useful for collections like BTreeSet, HashMap, or custom iterators.
use BTreeSet;
let set: = .into_iter.collect;
matcher.find_all_iter
matcher.find(slice) / matcher.find_iter(iter) — find the first match
Returns Option<MatchResult<T>>. The iterator variant stops consuming elements as soon as a match is found.
matcher.find // → Some({ start: 1, end: 1, data: [2] })
matcher.find // → None
matcher.test(slice) / matcher.test_iter(iter) — check if any match exists
Returns a bool.
matcher.test // → true
matcher.test // → false
Streaming
For data that arrives incrementally (event streams, network packets, sensor readings), use the push-based scanner API.
matcher.scanner() — create a streaming scanner
let mut scanner = matcher.scanner;
for event in event_source
// Signal end-of-stream to flush pending matches (greedy, at_end anchors)
for m in scanner.end
push(element) advances the NFA simulation by one element and returns any matches that have become definitive. end() signals that no more elements will arrive, resolving pending greedy matches and at_end anchors.
For greedy patterns, matches are held until the greedy quantifier can no longer extend (i.e., the simulation dies). For lazy patterns, matches emit from push() as early as possible.
Works with any type
The library is generic over T: Clone + 'static — match numbers, strings, structs, enums, or anything else.
// Strings
let matcher = new
.followed_by
.compile;
let seq = vec!;
matcher.find_all
// → [{ start: 0, end: 1, data: ["apple", "banana"] },
// { start: 2, end: 3, data: ["ant", "elephant"] }]
// Structs
let matcher = new
.one_or_more
.followed_by
.compile;
Advanced patterns
Each predicate sees a single element in isolation. For patterns that depend on relationships between elements, there are two approaches.
Pre-processing
Transform the sequence so each element carries the context it needs. This is pure and composable.
// Detect runs of 3+ strictly increasing numbers
let nums = vec!;
let pairs: = nums.windows
.map
.collect;
let m = new
.at_least
.compile;
m.find_all // finds [1,3,5] and [2,4,8,12]
Closure variables
When a later predicate needs to reference what an earlier one matched, use shared mutable state. The NFA evaluates predicates left-to-right during simulation, so the ordering is reliable.
use RefCell;
use Rc;
let open_name = new;
let on1 = clone;
let on2 = clone;
let m = new
.followed_by
.followed_by
.compile;
Tests
How it works
- The fluent
Patternbuilder constructs an AST (abstract syntax tree) of pattern nodes .compile()converts the AST into an NFA (nondeterministic finite automaton) using Thompson's construction- The matching engine simulates the NFA using the standard Thompson algorithm — tracking all active states simultaneously
- Each element is tested against predicate transitions on active states to advance the simulation
- This gives O(n × m) time complexity where n is the sequence length and m is the pattern size — no pathological backtracking cases
For slices, the engine runs the full simulation in a tight loop. For iterators and the streaming scanner, the same NFA simulation is broken into per-element steps with buffer-and-replay for find_all semantics.