Source code for analysis

"""Small, deterministic text analysis functions used by the workshop."""

import re
from collections import Counter
from pathlib import Path


[docs] def tokenize(text: str) -> list[str]: """Extract case-insensitive Unicode words from raw text. Letters and internal apostrophes are retained. Numbers and underscores separate words. Markdown links, code and markup are not removed. :param text: Text to analyze. :returns: Lowercase words in their original order. """ normalized = text.casefold().replace("’", "'").replace("ʼ", "'") return re.findall(r"[^\W\d_]+(?:'[^\W\d_]+)*", normalized)
[docs] def analyze_text(text: str, top: int = 10) -> dict: """Count words and select the most frequent ones. Ties are sorted alphabetically for deterministic output. An empty text has zero words. Common words are included; no stop-word list is applied. :param text: Raw input text, including any Markdown syntax. :param top: Positive number of frequent words to include. :returns: Total count, unique count and a list of frequent words. :raises ValueError: If top is not a positive integer. """ if isinstance(top, bool) or not isinstance(top, int) or top < 1: raise ValueError("top must be a positive integer") words = tokenize(text) counts = Counter(words) ranked = sorted(counts.items(), key=lambda item: (-item[1], item[0])) return { "total_words": len(words), "unique_words": len(counts), "top_words": [ {"word": word, "count": count} for word, count in ranked[:top] ], }
[docs] def analyze_file(path: Path, top: int = 10) -> dict: """Read a UTF-8 document and attach its filename to the statistics. :param path: Path of an existing UTF-8 text file. :param top: Number of frequent words to include. :returns: A versioned record suitable for JSON serialization. :raises OSError: If the file cannot be read. :raises UnicodeError: If the input is not UTF-8. :raises ValueError: If top is invalid. """ text = path.read_text(encoding="utf-8-sig") return {"schema_version": 1, "source": path.name, **analyze_text(text, top)}