From 5fa829a544f78a0553e06f92e3095641c019363e Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 19 May 2026 17:06:24 -0400 Subject: [PATCH 1/4] Add ignore file --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file -- 2.34.1 From 9f445b666da8bb769323a99caa2bd058c796f15f Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 19 May 2026 17:09:13 -0400 Subject: [PATCH 2/4] 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() + -- 2.34.1 From d782d5e662a6f54959401338b19813f084391a8d Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 19 May 2026 17:29:36 -0400 Subject: [PATCH 3/4] Simplify interface for testing --- day2/day2_sample.py | 40 +++++++++++++++++++++++--------------- day2/test_cases/sample.txt | 13 ++++++------- day2/test_day2.py | 16 +++++---------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/day2/day2_sample.py b/day2/day2_sample.py index 907d61a..9e290dc 100644 --- a/day2/day2_sample.py +++ b/day2/day2_sample.py @@ -8,7 +8,6 @@ A report (one line of space-separated integers) is safe if: Usage: python day2/aoc_2024_day2.py input.txt - cat input.txt | python day2/aoc_2024_day2.py """ import argparse @@ -59,13 +58,14 @@ class Report: return True -def parse_reports(lines): +def parse_reports(path): reports = [] - for raw in lines: - line = raw.strip() - if not line: - continue - reports.append(Report.from_line(line)) + with open(path, "r", encoding="utf-8") as f: + for raw in f: + line = raw.strip() + if not line: + continue + reports.append(Report.from_line(line)) return reports @@ -77,17 +77,25 @@ def count_safe(reports): 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) +def count_safe_in_file(path): + return count_safe(parse_reports(path)) - print(count_safe(reports)) + +def main(argv=None): + parser = argparse.ArgumentParser("CLI to read sample files.") + parser.add_argument( + "input", + help="Input file path.", + ) + + args = parser.parse_args(argv) + return count_safe_in_file(args.input) + + +def cli(argv=None): + print(main(argv)) return 0 if __name__ == "__main__": - main() - + raise SystemExit(cli()) diff --git a/day2/test_cases/sample.txt b/day2/test_cases/sample.txt index 55678f8..2d6faf5 100644 --- a/day2/test_cases/sample.txt +++ b/day2/test_cases/sample.txt @@ -1,7 +1,6 @@ -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 - +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 \ No newline at end of file diff --git a/day2/test_day2.py b/day2/test_day2.py index 06d6886..48a59f1 100644 --- a/day2/test_day2.py +++ b/day2/test_day2.py @@ -1,32 +1,26 @@ import os import unittest -from day2 import aoc_2024_day2 +from day2_sample import count_safe_in_file 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) + self.assertEqual(count_safe_in_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) + self.assertEqual(count_safe_in_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) + self.assertEqual(count_safe_in_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) + self.assertEqual(count_safe_in_file(self._case_path("edge_cases.txt")), 3) if __name__ == "__main__": unittest.main() - -- 2.34.1 From a10f173ff3865332717f021cc44d7b6742700419 Mon Sep 17 00:00:00 2001 From: josh Date: Tue, 19 May 2026 17:30:47 -0400 Subject: [PATCH 4/4] Remove cli function --- day2/day2_sample.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/day2/day2_sample.py b/day2/day2_sample.py index 9e290dc..cbb1fda 100644 --- a/day2/day2_sample.py +++ b/day2/day2_sample.py @@ -92,10 +92,5 @@ def main(argv=None): return count_safe_in_file(args.input) -def cli(argv=None): - print(main(argv)) - return 0 - - if __name__ == "__main__": - raise SystemExit(cli()) + print(main()) \ No newline at end of file -- 2.34.1