Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions fixit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""fixit - a tiny Python CLI toolbox made for practicing open source contributions."""

__version__ = "0.1.0"

7 changes: 6 additions & 1 deletion fixit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down Expand Up @@ -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")
Expand Down
6 changes: 4 additions & 2 deletions fixit/jsonfmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
50 changes: 50 additions & 0 deletions fixit/roman.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Expand All @@ -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}'
3 changes: 3 additions & 0 deletions tests/test_jsonfmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'