|
1 | 1 | """Day 5 - Cafeteria""" |
2 | 2 |
|
3 | | -from typing import Any |
| 3 | +from typing import Any, Iterator |
4 | 4 | from utils.aoc_utils import report_results, AoCResult, input_for_day |
5 | 5 |
|
6 | 6 | IngredientIds = list[int] |
7 | 7 | FreshRanges = list[range] |
| 8 | +FreshRange = tuple[int, int] |
8 | 9 |
|
9 | 10 | EXAMPLE: str = """3-5 |
10 | 11 | 10-14 |
|
17 | 18 | 11 |
18 | 19 | 17 |
19 | 20 | 32""" |
20 | | -DATA = input_for_day(5, 2025) |
| 21 | +DATA: str = input_for_day(5, 2025) |
21 | 22 |
|
22 | 23 |
|
23 | 24 | def parse_input(puzzle_input: str) -> tuple[FreshRanges, IngredientIds]: |
24 | | - ranges: FreshRanges = [] |
| 25 | + """Convert input of str. |
| 26 | + Split on the \n\n, remove any \n from each line. |
| 27 | + Then parse fresh ranges into list of ranges, and |
| 28 | + ingredient ids into a list of ints. |
| 29 | +
|
| 30 | + Args: |
| 31 | + puzzle_input (str): Puzzle input. |
| 32 | +
|
| 33 | + Returns: |
| 34 | + tuple[FreshRanges, IngredientIds]: ranges of fresh ids, fresh ids. |
| 35 | + """ |
25 | 36 | ranges_str, ingredient_ids_str = [ |
26 | 37 | x.splitlines() for x in puzzle_input.split('\n\n') |
27 | 38 | ] |
28 | | - ranges = [ |
29 | | - range(start, end + 1) for start, end in (map(int, r.split('-')) for r in ranges_str) |
| 39 | + ranges: FreshRanges = [ |
| 40 | + range(start, end + 1) for start, |
| 41 | + end in (map(int, r.split('-')) for r in ranges_str) |
30 | 42 | ] |
31 | 43 |
|
32 | | - # ranges_ints = [list(map(int, r.split('-'))) for r in ranges_str] |
33 | | - # for rng in ranges_ints: |
34 | | - # ranges.extend(list(range(rng[0], rng[1]+1))) |
35 | | - |
36 | 44 | ingredient_ids: IngredientIds = list(map(int, ingredient_ids_str)) |
37 | 45 | return ranges, ingredient_ids |
38 | 46 |
|
39 | 47 |
|
40 | | -def solve_part1(data: Any) -> int: |
| 48 | +def merge_ranges(ranges: FreshRanges) -> Iterator[FreshRange]: |
| 49 | + """Merge current ranges if overlap in any way. |
| 50 | +
|
| 51 | + Args: |
| 52 | + ranges (FreshRanges): ranges of fresh ids |
| 53 | +
|
| 54 | + Yields: |
| 55 | + Iterator[FreshRange]: merged ranges |
| 56 | + """ |
| 57 | + # change ranges back to tuples of the start/stop |
| 58 | + sorted_rngs: Iterator[FreshRange] = iter( |
| 59 | + sorted((x.start, x.stop-1) for x in ranges) |
| 60 | + ) |
| 61 | + start, end = next(sorted_rngs) # get first fresh range |
| 62 | + |
| 63 | + for s, e in sorted_rngs: |
| 64 | + # if the next interval starts before/at current end |
| 65 | + # they overlap so merge range by getting biggest end |
| 66 | + if s <= end: |
| 67 | + end = max(end, e) |
| 68 | + else: |
| 69 | + # if they dont overlap - start a new fresh range |
| 70 | + # and yield the current one |
| 71 | + yield (start, end) |
| 72 | + start, end = s, e |
| 73 | + yield start, end |
| 74 | + |
| 75 | + |
| 76 | +def solve_part2(ranges: FreshRanges) -> int: |
| 77 | + """Sum up the merged range lengths |
| 78 | +
|
| 79 | + Args: |
| 80 | + ranges (FreshRanges): fresh id ranges. |
| 81 | +
|
| 82 | + Returns: |
| 83 | + int: sum of lengths of ranges. |
| 84 | + """ |
| 85 | + # sum the lengths of each range |
| 86 | + return sum(len(range(x[0], x[1]+1)) for x in merge_ranges(ranges)) |
| 87 | + |
| 88 | + |
| 89 | +def solve_day05(data: Any) -> tuple[int, int]: |
| 90 | + """solve both days - check if an ingredient id is |
| 91 | + in any of the ranges. If so - break and count. |
| 92 | +
|
| 93 | + Then apply solve_part2. |
| 94 | +
|
| 95 | + Args: |
| 96 | + data (Any): Puzzle data. |
| 97 | +
|
| 98 | + Returns: |
| 99 | + tuple[int, int]: part 1 value, part 2 value. |
| 100 | + """ |
41 | 101 | ranges, ingredient_ids = parse_input(data) |
42 | | - valid_id = 0 |
43 | | - for x in ingredient_ids: |
44 | | - for y in ranges: |
45 | | - if x in y: |
46 | | - valid_id += 1 |
47 | | - break |
48 | | - return valid_id |
| 102 | + valid_id: int = 0 |
| 103 | + for ingredient in ingredient_ids: |
| 104 | + # THE OLD WAY I DID IT. |
| 105 | + # for y in ranges: |
| 106 | + # if x in y: |
| 107 | + # valid_id += 1 |
| 108 | + # break |
| 109 | + if any(ingredient in r for r in ranges): |
| 110 | + valid_id += 1 |
| 111 | + return valid_id, solve_part2(ranges) |
49 | 112 |
|
50 | 113 |
|
51 | 114 | @report_results |
52 | 115 | def solveday(data: Any) -> AoCResult: |
53 | | - p1: int = solve_part1(data) |
54 | | - p2: int = 0 |
| 116 | + p1, p2 = solve_day05(data) |
55 | 117 | return p1, p2 |
56 | 118 |
|
57 | 119 |
|
58 | | -expected_test_results: AoCResult = (3, 0) |
| 120 | +expected_test_results: AoCResult = (3, 14) |
59 | 121 |
|
60 | 122 |
|
61 | 123 | def tests(test_input: Any) -> None: |
|
0 commit comments