From a5eaedc9a1986547232af1f5540e46fedc009034 Mon Sep 17 00:00:00 2001 From: G-type <162748908+gtref@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:39:13 +1300 Subject: [PATCH] Add top-words command to find top words in a string. Signed-off-by: G-type <162748908+gtref@users.noreply.github.com> --- fixit/cli.py | 9 +++++++++ fixit/wordcount.py | 15 +++++++++++++++ tests/test_cli.py | 14 ++++++++++++++ tests/test_wordcount.py | 19 ++++++++++++++++++- 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/fixit/cli.py b/fixit/cli.py index a91820f..7909012 100644 --- a/fixit/cli.py +++ b/fixit/cli.py @@ -56,6 +56,10 @@ def run_palindrome(args): def run_reverse(args): print(textutils.reverse_words(args.text)) +def run_top_words(args): + for word, count in wordcount.top_words(args.text, args.n): + print(f"{word}: {count}") + COMMAND_TABLE = { "wc": run_wc, "convert": run_convert, @@ -64,6 +68,7 @@ def run_reverse(args): "json": run_json, "palindrome": run_palindrome, "reverse": run_reverse, + "top-words": run_top_words, } def build_parser() -> argparse.ArgumentParser: @@ -103,6 +108,10 @@ def build_parser() -> argparse.ArgumentParser: rw = sub.add_parser("reverse", help="reverse the words in a sentence") rw.add_argument("text") + + top = sub.add_parser("top-words", help="show the most frequent words") + top.add_argument("text") + top.add_argument("--n", type=int, default=3, help="number of words to show (default: 3)") return parser diff --git a/fixit/wordcount.py b/fixit/wordcount.py index ede6f0d..7f87b2c 100644 --- a/fixit/wordcount.py +++ b/fixit/wordcount.py @@ -1,6 +1,8 @@ """Word, line and character counting.""" import re +from collections import Counter +from typing import List, Tuple def count_words(text: str) -> int: @@ -31,3 +33,16 @@ def count_chars(text: str) -> int: count_chars("hi!") returns 3. """ return len(text) + + +def top_words(text: str, n: int = 3) -> List[Tuple[str, int]]: + """Return up to n most frequent words and their counts. + + Words are matched case-insensitively; punctuation is treated as a + separator. Ties retain the order in which words first appeared. + """ + if n < 1: + raise ValueError("n must be at least 1") + + words = re.findall(r"\b\w+\b", text.lower()) + return Counter(words).most_common(n) diff --git a/tests/test_cli.py b/tests/test_cli.py index d60cd3e..532b78e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -29,3 +29,17 @@ def test_wc_file_not_found(capsys): assert result == 1 assert "missing_file.txt" in capsys.readouterr().err + + +def test_top_words_command(capsys): + result = main(["top-words", "Cat dog CAT bird dog.", "--n", "2"]) + + assert result == 0 + assert capsys.readouterr().out == "cat: 2\ndog: 2\n" + + +def test_top_words_command_defaults_to_three(capsys): + result = main(["top-words", "one two three four"]) + + assert result == 0 + assert capsys.readouterr().out == "one: 1\ntwo: 1\nthree: 1\n" diff --git a/tests/test_wordcount.py b/tests/test_wordcount.py index d6a9607..5db75ec 100644 --- a/tests/test_wordcount.py +++ b/tests/test_wordcount.py @@ -1,5 +1,5 @@ import pytest -from fixit.wordcount import count_chars, count_lines, count_words +from fixit.wordcount import count_chars, count_lines, count_words, top_words def test_count_words_simple(): @@ -22,3 +22,20 @@ def test_count_lines_trailing_newline(): def test_count_chars(): assert count_chars("abc") == 3 + + +def test_top_words_ignores_case_and_punctuation(): + assert top_words("Cat, dog! CAT; bird dog.", 2) == [("cat", 2), ("dog", 2)] + + +def test_top_words_preserves_first_seen_order_for_ties(): + assert top_words("pear apple pear apple") == [("pear", 2), ("apple", 2)] + + +def test_top_words_empty_text(): + assert top_words("") == [] + + +def test_top_words_requires_positive_n(): + with pytest.raises(ValueError, match="at least 1"): + top_words("hello", 0)