use std::ops::Index;
#[derive(Debug, Clone, PartialEq)]
pub struct Group {
pub index: usize,
pub is_capturing: bool,
pub name: Option<String>,
}
impl Group {
pub fn new(index: usize) -> Self {
Self {
index,
is_capturing: true,
name: None,
}
}
pub fn non_capturing() -> Self {
Self {
index: 0,
is_capturing: false,
name: None,
}
}
pub fn named(index: usize, name: String) -> Self {
Self {
index,
is_capturing: true,
name: Some(name),
}
}
}
#[derive(Debug, Clone)]
pub struct Captures<'t> {
text: &'t str,
positions: Vec<Option<(usize, usize)>>,
}
impl<'t> Captures<'t> {
pub fn new(text: &'t str, full_match: (usize, usize), num_groups: usize) -> Self {
let mut positions = vec![None; num_groups + 1];
positions[0] = Some(full_match);
Self { text, positions }
}
pub fn get(&self, index: usize) -> Option<&'t str> {
self.positions
.get(index)?
.map(|(start, end)| &self.text[start..end])
}
pub fn pos(&self, index: usize) -> Option<(usize, usize)> {
self.positions.get(index).and_then(|&pos| pos)
}
pub fn as_str(&self) -> &'t str {
self.get(0).unwrap_or("")
}
pub(crate) fn set(&mut self, index: usize, start: usize, end: usize) {
if let Some(slot) = self.positions.get_mut(index) {
*slot = Some((start, end));
}
}
pub fn len(&self) -> usize {
self.positions.len()
}
pub fn is_empty(&self) -> bool {
self.positions.is_empty()
}
pub fn iter(&self) -> CapturesIter<'_, 't> {
CapturesIter {
captures: self,
index: 0,
}
}
}
impl<'t> Index<usize> for Captures<'t> {
type Output = str;
fn index(&self, index: usize) -> &Self::Output {
self.get(index)
.unwrap_or_else(|| panic!("no capture group at index {}", index))
}
}
pub struct CapturesIter<'c, 't> {
captures: &'c Captures<'t>,
index: usize,
}
impl<'c, 't> Iterator for CapturesIter<'c, 't> {
type Item = Option<&'t str>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.captures.len() {
return None;
}
let result = self.captures.get(self.index);
self.index += 1;
Some(result)
}
}
pub struct CapturesMatches<'r, 't> {
text: &'t str,
last_end: usize,
num_groups: usize,
_phantom: std::marker::PhantomData<&'r ()>,
}
impl<'r, 't> CapturesMatches<'r, 't> {
pub fn new(text: &'t str, num_groups: usize) -> Self {
Self {
text,
last_end: 0,
num_groups,
_phantom: std::marker::PhantomData,
}
}
}
impl<'r, 't> Iterator for CapturesMatches<'r, 't> {
type Item = Captures<'t>;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_captures_basic() {
let text = "Hello, world!";
let caps = Captures::new(text, (0, 5), 0);
assert_eq!(caps.get(0), Some("Hello"));
assert_eq!(caps.as_str(), "Hello");
assert_eq!(caps.len(), 1);
}
#[test]
fn test_captures_groups() {
let text = "2026-01-22";
let mut caps = Captures::new(text, (0, 10), 3);
caps.set(1, 0, 4); caps.set(2, 5, 7); caps.set(3, 8, 10);
assert_eq!(caps.get(0), Some("2026-01-22"));
assert_eq!(caps.get(1), Some("2026"));
assert_eq!(caps.get(2), Some("01"));
assert_eq!(caps.get(3), Some("22"));
assert_eq!(caps.len(), 4);
}
#[test]
fn test_captures_indexing() {
let text = "foo=123";
let mut caps = Captures::new(text, (0, 7), 2);
caps.set(1, 0, 3); caps.set(2, 4, 7);
assert_eq!(&caps[0], "foo=123");
assert_eq!(&caps[1], "foo");
assert_eq!(&caps[2], "123");
}
#[test]
fn test_captures_pos() {
let text = "abc123";
let mut caps = Captures::new(text, (0, 6), 2);
caps.set(1, 0, 3);
caps.set(2, 3, 6);
assert_eq!(caps.pos(0), Some((0, 6)));
assert_eq!(caps.pos(1), Some((0, 3)));
assert_eq!(caps.pos(2), Some((3, 6)));
assert_eq!(caps.pos(3), None);
}
#[test]
fn test_group_types() {
let capturing = Group::new(1);
assert!(capturing.is_capturing);
assert_eq!(capturing.index, 1);
let non_capturing = Group::non_capturing();
assert!(!non_capturing.is_capturing);
let named = Group::named(2, "year".to_string());
assert!(named.is_capturing);
assert_eq!(named.name, Some("year".to_string()));
}
}