From 9f445b666da8bb769323a99caa2bd058c796f15f Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 19 May 2026 17:09:13 -0400 Subject: [PATCH] Add python script --- day2/day2_sample.py | 93 ++++++++++++++++++++++++++++++++++ day2/test_cases/all_safe.txt | 5 ++ day2/test_cases/all_unsafe.txt | 6 +++ day2/test_cases/edge_cases.txt | 5 ++ day2/test_cases/sample.txt | 7 +++ day2/test_day2.py | 32 ++++++++++++ 6 files changed, 148 insertions(+) create mode 100644 day2/day2_sample.py create mode 100644 day2/test_cases/all_safe.txt create mode 100644 day2/test_cases/all_unsafe.txt create mode 100644 day2/test_cases/edge_cases.txt create mode 100644 day2/test_cases/sample.txt create mode 100644 day2/test_day2.py diff --git a/day2/day2_sample.py b/day2/day2_sample.py new file mode 100644 index 0000000..907d61a --- /dev/null +++ b/day2/day2_sample.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +Counts how many reports are "safe". + +A report (one line of space-separated integers) is safe if: +1) Levels are strictly monotonic (all increasing OR all decreasing). +2) Adjacent levels differ by at least 1 and at most 3 (inclusive). + +Usage: + python day2/aoc_2024_day2.py input.txt + cat input.txt | python day2/aoc_2024_day2.py +""" + +import argparse + + +class Report: + def __init__(self, levels): + self.levels = levels + + @classmethod + def from_line(cls, line): + parts = line.split() + levels = [] + for part in parts: + levels.append(int(part)) + return cls(levels=levels) + + def is_safe(self): + if len(self.levels) < 2: + return True + + # Determine direction (increasing/decreasing) from the first valid step. + # 0 => not decided yet + # +1 => increasing + # -1 => decreasing + direction = 0 + + for i in range(1, len(self.levels)): + previous_level = self.levels[i - 1] + current_level = self.levels[i] + delta = current_level - previous_level + + # Adjacent levels must change (no equals). + if delta == 0: + return False + + # Differ by at least one and at most three. + if abs(delta) > 3: + return False + + step_direction = 1 if delta > 0 else -1 + if direction == 0: + direction = step_direction + # The levels are either all increasing or all decreasing. + elif step_direction != direction: + return False + + return True + + +def parse_reports(lines): + reports = [] + for raw in lines: + line = raw.strip() + if not line: + continue + reports.append(Report.from_line(line)) + return reports + + +def count_safe(reports): + safe_count = 0 + for report in reports: + if report.is_safe(): + safe_count += 1 + return safe_count + + +def main(): + parser = argparse.ArgumentParser("CLI to read sample files.") + parser.add_argument("input", help="Input file path (defaults to stdin). Use '-' for stdin.") + + args = parser.parse_args() + reports = parse_reports(args.input) + + print(count_safe(reports)) + return 0 + + +if __name__ == "__main__": + main() + diff --git a/day2/test_cases/all_safe.txt b/day2/test_cases/all_safe.txt new file mode 100644 index 0000000..b51938e --- /dev/null +++ b/day2/test_cases/all_safe.txt @@ -0,0 +1,5 @@ +1 2 3 +3 2 1 +10 9 7 6 +1 4 7 + diff --git a/day2/test_cases/all_unsafe.txt b/day2/test_cases/all_unsafe.txt new file mode 100644 index 0000000..0564084 --- /dev/null +++ b/day2/test_cases/all_unsafe.txt @@ -0,0 +1,6 @@ +1 1 2 +1 5 6 +1 3 2 +5 2 3 +9 6 2 + diff --git a/day2/test_cases/edge_cases.txt b/day2/test_cases/edge_cases.txt new file mode 100644 index 0000000..4ed5a81 --- /dev/null +++ b/day2/test_cases/edge_cases.txt @@ -0,0 +1,5 @@ +5 +1 4 +4 1 +1 5 + diff --git a/day2/test_cases/sample.txt b/day2/test_cases/sample.txt new file mode 100644 index 0000000..55678f8 --- /dev/null +++ b/day2/test_cases/sample.txt @@ -0,0 +1,7 @@ +7 6 4 2 1 +1 2 7 8 9 +9 7 6 2 1 +1 3 2 4 5 +8 6 4 4 1 +1 3 6 7 9 + diff --git a/day2/test_day2.py b/day2/test_day2.py new file mode 100644 index 0000000..06d6886 --- /dev/null +++ b/day2/test_day2.py @@ -0,0 +1,32 @@ +import os +import unittest + +from day2 import aoc_2024_day2 + + +class TestDay2(unittest.TestCase): + def _count_safe_from_file(self, path): + with open(path, "r", encoding="utf-8") as f: + reports = aoc_2024_day2.parse_reports(f) + return aoc_2024_day2.count_safe(reports) + + def _case_path(self, filename): + here = os.path.dirname(__file__) + return os.path.join(here, "test_cases", filename) + + def test_sample(self): + self.assertEqual(self._count_safe_from_file(self._case_path("sample.txt")), 2) + + def test_all_safe(self): + self.assertEqual(self._count_safe_from_file(self._case_path("all_safe.txt")), 4) + + def test_all_unsafe(self): + self.assertEqual(self._count_safe_from_file(self._case_path("all_unsafe.txt")), 0) + + def test_edge_cases(self): + self.assertEqual(self._count_safe_from_file(self._case_path("edge_cases.txt")), 3) + + +if __name__ == "__main__": + unittest.main() +