Coverage for src/seqrule/generators/patterns.py: 11%
54 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-27 10:39 -0600
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-27 10:39 -0600
1"""
2Pattern-based sequence generation.
4This module provides functionality for pattern-based sequence generation
5and pattern matching.
6"""
8from typing import Any, Dict, List
11class PropertyPattern:
12 """
13 Represents a pattern of property values to match or generate from.
15 A PropertyPattern tracks values of a specific property across a sequence,
16 and can be used to verify patterns or predict next values.
17 """
19 def __init__(self, property_name: str, values: List[Any], is_cyclic: bool = False):
20 """
21 Initialize a property pattern.
23 Args:
24 property_name: The name of the property to track
25 values: The expected sequence of values for this property
26 is_cyclic: Whether the pattern repeats cyclically
27 """
28 self.property_name = property_name
29 self.values = values
30 self.is_cyclic = is_cyclic
32 def _get_property_value(self, obj: Any) -> Any:
33 """Get property value from either AbstractObject or dict."""
34 if hasattr(obj, "properties"):
35 return obj.properties.get(self.property_name)
36 elif hasattr(obj, "__getitem__"):
37 try:
38 return obj[self.property_name]
39 except (KeyError, TypeError):
40 return None
41 return getattr(obj, self.property_name, None)
43 def matches(self, sequence: List[Dict[str, Any]], start_idx: int = 0) -> bool:
44 """Check if the sequence matches the pattern starting from start_idx."""
45 if not sequence:
46 return True
48 # For cyclic patterns, we need to check each position relative to the pattern start
49 if self.is_cyclic:
50 # Special case for single-value patterns
51 if len(self.values) == 1:
52 expected_value = self.values[0]
53 return all(
54 self._get_property_value(obj) == expected_value
55 for obj in sequence[start_idx:]
56 )
58 # Check each position against the pattern
59 pattern_pos = 0
60 for i in range(start_idx, len(sequence)):
61 obj = sequence[i]
62 expected_value = self.values[pattern_pos]
63 actual_value = self._get_property_value(obj)
65 if actual_value != expected_value:
66 return False
68 # Move to next position in pattern, wrapping around if needed
69 pattern_pos = (pattern_pos + 1) % len(self.values)
71 return True
72 else:
73 # Non-cyclic pattern: must match exactly
74 pattern_length = len(self.values)
75 remaining_length = len(sequence) - start_idx
77 # Pattern is longer than remaining sequence
78 if pattern_length > remaining_length:
79 return False
81 # Check each position against the pattern
82 for i in range(pattern_length):
83 obj = sequence[start_idx + i]
84 expected_value = self.values[i]
85 actual_value = self._get_property_value(obj)
87 if actual_value != expected_value:
88 return False
90 return True
92 def get_next_value(self, sequence: List[Dict[str, Any]]) -> Any:
93 """Predict the next value based on the pattern and current sequence."""
94 if not self.values:
95 return None
97 # For single-value patterns, always return that value
98 if len(self.values) == 1:
99 return self.values[0]
101 # For cyclic patterns, calculate the next position
102 if self.is_cyclic:
103 next_pos = len(sequence) % len(self.values)
104 return self.values[next_pos]
106 # For non-cyclic patterns, if we've reached the end, return None
107 matched_length = min(len(sequence), len(self.values))
108 if matched_length >= len(self.values):
109 return None
111 # Otherwise, return the next value in the pattern
112 return self.values[matched_length]