Source code for conllu_tools.matching.sentence

"""SentencePattern class for matching token patterns in sentences."""

from __future__ import annotations

import uuid

import conllu

from .result import MatchResult
from .token import TokenPattern


[docs] class SentencePattern: """Represents a sequence of TokenPattern to match in a sentence as a whole. Attributes: name (str): The name of the SentencePattern. pattern (list[TokenPattern]): The list of TokenPatterns to match in sequence. """
[docs] def __init__(self, pattern: list[TokenPattern], name: str | None = None) -> None: """Initialize the SentencePattern.""" if not pattern: msg = 'SentencePattern requires a pattern.' raise ValueError(msg) if not isinstance(pattern, list) or not all(isinstance(p, TokenPattern) for p in pattern): msg = 'Pattern must be a list of TokenPattern instances.' raise ValueError(msg) self.name: str = name if name is not None else str(uuid.uuid4()) self.pattern: list[TokenPattern] = pattern self.current_index: int = 0 self.current_check: TokenPattern | None = None self.previous_check: TokenPattern | None = None self.matched_tokens: list[conllu.Token] = []
[docs] def reset(self) -> None: """Reset the matching state.""" self.current_index = 0 self.current_check = None self.previous_check = None self.matched_tokens = [] for pattern in self.pattern: if pattern.match_multiple: pattern.count = 0
[docs] def match(self, sentence: conllu.TokenList) -> list[MatchResult]: """Match the pattern in the given sentence. Uses a backtracking algorithm: when a partial match fails, the algorithm retries from the position after where the failed match started, ensuring all possible matches are found. """ matches = [] tokens = list(sentence) start_pos = 0 while start_pos < len(tokens): self.reset() pattern_idx = 0 pos = start_pos while pos < len(tokens) and pattern_idx < len(self.pattern): token = tokens[pos] current_pattern = self.pattern[pattern_idx] if current_pattern.test(token): self.matched_tokens.append(token) pattern_idx += 1 pos += 1 elif pattern_idx > 0 and self.pattern[pattern_idx - 1].match_multiple: # Check if previous pattern (with match_multiple) can consume this token if self.pattern[pattern_idx - 1].test(token): self.matched_tokens.append(token) pos += 1 continue # Previous pattern can't match either, break and try next start position break else: # No match - break and try next starting position break if pattern_idx >= len(self.pattern): # Complete match found matches.append( MatchResult( pattern_name=self.name, sentence_id=sentence.metadata.get('sent_id', 'unknown'), tokens=self.matched_tokens.copy(), ), ) # Move start position past the matched tokens to avoid overlapping matches start_pos = pos else: # No match starting from start_pos, try next position start_pos += 1 self.reset() return matches
[docs] def explain(self) -> str: """Provide a string explanation of the SentencePattern.""" tp_explanations = [tp.explain() for tp in self.pattern] exp = 'This pattern matches a sequence of the following token patterns:\n' for i, explanation in enumerate(tp_explanations): exp += f' Token Pattern {i + 1}: {explanation}\n' return exp
[docs] def __repr__(self) -> str: """Return a string representation of the SentencePattern.""" pattern_str = ', '.join(repr(pattern) for pattern in self.pattern) return f"SentencePattern(name='{self.name}', pattern=[{pattern_str}])"
[docs] def __str__(self) -> str: """Return a string description of the SentencePattern.""" return self.__repr__()