From 843a6a1c12814fa7f4bc9d77d96db0755672603a Mon Sep 17 00:00:00 2001 From: 24_PolujanovVE <24_PolujanovVE@iux.local> Date: Mon, 7 Sep 2026 22:33:51 +0300 Subject: [PATCH] =?UTF-8?q?=D1=87=D1=83=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++- pyproject.toml | 19 +++- requirements.txt | 4 + src/{text_analyzer => numstats}/__init__.py | 0 src/numstats/cli.py | 73 ++++++++++++++ src/numstats/core.py | 76 ++++++++++++++ src/text_analyzer/cli.py | 40 -------- src/text_analyzer/core.py | 28 ------ tests/test_core.py | 106 +++++++++++--------- 9 files changed, 245 insertions(+), 121 deletions(-) rename src/{text_analyzer => numstats}/__init__.py (100%) create mode 100644 src/numstats/cli.py create mode 100644 src/numstats/core.py delete mode 100644 src/text_analyzer/cli.py delete mode 100644 src/text_analyzer/core.py diff --git a/README.md b/README.md index 3219bd0..9be5452 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,21 @@ -# Text Analyzer CLI +# NumStats -CLI-инструмент на Python для анализа текстовых файлов (подсчёт строк, слов, символов и топ-N популярных слов). +CLI-инструмент для статистического анализа числовых данных из файла (по одному числу на строку). ## Установка 1. Клонируйте репозиторий и перейдите в папку проекта: -```bash -cd lb_1 \ No newline at end of file + ```bash + git clone + cd numstats + +2. Создайте и активируйте виртуальное окружение: + ```bash + python3 -m venv .venv + source .venv/bin/activate # Linux/macOS + .venv\Scripts\activate # Windows + +3. Установите пакет в редактируемом режиме: + ```bash + pip install -e . + pip install -r requirements.txt # для запуска тестов \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 768f9fa..87ae43a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,20 +3,33 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "text_analyzer" +name = "numstats" version = "0.1.0" -description = "CLI tool for analyzing text files" +description = "CLI tool for statistical analysis of numeric data from files" +readme = "README.md" requires-python = ">=3.12" +authors = [ + {name = "Your Name", email = "your@email.com"} +] dependencies = [] [project.scripts] -text-analyzer = "text_analyzer.cli:main" +numstats = "numstats.cli:main" [tool.ruff] line-length = 88 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B"] [tool.mypy] +python_version = "3.12" strict = true +warn_unused_ignores = true +warn_return_any = true +warn_unreachable = true [tool.pytest.ini_options] testpaths = ["tests"] +addopts = "--cov=src/numstats --cov-report=term-missing" diff --git a/requirements.txt b/requirements.txt index e69de29..766703f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,4 @@ +pytest>=8.2.0 +pytest-cov>=5.0.0 +mypy>=1.10.0 +ruff>=0.5.0 \ No newline at end of file diff --git a/src/text_analyzer/__init__.py b/src/numstats/__init__.py similarity index 100% rename from src/text_analyzer/__init__.py rename to src/numstats/__init__.py diff --git a/src/numstats/cli.py b/src/numstats/cli.py new file mode 100644 index 0000000..90ffd6c --- /dev/null +++ b/src/numstats/cli.py @@ -0,0 +1,73 @@ +"""Command-line interface for numstats.""" + +import argparse +import json +import sys +from pathlib import Path + +from numstats.core import analyze_numbers + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Analyze numeric data from a file (one number per line)." + ) + parser.add_argument("file", type=Path, help="Path to input file") + parser.add_argument( + "--top", type=int, default=5, help="Number of largest values to display" + ) + parser.add_argument( + "--min-len", type=float, default=0.0, + help="Minimum value to include (numbers below are ignored)" + ) + parser.add_argument( + "--format", choices=["text", "json"], default="text", + help="Output format" + ) + parser.add_argument( + "--encoding", default="utf-8", help="File encoding (default: utf-8)" + ) + + args = parser.parse_args() + + try: + stats = analyze_numbers( + args.file, + top_n=args.top, + min_len=args.min_len, + encoding=args.encoding, + ) + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + sys.exit(1) + + if args.format == "json": + # convert numbers to strings or keep as is; JSON serializable + output = { + "count": stats["count"], + "sum": stats["sum"], + "mean": stats["mean"], + "min": stats["min"], + "max": stats["max"], + "top": stats["top"], + } + print(json.dumps(output, indent=2)) + else: + print(f"Count: {stats['count']}") + print(f"Sum: {stats['sum']:.4f}" if isinstance(stats['sum'], float) else f"Sum: {stats['sum']}") + print(f"Mean: {stats['mean']:.4f}" if isinstance(stats['mean'], float) else f"Mean: {stats['mean']}") + print(f"Min: {stats['min']}") + print(f"Max: {stats['max']}") + if stats['top']: + print("Top largest values:") + for idx, val in enumerate(stats['top'], 1): + print(f" {idx}. {val}") + else: + print("No values to display.") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/numstats/core.py b/src/numstats/core.py new file mode 100644 index 0000000..b57655d --- /dev/null +++ b/src/numstats/core.py @@ -0,0 +1,76 @@ +"""Core functions for numeric analysis.""" + +from pathlib import Path +from typing import List, Tuple, Optional + + +def read_numbers( + file_path: Path, encoding: str = "utf-8", min_value: Optional[float] = None +) -> List[float]: + """ + Read numbers from a file. Each line may contain one number. + Lines that cannot be parsed as float are skipped. + """ + if not file_path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + numbers: List[float] = [] + with file_path.open(encoding=encoding) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + val = float(line) + if min_value is None or val >= min_value: + numbers.append(val) + except ValueError: + # skip non‑numeric lines (per requirement, we can ignore them) + continue + return numbers + + +def analyze_numbers( + file_path: Path, + top_n: int = 5, + min_len: int = 0, # used as minimum numeric value + encoding: str = "utf-8", +) -> dict: + """ + Perform statistical analysis on numbers from a file. + + Returns a dict with: + - count: total number of valid numbers + - sum: sum of all numbers + - mean: arithmetic mean + - min: minimum value + - max: maximum value + - top: list of (value, count?) or just values? We'll return top N largest numbers. + """ + numbers = read_numbers(file_path, encoding=encoding, min_value=float(min_len)) + if not numbers: + return { + "count": 0, + "sum": 0.0, + "mean": 0.0, + "min": None, + "max": None, + "top": [], + } + + total = sum(numbers) + count = len(numbers) + mean = total / count + min_val = min(numbers) + max_val = max(numbers) + # top N largest numbers (if top_n > 0) + top_values = sorted(numbers, reverse=True)[:top_n] + + return { + "count": count, + "sum": total, + "mean": mean, + "min": min_val, + "max": max_val, + "top": top_values, + } \ No newline at end of file diff --git a/src/text_analyzer/cli.py b/src/text_analyzer/cli.py deleted file mode 100644 index 9ca9d15..0000000 --- a/src/text_analyzer/cli.py +++ /dev/null @@ -1,40 +0,0 @@ -import argparse -import json -import sys -from pathlib import Path - -from text_analyzer.core import analyze_file - - -def main() -> None: - parser = argparse.ArgumentParser(description="Text Analyzer CLI tool") - parser.add_argument("file", type=Path, help="Path to text file") - parser.add_argument("--top", type=int, default=5, help="Top N words") - parser.add_argument("--min-len", type=int, default=1, help="Minimum word length") - parser.add_argument( - "--format", choices=["text", "json"], default="text", help="Output format" - ) - - args = parser.parse_args() - - try: - results = analyze_file(args.file, top_n=args.top, min_len=args.min_len) - except FileNotFoundError as e: - print(f"Error: {e}", file=sys.stderr) - sys.exit(1) - - if args.format == "json": - print(json.dumps(results, indent=2)) - else: - print(f"Lines: {results['lines']}") - print(f"Words: {results['words']}") - print(f"Chars: {results['chars']}") - print("Top words:") - top_words = results["top_words"] - if isinstance(top_words, list): - for word, count in top_words: - print(f" {word}: {count}") - - -if __name__ == "__main__": - main() diff --git a/src/text_analyzer/core.py b/src/text_analyzer/core.py deleted file mode 100644 index 910047d..0000000 --- a/src/text_analyzer/core.py +++ /dev/null @@ -1,28 +0,0 @@ -from collections import Counter -from pathlib import Path - - -def analyze_file( - file_path: Path, top_n: int = 5, min_len: int = 1 -) -> dict[str, object]: - # Чтение файла и сбор базовой статистики - if not file_path.exists(): - raise FileNotFoundError(f"File not found: {file_path}") - - text = file_path.read_text(encoding="utf-8") - lines = text.splitlines() - words = [ - w.lower().strip(".,!?;:\"'()") - for w in text.split() - if len(w.strip(".,!?;:\"'()")) >= min_len - ] - - counter = Counter(words) - top_words = counter.most_common(top_n) - - return { - "lines": len(lines), - "words": len(words), - "chars": len(text), - "top_words": top_words, - } diff --git a/tests/test_core.py b/tests/test_core.py index 28774db..e9a81e0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,64 +1,78 @@ -from pathlib import Path +"""Tests for core numeric analysis.""" +from pathlib import Path import pytest -from text_analyzer.core import analyze_file +from numstats.core import analyze_numbers, read_numbers -def test_basic_analysis(tmp_path: Path) -> None: - test_file = tmp_path / "test.txt" - test_file.write_text("hello world\nhello python") - - res = analyze_file(test_file) - assert res["lines"] == 2 - assert res["words"] == 4 - assert res["top_words"] == [("hello", 2), ("world", 1), ("python", 1)] +def test_read_numbers_basic(tmp_path: Path) -> None: + f = tmp_path / "data.txt" + f.write_text("1\n2.5\n3\n") + nums = read_numbers(f) + assert nums == [1.0, 2.5, 3.0] -def test_file_not_found() -> None: +def test_read_numbers_skips_non_numeric(tmp_path: Path) -> None: + f = tmp_path / "bad.txt" + f.write_text("1\na\n2\n") + nums = read_numbers(f) + assert nums == [1.0, 2.0] + + +def test_read_numbers_min_filter(tmp_path: Path) -> None: + f = tmp_path / "filter.txt" + f.write_text("1\n5\n10\n") + nums = read_numbers(f, min_value=5) + assert nums == [5.0, 10.0] + + +def test_read_numbers_file_not_found() -> None: with pytest.raises(FileNotFoundError): - analyze_file(Path("non_existent_file_123.txt")) + read_numbers(Path("nonexistent.txt")) -def test_empty_file(tmp_path: Path) -> None: - test_file = tmp_path / "empty.txt" - test_file.write_text("") - - res = analyze_file(test_file) - assert res["lines"] == 0 - assert res["words"] == 0 - assert res["top_words"] == [] +def test_analyze_numbers_empty(tmp_path: Path) -> None: + f = tmp_path / "empty.txt" + f.write_text("") + res = analyze_numbers(f) + assert res["count"] == 0 + assert res["mean"] == 0.0 + assert res["min"] is None + assert res["max"] is None + assert res["top"] == [] @pytest.mark.parametrize( - "min_len,expected_words", + "content, top_n, min_len, expected_count, expected_sum, expected_mean, expected_top", [ - (1, 4), - (3, 2), - (4, 1), + ("1\n2\n3\n", 2, 0, 3, 6.0, 2.0, [3.0, 2.0]), + ("-5\n0\n10\n", 1, 0, 3, 5.0, 5.0/3, [10.0]), + ("1\n2\n3\n4\n", 3, 2, 3, 9.0, 3.0, [4.0, 3.0, 2.0]), + ("", 5, 0, 0, 0.0, 0.0, []), ], ) -def test_min_length_filter(tmp_path: Path, min_len: int, expected_words: int) -> None: - test_file = tmp_path / "filter.txt" - test_file.write_text("a ab abc abcd") - - res = analyze_file(test_file, min_len=min_len) - assert res["words"] == expected_words +def test_analyze_numbers_parametrized( + tmp_path: Path, + content: str, + top_n: int, + min_len: int, + expected_count: int, + expected_sum: float, + expected_mean: float, + expected_top: list, +) -> None: + f = tmp_path / "param.txt" + f.write_text(content) + res = analyze_numbers(f, top_n=top_n, min_len=min_len) + assert res["count"] == expected_count + assert abs(res["sum"] - expected_sum) < 1e-9 + assert abs(res["mean"] - expected_mean) < 1e-9 + assert res["top"] == expected_top -def test_top_n_param(tmp_path: Path) -> None: - test_file = tmp_path / "top.txt" - test_file.write_text("one two two three three three") - - res = analyze_file(test_file, top_n=2) - top_words = res["top_words"] - assert isinstance(top_words, list) - assert len(top_words) == 2 - - -def test_punctuation_stripping(tmp_path: Path) -> None: - test_file = tmp_path / "punct.txt" - test_file.write_text("hello, world! hello...") - - res = analyze_file(test_file) - assert res["top_words"] == [("hello", 2), ("world", 1)] +def test_analyze_numbers_encoding(tmp_path: Path) -> None: + f = tmp_path / "utf16.txt" + f.write_text("1\n2\n3\n", encoding="utf-16") + res = analyze_numbers(f, encoding="utf-16") + assert res["count"] == 3 \ No newline at end of file