diff --git a/pyproject.toml b/pyproject.toml index 8e316e2..29f174b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,60 +74,6 @@ select = [ # https://docs.astral.sh/ruff/rules/ # TODO: enable additional linter ] [tool.pytest.ini_options] -# skip slow tests by default -addopts = "-m 'not slow'" -markers = [ - "slow: marks tests as slow", -] -xfail_strict = true - - -strict = true - -# Disallow dynamic typing -disallow_any_unimported = true -disallow_any_decorated = true -disallow_any_generics = true -disallow_subclassing_any = true - -# Untyped definitions and calls -disallow_untyped_calls = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -check_untyped_defs = true -disallow_untyped_decorators = true - -# Configuring warnings -warn_redundant_casts = true -warn_unused_ignores = true -warn_no_return = true -warn_return_any = true -warn_unreachable = true - -# Miscellaneous strictness flags -allow_untyped_globals = false -allow_redefinition = true -enable_error_code = [ # https://mypy.readthedocs.io/en/stable/error_code_list2.html#error-codes-optional - 'redundant-self', - 'redundant-expr', - 'possibly-undefined', - 'truthy-bool', - 'truthy-iterable', - 'ignore-without-code', - 'unused-awaitable', - 'unused-ignore', - 'explicit-override', - 'unimported-reveal', -] -implicit_reexport = true -strict_equality = true - -# Configuring error messages -show_column_numbers = true -pretty = true - -# Miscellaneous -warn_unused_configs = true [[tool.mypy.overrides]] # aocd doesn't provide types https://github.com/wimglenn/advent-of-code-data/issues/78 diff --git a/src/aoc/aoc2024/day_01.py b/src/aoc/aoc2024/day_01.py index 0709e0d..24f12b6 100644 --- a/src/aoc/aoc2024/day_01.py +++ b/src/aoc/aoc2024/day_01.py @@ -6,21 +6,26 @@ from src.aoc.aoc_helper import Aoc -def parse(txt: str) -> Tuple[Tuple[int, ...], Tuple[int, ...]]: - cols = tuple(zip(*(map(int, line.split()) for line in txt.splitlines()))) - if len(cols) != 2: - raise ValueError("Input must yield exactly two columns") - return cols +def parse(txt: str) -> tuple[tuple[int, ...], tuple[int, ...]]: + l, r = list( + map( + list, + zip(*[list(map(int, line.split())) for line in txt.splitlines()]), + ) + ) + return l, r def part_a(txt: str) -> int: - left, right = parse(txt) - return sum(map(abs, map(sub, sorted(left), sorted(right)))) + left_col, right_col = parse(txt) + return sum( + abs(l - r) for l, r in zip(sorted(sorted(left_col)), sorted(sorted(right_col))) + ) def part_b(txt: str) -> int: - left, right = parse(txt) - return sum(l * count for l, count in zip(left, Counter(right).values())) + l, r = parse(txt) + return sum(x * r.count(x) for x in l) def main(txt: str) -> None: diff --git a/src/aoc/aoc2024/readme.md b/src/aoc/aoc2024/readme.md index e69de29..47e7016 100644 --- a/src/aoc/aoc2024/readme.md +++ b/src/aoc/aoc2024/readme.md @@ -0,0 +1,29 @@ +# Advent of Code Solutions + +| Day | Problem | Solution | Part A | Part B | +|-----|---------|----------|---------|---------| +| 01 | [Historian Hysteria](https://adventofcode.com/2024/day/1) | [Solution](day_01.py) | :x: | :heavy_check_mark: | :heavy_check_mark: | +| 02 | [?](https://adventofcode.com/2024/day/2) | [Solution](day_02.py) | :x: | :x: | +| 03 | [?](https://adventofcode.com/2024/day/3) | [Solution](day_03.py) | :x: | :x: | +| 04 | [?](https://adventofcode.com/2024/day/4) | [Solution](day_04.py) | :x: | :x: | +| 05 | [?](https://adventofcode.com/2024/day/5) | [Solution](day_05.py) | :x: | :x: | +| 06 | [?](https://adventofcode.com/2024/day/6) | [Solution](day_06.py) | :x: | :x: | +| 07 | [?](https://adventofcode.com/2024/day/7) | [Solution](day_07.py) | :x: | :x: | +| 08 | [?](https://adventofcode.com/2024/day/8) | [Solution](day_08.py) | :x: | :x: | +| 09 | [?](https://adventofcode.com/2024/day/9) | [Solution](day_09.py) | :x: | :x: | +| 10 | [?](https://adventofcode.com/2024/day/10) | [Solution](day_10.py) | :x: | :x: | +| 11 | [?](https://adventofcode.com/2024/day/11) | [Solution](day_11.py) | :x: | :x: | +| 12 | [?](https://adventofcode.com/2024/day/12) | [Solution](day_12.py) | :x: | :x: | +| 13 | [?](https://adventofcode.com/2024/day/13) | [Solution](day_13.py) | :x: | :x: | +| 14 | [?](https://adventofcode.com/2024/day/14) | [Solution](day_14.py) | :x: | :x: | +| 15 | [?](https://adventofcode.com/2024/day/15) | [Solution](day_15.py) | :x: | :x: | +| 16 | [?](https://adventofcode.com/2024/day/16) | [Solution](day_16.py) | :x: | :x: | +| 17 | [?](https://adventofcode.com/2024/day/17) | [Solution](day_17.py) | :x: | :x: | +| 18 | [?](https://adventofcode.com/2024/day/18) | [Solution](day_18.py) | :x: | :x: | +| 19 | [?](https://adventofcode.com/2024/day/19) | [Solution](day_19.py) | :x: | :x: | +| 20 | [?](https://adventofcode.com/2024/day/20) | [Solution](day_20.py) | :x: | :x: | +| 21 | [?](https://adventofcode.com/2024/day/21) | [Solution](day_21.py) | :x: | :x: | +| 22 | [?](https://adventofcode.com/2024/day/22) | [Solution](day_22.py) | :x: | :x: | +| 23 | [?](https://adventofcode.com/2024/day/23) | [Solution](day_23.py) | :x: | :x: | +| 24 | [?](https://adventofcode.com/2024/day/24) | [Solution](day_24.py) | :x: | :x: | +| 25 | [?](https://adventofcode.com/2024/day/25) | [Solution](day_25.py) | :x: | :x: | diff --git a/src/aoc/aoc_helper.py b/src/aoc/aoc_helper.py index 7ba02af..2123858 100644 --- a/src/aoc/aoc_helper.py +++ b/src/aoc/aoc_helper.py @@ -15,7 +15,6 @@ PROJECT_ROOT = Path(__file__).parent.parent.parent - def warn(s): logging.warning(s) @@ -28,12 +27,15 @@ def info(s): class Aoc: - - def __init__(self, day: int = int(datetime.now().day), years: int = int(datetime.now().year)): + def __init__( + self, day: int = int(datetime.now().day), years: int = int(datetime.now().year) + ): self.day = day self.year = years self.data = get_data(day=self.day, year=self.year) - self.test_module = importlib.import_module(f"tests.aoc{self.year}.{self.year}_day_{self.day:02d}_test") + self.test_module = importlib.import_module( + f"tests.aoc{self.year}.{self.year}_day_{self.day:02d}_test" + ) def submit(self, answer, part=None) -> None: submit(answer, part=part, day=self.day, year=self.year) @@ -52,7 +54,13 @@ def wrapper(self, func) -> Any: self.submit(answer) return answer - def run(self, func=None, submit: bool = False, part: Union[None, str] = None, readme_update: bool = False) -> None: + def run( + self, + func=None, + submit: bool = False, + part: Union[None, str] = None, + readme_update: bool = False, + ) -> None: """Run a function and submit the answer to the website. func : Main Function to run This need to be the outside function, although it can be None @@ -69,7 +77,9 @@ def run(self, func=None, submit: bool = False, part: Union[None, str] = None, re func(self.get_data()) if submit: - modules = importlib.import_module(f"src.aoc.aoc{self.year}.day_{self.day:02d}") + modules = importlib.import_module( + f"src.aoc.aoc{self.year}.day_{self.day:02d}" + ) tests = (self.run_test("a"), self.run_test("b")) options = { "a": (0,), @@ -83,7 +93,10 @@ def run(self, func=None, submit: bool = False, part: Union[None, str] = None, re current_part = "a" if i == 0 else "b" ic(f"Part {current_part} is not passing the test cases") if tests[i]: - self.submit(getattr(modules, f"part_{current_part}")(self.get_data()), part=current_part) + self.submit( + getattr(modules, f"part_{current_part}")(self.get_data()), + part=current_part, + ) else: ic.configureOutput(outputFunction=warn) ic(f"Part {current_part} is not passing the test cases") @@ -96,37 +109,100 @@ def run_test(self, part: Literal["a", "b"]) -> bool: assert inspect.isfunction(f) try: f() + print(f"Test {part} Passed") return True except AssertionError: + print(f"Test {part} failed") return False + print(f"Test {part} failed") return False def update_readme(self) -> None: - - writeer_path = os.path.join(PROJECT_ROOT, f"src/aoc/aoc{self.year}", "readme.md") - with open(writeer_path, "r+") as f: - lines = f.readlines() - for i, line in enumerate(lines): - removed_leading_zero = str(self.day).lstrip("0") - if f"| [?](https://adventofcode.com/{self.year}/day/{removed_leading_zero})" in line: - print(f"Found line {i}") - lines[i] = line.replace( - f"| [?](https://adventofcode.com/{self.year}/day/{removed_leading_zero})", - f"| [{self.get_problem_name()}](https://adventofcode.com/{self.year}/day/{removed_leading_zero})", - ).replace(" :x: ", ":heavy_check_mark:") - - break - f.seek(0) + readme_path = os.path.join(PROJECT_ROOT, f"src/aoc/aoc{self.year}", "readme.md") + + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(readme_path), exist_ok=True) + + # Initialize table headers + table_headers = [ + "# Advent of Code Solutions\n", + "\n", + "| Day | Problem | Solution | Part A | Part B |\n", + "|-----|---------|----------|---------|---------|", + ] + + # Generate default table content for all 25 days + default_table = [] + for day in range(1, 26): + default_table.append( + f"| {day:02d} | [?](https://adventofcode.com/{self.year}/day/{day}) | [Solution](day_{day:02d}.py) | :x: | :x: |\n" + ) + + try: + # Try to read existing content + with open(readme_path, "r") as f: + lines = f.readlines() + + # If file exists but doesn't have table, create new + if not any("| Day | Problem |" in line for line in lines): + lines = table_headers + ["\n"] + default_table + + except FileNotFoundError: + # Create new file with default table + lines = table_headers + ["\n"] + default_table + + # Update the specific day's entry + removed_leading_zero = str(self.day).lstrip("0") + day_found = False + + for i, line in enumerate(lines): + if f"day/{removed_leading_zero})" in line: + day_found = True + # Get current status + current_line = line + + # Update problem name + if "[?]" in current_line: + current_line = current_line.replace( + f"[?](https://adventofcode.com/{self.year}/day/{removed_leading_zero})", + f"[{self.get_problem_name()}](https://adventofcode.com/{self.year}/day/{removed_leading_zero})", + ) + + # Update completion status based on test results + test_a_passed = self.run_test("a") + test_b_passed = self.run_test("b") + + # Replace status markers + parts = current_line.split("|") + if test_a_passed: + parts[-2] = " :heavy_check_mark: " + if test_b_passed: + parts[-1] = " :heavy_check_mark: |\n" + + lines[i] = "|".join(parts) + break + + # If day wasn't found in existing table, add it + if not day_found: + new_line = ( + f"| {self.day:02d} " + f"| [{self.get_problem_name()}](https://adventofcode.com/{self.year}/day/{removed_leading_zero}) " + f"| [Solution](day_{self.day:02d}.py) " + f"| {'✓' if self.run_test('a') else '❌'} " + f"| {'✓' if self.run_test('b') else '❌'} |\n" + ) + lines.append(new_line) + + # Write back to file + with open(readme_path, "w") as f: f.writelines(lines) - f.truncate() def run_all_tests(self) -> None: - __import__("os").system("poetry run pytest") + __import__("os").system("uv run pytest") def custom_solve(self) -> None: solve(year=self.year, day=self.day, data=self.get_data()) def get_problem_name(self) -> str: puzzle = Puzzle(year=self.year, day=self.day) - soup = puzzle._soup() - return soup.find("h2").text.replace("---", "").replace(f"Day {self.day}:", "").strip() + return puzzle.title diff --git a/tests/aoc2022/2022_day_11_test.py b/tests/aoc2022/2022_day_11_test.py deleted file mode 100644 index 8e78d67..0000000 --- a/tests/aoc2022/2022_day_11_test.py +++ /dev/null @@ -1,41 +0,0 @@ -import pytest - -from src.aoc.aoc2022 import day_11 as d - -TEST_INPUT = """ -Monkey 0: - Starting items: 79, 98 - Operation: new = old * 19 - Test: divisible by 23 - If true: throw to monkey 2 - If false: throw to monkey 3 - -Monkey 1: - Starting items: 54, 65, 75, 74 - Operation: new = old + 6 - Test: divisible by 19 - If true: throw to monkey 2 - If false: throw to monkey 0 - -Monkey 2: - Starting items: 79, 60, 97 - Operation: new = old * old - Test: divisible by 13 - If true: throw to monkey 1 - If false: throw to monkey 3 - -Monkey 3: - Starting items: 74 - Operation: new = old + 3 - Test: divisible by 17 - If true: throw to monkey 0 - If false: throw to monkey 1 -""".strip() - - -def test_a() -> None: - assert d.part_a(TEST_INPUT) == 10605 - - -def test_b() -> None: - assert d.part_b(TEST_INPUT) == 2713310158 diff --git a/tests/aoc2023/2023_day_01_test.py b/tests/aoc2023/2023_day_01_test.py deleted file mode 100644 index 2ac17c1..0000000 --- a/tests/aoc2023/2023_day_01_test.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from src.aoc.aoc2023 import day_01 as d - -TEST_INPUT = """ -1abc2 -pqr3stu8vwx -a1b2c3d4e5f -treb7uchet -""".strip() - - -def test_a() -> None: - assert d.part_a(TEST_INPUT) == 142 - - -def test_b() -> None: - assert d.part_b(TEST_INPUT) == 281 diff --git a/tests/aoc2024/2024_day_01_test.py b/tests/aoc2024/2024_day_01_test.py index 31a34cb..5b2b294 100644 --- a/tests/aoc2024/2024_day_01_test.py +++ b/tests/aoc2024/2024_day_01_test.py @@ -1,5 +1,3 @@ -import pytest - from src.aoc.aoc2024 import day_01 as d TEST_INPUT = """ @@ -17,4 +15,4 @@ def test_a() -> None: def test_b() -> None: - assert d.part_b(TEST_INPUT) == 21 + assert d.part_b(TEST_INPUT) == 31