use crate::parser::quantifier::Quantifier;
use crate::parser::sequence::Sequence;
use crate::parser::sequence_parser::{is_sequence_pattern, parse_sequence};
#[derive(Debug, Clone, PartialEq)]
pub struct Group {
pub content: GroupContent,
pub capturing: bool,
pub quantifier: Option<Quantifier>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum GroupContent {
Single(String),
Alternation(Vec<String>),
Sequence(Sequence),
ParsedAlternation(Vec<Sequence>),
}
impl Group {
pub fn new_capturing(content: GroupContent) -> Self {
Group {
content,
capturing: true,
quantifier: None,
}
}
pub fn new_non_capturing(content: GroupContent) -> Self {
Group {
content,
capturing: false,
quantifier: None,
}
}
pub fn with_quantifier(mut self, quantifier: Quantifier) -> Self {
self.quantifier = Some(quantifier);
self
}
pub fn match_at(&self, text: &str, pos: usize) -> Option<usize> {
let base_consumed = self.match_base_at(text, pos)?;
if let Some(quantifier) = &self.quantifier {
self.match_with_quantifier(text, pos, base_consumed, quantifier)
} else {
Some(base_consumed)
}
}
fn match_base_at(&self, text: &str, pos: usize) -> Option<usize> {
let remaining = &text[pos..];
match &self.content {
GroupContent::Single(pattern) => {
if remaining.starts_with(pattern) {
Some(pattern.len())
} else {
None
}
}
GroupContent::Alternation(patterns) => {
for pattern in patterns {
if remaining.starts_with(pattern) {
return Some(pattern.len());
}
}
None
}
GroupContent::Sequence(seq) => seq.match_at(remaining),
GroupContent::ParsedAlternation(sequences) => {
for seq in sequences {
if let Some(consumed) = seq.match_at(remaining) {
return Some(consumed);
}
}
None
}
}
}
fn match_with_quantifier(
&self,
text: &str,
start_pos: usize,
_base_match_size: usize,
quantifier: &Quantifier,
) -> Option<usize> {
let (min, max) = quantifier_bounds(quantifier);
let mut total_consumed = 0;
let mut count = 0;
let mut pos = start_pos;
while count < max {
match self.match_base_at(text, pos) {
Some(consumed) if consumed > 0 => {
total_consumed += consumed;
pos += consumed;
count += 1;
}
_ => break,
}
}
if count >= min {
Some(total_consumed)
} else {
None
}
}
pub fn is_match(&self, text: &str) -> bool {
if self.match_at(text, 0).is_some() {
return true;
}
let byte_positions: Vec<usize> = text.char_indices().map(|(i, _)| i).collect();
for &start_pos in &byte_positions {
if start_pos == 0 {
continue; }
if self.match_at(text, start_pos).is_some() {
return true; }
}
false
}
pub fn find(&self, text: &str) -> Option<(usize, usize)> {
if let GroupContent::Alternation(alternatives) = &self.content {
if let Some(prefix) = find_common_prefix(alternatives) {
if prefix.len() >= 3 {
use memchr::memmem;
let mut search_pos = 0;
while let Some(found) =
memmem::find(&text.as_bytes()[search_pos..], prefix.as_bytes())
{
let abs_pos = search_pos + found;
if let Some(consumed) = self.match_at(text, abs_pos) {
return Some((abs_pos, abs_pos + consumed));
}
search_pos = abs_pos + 1;
}
return None;
}
}
}
let byte_positions: Vec<usize> = text.char_indices().map(|(i, _)| i).collect();
for &start_pos in &byte_positions {
if let Some(consumed) = self.match_at(text, start_pos) {
return Some((start_pos, start_pos + consumed));
}
}
None
}
pub fn find_all(&self, text: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let byte_positions: Vec<usize> = text.char_indices().map(|(i, _)| i).collect();
let mut i = 0;
while i < byte_positions.len() {
let start_pos = byte_positions[i];
if let Some(consumed) = self.match_at(text, start_pos) {
let end_pos = start_pos + consumed;
results.push((start_pos, end_pos));
while i < byte_positions.len() && byte_positions[i] < end_pos {
i += 1;
}
} else {
i += 1;
}
}
results
}
}
fn find_common_prefix(alternatives: &[String]) -> Option<String> {
if alternatives.is_empty() {
return None;
}
let first = &alternatives[0];
let mut prefix_len = first.len();
for alt in &alternatives[1..] {
let common = first
.chars()
.zip(alt.chars())
.take_while(|(a, b)| a == b)
.count();
prefix_len = prefix_len.min(common);
if prefix_len == 0 {
return None;
}
}
if prefix_len > 0 {
Some(first.chars().take(prefix_len).collect())
} else {
None
}
}
fn quantifier_bounds(q: &Quantifier) -> (usize, usize) {
match q {
Quantifier::ZeroOrMore | Quantifier::ZeroOrMoreLazy => (0, usize::MAX),
Quantifier::OneOrMore | Quantifier::OneOrMoreLazy => (1, usize::MAX),
Quantifier::ZeroOrOne | Quantifier::ZeroOrOneLazy => (0, 1),
Quantifier::Exactly(n) => (*n, *n),
Quantifier::AtLeast(n) => (*n, usize::MAX),
Quantifier::Between(n, m) => (*n, *m),
}
}
pub fn parse_group(pattern: &str) -> Result<(Group, usize), String> {
if !pattern.starts_with('(') {
return Err("Pattern must start with '('".to_string());
}
let mut depth = 0;
let mut close_idx = None;
for (i, ch) in pattern.char_indices() {
if ch == '(' {
depth += 1;
} else if ch == ')' {
depth -= 1;
if depth == 0 {
close_idx = Some(i);
break;
}
}
}
let close_idx = close_idx.ok_or("Unclosed group")?;
let group_str = &pattern[1..close_idx];
let (is_capturing, content_str) = if group_str.starts_with("?:") {
(false, &group_str[2..])
} else {
(true, group_str)
};
let content = if content_str.contains('|') {
let parts: Vec<String> = content_str.split('|').map(|s| s.to_string()).collect();
let has_sequences = parts
.iter()
.any(|p| is_sequence_pattern(p) || has_quantified_element(p));
if has_sequences {
GroupContent::Alternation(parts)
} else {
GroupContent::Alternation(parts)
}
} else if is_sequence_pattern(content_str) || has_quantified_element(content_str) {
match parse_sequence(content_str) {
Ok(seq) => GroupContent::Sequence(seq),
Err(_) => GroupContent::Single(content_str.to_string()),
}
} else {
GroupContent::Single(content_str.to_string())
};
let group = if is_capturing {
Group::new_capturing(content)
} else {
Group::new_non_capturing(content)
};
let mut bytes_consumed = close_idx + 1;
if bytes_consumed < pattern.len() {
let remaining = &pattern[bytes_consumed..];
let (quantifier_opt, qlen) = parse_quantifier_with_lazy(remaining);
if let Some(quantifier) = quantifier_opt {
bytes_consumed += qlen;
return Ok((group.with_quantifier(quantifier), bytes_consumed));
}
}
Ok((group, bytes_consumed))
}
fn parse_quantifier_with_lazy(remaining: &str) -> (Option<Quantifier>, usize) {
let chars: Vec<char> = remaining.chars().take(2).collect();
if chars.is_empty() {
return (None, 0);
}
let first = chars[0];
let has_lazy = chars.len() > 1 && chars[1] == '?';
match first {
'*' if has_lazy => (Some(Quantifier::ZeroOrMoreLazy), 2),
'*' => (Some(Quantifier::ZeroOrMore), 1),
'+' if has_lazy => (Some(Quantifier::OneOrMoreLazy), 2),
'+' => (Some(Quantifier::OneOrMore), 1),
'?' if has_lazy => (Some(Quantifier::ZeroOrOneLazy), 2),
'?' => (Some(Quantifier::ZeroOrOne), 1),
_ => (None, 0),
}
}
fn has_quantified_element(pattern: &str) -> bool {
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\' && i + 1 < chars.len() {
i += 2;
if i < chars.len() && matches!(chars[i], '*' | '+' | '?') {
return true;
}
} else if chars[i] == '[' {
while i < chars.len() && chars[i] != ']' {
i += 1;
}
i += 1; if i < chars.len() && matches!(chars[i], '*' | '+' | '?') {
return true;
}
} else {
i += 1;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_group() {
let (group, len) = parse_group("(abc)").unwrap();
assert_eq!(len, 5);
assert!(group.capturing);
assert!(group.quantifier.is_none());
}
#[test]
fn test_parse_non_capturing() {
let (group, len) = parse_group("(?:abc)").unwrap();
assert_eq!(len, 7);
assert!(!group.capturing);
}
#[test]
fn test_parse_alternation() {
let (group, _) = parse_group("(a|b|c)").unwrap();
match group.content {
GroupContent::Alternation(parts) => {
assert_eq!(parts.len(), 3);
assert_eq!(parts, vec!["a", "b", "c"]);
}
_ => panic!("Expected alternation"),
}
}
#[test]
fn test_parse_quantified_group() {
let (group, len) = parse_group("(abc)+").unwrap();
assert_eq!(len, 6);
assert!(group.quantifier.is_some());
}
#[test]
fn test_match_simple() {
let group = Group::new_capturing(GroupContent::Single("abc".to_string()));
assert_eq!(group.match_at("abc", 0), Some(3));
assert_eq!(group.match_at("xyzabc", 3), Some(3));
assert_eq!(group.match_at("xyz", 0), None);
}
#[test]
fn test_match_alternation() {
let group = Group::new_capturing(GroupContent::Alternation(vec![
"foo".to_string(),
"bar".to_string(),
]));
assert_eq!(group.match_at("foo", 0), Some(3));
assert_eq!(group.match_at("bar", 0), Some(3));
assert_eq!(group.match_at("baz", 0), None);
}
#[test]
fn test_find() {
let group = Group::new_capturing(GroupContent::Single("abc".to_string()));
assert_eq!(group.find("xyzabcdef"), Some((3, 6)));
}
#[test]
fn test_find_all() {
let group = Group::new_capturing(GroupContent::Single("ab".to_string()));
let matches = group.find_all("ab cd ab ef ab");
assert_eq!(matches, vec![(0, 2), (6, 8), (12, 14)]);
}
}