diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 712edab..ad1a7f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.11", "3.12"] + python-version: ["3.11", "3.12"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/fixit/__init__.py b/fixit/__init__.py index f3770bc..ab9ffcc 100644 --- a/fixit/__init__.py +++ b/fixit/__init__.py @@ -1,3 +1,4 @@ """fixit - a tiny Python CLI toolbox made for practicing open source contributions.""" __version__ = "0.1.0" + diff --git a/fixit/cli.py b/fixit/cli.py index 7c08757..a9a63c8 100644 --- a/fixit/cli.py +++ b/fixit/cli.py @@ -54,7 +54,7 @@ def run_uuid(args: argparse.Namespace) -> None: def run_json(args: argparse.Namespace) -> None: try: - print(jsonfmt.format_json(args.raw)) + print(jsonfmt.format_json(args.raw, minify=args.minify)) except ValueError as e: raise ValueError(f"Error: invalid JSON - {e}") @@ -139,6 +139,11 @@ def build_parser() -> argparse.ArgumentParser: js = sub.add_parser("json", help="pretty-print JSON") js.add_argument("raw") + js.add_argument( + "--minify", + action="store_true", + help="Minify JSON output by removing whitespace and indentation", + ) pal = sub.add_parser("palindrome", help="check if text is a palindrome") pal.add_argument("text") diff --git a/fixit/jsonfmt.py b/fixit/jsonfmt.py index 89ba00f..a2dc4b1 100644 --- a/fixit/jsonfmt.py +++ b/fixit/jsonfmt.py @@ -3,14 +3,16 @@ import json -def format_json(raw: str, indent: int = 2) -> str: - """Format a JSON string with indentation. +def format_json(raw: str, indent: int = 2, minify: bool = False) -> str: + """Format a JSON string with indentation or minify it. Example: format_json('{"name":"Sam"}') returns '{\n "name": "Sam"\n}'. """ try: data = json.loads(raw) + if minify: + return json.dumps(data, separators=(",", ":")) return json.dumps(data, indent=indent) except json.JSONDecodeError: raise ValueError("Invalid JSON") diff --git a/fixit/roman.py b/fixit/roman.py new file mode 100644 index 0000000..ed83f5a --- /dev/null +++ b/fixit/roman.py @@ -0,0 +1,50 @@ +"""Roman numeral conversion module with validation.""" + +def to_roman(n: int) -> str: + """Convert an integer to a Roman numeral (1 to 3999).""" + if not isinstance(n, int) or not (1 <= n <= 3999): + raise ValueError("Input must be an integer between 1 and 3999") + + val = [ + 1000, 900, 500, 400, + 100, 90, 50, 40, + 10, 9, 5, 4, 1 + ] + syb = [ + "M", "CM", "D", "CD", + "C", "XC", "L", "XL", + "X", "IX", "V", "IV", + "I" + ] + roman_num = "" + i = 0 + while n > 0: + for _ in range(n // val[i]): + roman_num += syb[i] + n -= val[i] + i += 1 + return roman_num + +def from_roman(s: str) -> int: + """Convert a Roman numeral string to an integer.""" + if not isinstance(s, str): + raise TypeError("Input must be a string not a valid Roman numeral") + + # Normalize to uppercase to support lowercase inputs like 'mcmxciv' + s = s.upper() + + # Check for invalid characters or basic structural violations + import re + # Pattern to validate standard Roman numerals from 1 to 3999 + pattern = r"^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$" + if not re.match(pattern, s): + raise ValueError(f"'{s}' is not a valid Roman numeral") + + roman_map = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} + int_val = 0 + for i in range(len(s)): + if i > 0 and roman_map[s[i]] > roman_map[s[i - 1]]: + int_val += roman_map[s[i]] - 2 * roman_map[s[i - 1]] + else: + int_val += roman_map[s[i]] + return int_val \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 532b78e..1917de5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -30,7 +30,6 @@ 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"]) @@ -43,3 +42,8 @@ def test_top_words_command_defaults_to_three(capsys): assert result == 0 assert capsys.readouterr().out == "one: 1\ntwo: 1\nthree: 1\n" + +def test_json_minify_command(capsys): + result = main(["json", '{"b": 2, "a": 1}', "--minify"]) + assert result == 0 + assert capsys.readouterr().out.strip() == '{"b":2,"a":1}' diff --git a/tests/test_jsonfmt.py b/tests/test_jsonfmt.py index dbdb514..53ea0fd 100644 --- a/tests/test_jsonfmt.py +++ b/tests/test_jsonfmt.py @@ -10,3 +10,6 @@ def test_format_json(): def test_invalid_json_friendly_error(): with pytest.raises(ValueError, match="Invalid JSON"): format_json("{not json") + +def test_format_json_minify(): + assert format_json('{"a": 1, "b": 2}', minify=True) == '{"a":1,"b":2}'