Skip to content

Commit 234e15b

Browse files
committed
feat: add count_patterns() and docstrings
Add count_patterns(input) -> int that returns the total number of expansions without generating them. Runs in O(number of blocks), useful for informing users how many permutations a pattern produces. Add docstrings to all public functions: expand_patterns, expand_patterns_random, and count_patterns. Ref #184
1 parent 80387f3 commit 234e15b

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

tests/test_patterns.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
from itertools import islice
3-
from user_scanner.core.patterns import expand_patterns, expand_patterns_random
3+
from user_scanner.core.patterns import expand_patterns, expand_patterns_random, count_patterns
44

55

66
# ── expand_patterns: plain text (no pattern syntax) ──────────────────────
@@ -266,3 +266,32 @@ def test_random_reservoir_sampling():
266266
def test_random_is_iterator():
267267
result = expand_patterns_random("[ab]")
268268
assert hasattr(result, "__next__")
269+
270+
271+
# ── count_patterns ───────────────────────────────────────────────────────
272+
273+
def test_count_plain_text():
274+
assert count_patterns("john") == 1
275+
276+
277+
def test_count_empty():
278+
assert count_patterns("") == 1
279+
280+
281+
def test_count_charset():
282+
assert count_patterns("[a-z]") == 26
283+
284+
285+
def test_count_with_lenset():
286+
# length 0: 1 + length 1: 26 + length 2: 676 = 703
287+
assert count_patterns("[a-z]{0-2}") == 703
288+
289+
290+
def test_count_multiple_blocks():
291+
assert count_patterns("[a-c][0-9]") == 30 # 3 * 10
292+
293+
294+
def test_count_matches_expansion():
295+
"""count_patterns must match the actual number of expansions."""
296+
pattern = "pre[ab]{1-2}mid[xy]post"
297+
assert count_patterns(pattern) == len(list(expand_patterns(pattern)))

user_scanner/core/patterns.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,30 @@ def _iter_pattern(blocks: List[Block]) -> Iterator[str]:
176176

177177

178178
def expand_patterns(input: str) -> Iterator[str]:
179+
"""Expand a pattern string into all matching combinations.
180+
181+
Syntax:
182+
[chars] — character set, e.g. [a-z], [0-9], [abc]
183+
[chars]{len} — with length control, e.g. [a-z]{0-2}, [0-9]{1;3}
184+
\\[ \\] \\\\ — escaped literal brackets and backslash
185+
186+
Examples:
187+
"john[a-c]" → "johna", "johnb", "johnc"
188+
"john[0-9]{0-2}" → "john", "john0", ..., "john99"
189+
"hello\\[world\\]" → "hello[world]"
190+
"""
179191
blocks = _parse_patterns(input)
180192
yield from _iter_pattern(blocks)
181193

182194

183195
def expand_patterns_random(input: str, capacity: int = 1000) -> Iterator[str]:
196+
"""Expand a pattern string in randomized order using reservoir sampling.
197+
198+
Yields the same set of results as expand_patterns, but in a shuffled order.
199+
For small result sets (<= capacity), all items are buffered and shuffled.
200+
For larger sets, reservoir sampling ensures uniform randomness without
201+
loading everything into memory at once.
202+
"""
184203
patterns = expand_patterns(input)
185204
buffer = []
186205

@@ -201,3 +220,18 @@ def expand_patterns_random(input: str, capacity: int = 1000) -> Iterator[str]:
201220

202221
random.shuffle(buffer)
203222
yield from buffer
223+
224+
225+
def count_patterns(input: str) -> int:
226+
"""Return the total number of expansions for a pattern without generating them.
227+
228+
Useful for showing the user how many permutations a pattern produces
229+
before actually expanding it (e.g. "Scanning 100 of 18,279 permutations").
230+
Runs in O(number of blocks) — instant even for huge patterns.
231+
"""
232+
blocks = _parse_patterns(input)
233+
total = 1
234+
for block in blocks:
235+
if isinstance(block, PatternBlock):
236+
total *= sum(len(block.charset) ** length for length in block.lenset)
237+
return total

0 commit comments

Comments
 (0)