Add a max_length option to slugify
Done- Plan (done)
- Code (done)
- Test (done)
- Review (done)
- Finish (done)
Cost
$0.0071
Tests
Passed
Iterations
1
Re-plans
0
LLM calls
12
Prompt read from cache
76%
Usage by agent
- Cache read
- Cache write
- Input
- Output
View as table
| Agent | Cost | Cache read | Cache write | Input | Output |
|---|---|---|---|---|---|
| planner | $0.0024 | 8,183 | 2,935 | 12 | 3,842 |
| coder | $0.0019 | 16,119 | 4,539 | 10 | 2,391 |
| tester | $0.0028 | 21,891 | 7,217 | 10 | 3,344 |
Timeline
-
planner iteration 1 Finished 4 calls · 15.0K tokens · $0.0024 · 19.3s
Loading…
-
coder iteration 1 Finished 4 calls · 23.1K tokens · $0.0019 · 13.9s
Loading…
-
tester iteration 1 Finished 4 calls · 32.5K tokens · $0.0028 · 21.1s
Loading…
-
verify iteration 1 Passed 0.6s
................. [100%] 17 passed in 0.04s
Final diff
diff --git a/slug.py b/slug.py index c4d3700..82dd8d3 100644 --- a/slug.py +++ b/slug.py @@ -1,5 +1,20 @@ import re +from typing import Optional -def slugify(text: str) -> str: - return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") +def slugify(text: str, max_length: Optional[int] = None) -> str: + slug = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") + if max_length is None: + return slug + if max_length < 0: + raise ValueError("max_length must be non-negative") + if len(slug) <= max_length: + return slug + # The slug has no leading, trailing or doubled hyphens, so cutting at any + # hyphen at or before max_length yields a whole-word prefix with no + # trailing hyphen. + idx = slug.rfind("-", 0, max_length + 1) + if idx != -1: + return slug[:idx] + # The first word alone is longer than max_length: hard-cut it. + return slug[:max_length] diff --git a/test_slug.py b/test_slug.py index 73209ea..8097461 100644 --- a/test_slug.py +++ b/test_slug.py @@ -3,3 +3,125 @@ from slug import slugify def test_basic(): assert slugify("Hello, World!") == "hello-world" + + +def test_no_max_length_unchanged(): + assert slugify("the quick brown fox") == "the-quick-brown-fox" + + +def test_short_slug_unchanged(): + assert slugify("ab cd", 5) == "ab-cd" + assert slugify("ab cd", 50) == "ab-cd" + + +def test_cut_at_word_boundary(): + assert slugify("the quick brown fox", 10) == "the-quick" + + +def test_exact_fit_at_boundary(): + assert slugify("ab cd", 5) == "ab-cd" + + +def test_hyphen_exactly_at_max_length(): + # "abcd" is 4 chars; the hyphen sits at index 4 == max_length. + assert slugify("abcd efgh", 4) == "abcd" + + +def test_long_first_word_hard_cut(): + assert slugify("abcdefghij", 4) == "abcd" + + +def test_no_trailing_hyphen_after_cut(): + result = slugify("ab cd ef", 3) + assert result == "ab" + assert not result.endswith("-") + + +def test_max_length_zero(): + assert slugify("hello world", 0) == "" + assert slugify("", 0) == "" + + +def test_empty_with_max_length(): + assert slugify("", 5) == "" + + +def test_negative_max_length_raises(): + import pytest + + with pytest.raises(ValueError): + slugify("hello", -1) + + +def test_length_never_exceeds_max_length(): + text = "a bb ccc dddd eeeee ffffff" + for n in range(0, 30): + result = slugify(text, n) + assert len(result) <= n + assert not result.endswith("-") + + +def _original_slugify(text): + import re + + return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") + + +def test_default_matches_original_implementation(): + samples = [ + "Hello, World!", + " --Leading and trailing-- ", + "Café déjà vu & naïve ünïcode", + "", + "123 456-789", + "!!!", + "a" * 50, + ] + for text in samples: + assert slugify(text) == _original_slugify(text) + assert slugify(text, None) == _original_slugify(text) + + +def test_longest_whole_word_prefix_is_chosen(): + # "the-quick-brown" is 15 chars, so within 14 the longest whole-word prefix is "the-quick". + assert slugify("the quick brown fox", 15) == "the-quick-brown" + assert slugify("the quick brown fox", 14) == "the-quick" + + +def test_hard_cut_when_first_word_too_long_with_more_words(): + assert slugify("abcdefghij kl", 4) == "abcd" + assert slugify("abcdefghij-kl-mn", 9) == "abcdefghi" + + +def test_non_ascii_and_punctuation_with_max_length(): + # "Café déjà vu" slugifies to "caf-d-j-vu"; within 6 the longest whole-word prefix is "caf-d". + assert slugify("Café déjà vu", 6) == "caf-d" + assert slugify("Café déjà vu", 3) == "caf" + assert slugify("Hello, World!", 5) == "hello" + + +def test_property_prefix_of_full_slug_on_word_boundary(): + texts = [ + "the quick brown fox jumps over the lazy dog", + "a bb ccc dddd eeeee ffffff", + "x1 y22 z333 w4444", + ] + for text in texts: + full = slugify(text) + for n in range(0, len(full) + 3): + result = slugify(text, n) + assert len(result) <= n + assert not result.endswith("-") + if len(full) <= n: + assert result == full + else: + # Must be a whole-word prefix: next char in full is a hyphen. + assert full.startswith(result) + if len(full.split("-")[0]) > n: + # First word alone exceeds n: hard cut, no boundary expected. + assert result == full[:n] + continue + assert full[len(result)] == "-" + # And it must be the longest such prefix. + nxt = full.find("-", len(result) + 1) + assert nxt == -1 or nxt > n