""" This gist contains the solution for Day 5 of Advent of Code 2023 (https://adventofcode.com/2023/day/5). I first wrote it on version 3.11 and then ported it to earlier versions to see how the language evolved. Below is the listing for version 3.11 """ from __future__ import annotations from itertools import chain, islice from pathlib import Path INPUT_TXT = Path(__file__).parent / "input.txt" class Range: __slots__ = ("start", "end") def __init__(self, start: int, end: int) -> None: self.start = start self.end = end if self.start >= self.end: raise ValueError(f"{self.start=} must be < {self.end=}") def __repr__(self) -> str: return f"{self.__class__.__name__}({self.start}, {self.end})" def __eq__(self, other: object) -> bool: if not isinstance(other, Range): return NotImplemented return self.start == other.start and self.end == other.end def __contains__(self, n: int) -> bool: return self.start <= n < self.end def __len__(self) -> int: return self.end - self.start def has_intersection(self, other: Range) -> bool: return self.start < other.end and other.start < self.end def intersection(self, other: Range) -> Range | None: if not self.has_intersection(other): return None return Range(max(self.start, other.start), min(self.end, other.end)) def remainder(self, other: Range) -> list[Range]: intersection = self.intersection(other) if intersection is None: return [] result = [] if self.start < intersection.start: result.append(Range(self.start, intersection.start)) if intersection.end < self.end: result.append(Range(intersection.end, self.end)) return result def batched(iterable, n): # batched('ABCDEFG', 3) --> ABC DEF G if n < 1: raise ValueError("n must be at least one") it = iter(iterable) while batch := tuple(islice(it, n)): yield batch def compute(s: str) -> int: current_source_ranges = [] ranges = [] for line in chain(s.splitlines(), [""]): if not line.strip(): if not ranges: continue update_current_source_ranges(current_source_ranges, ranges) ranges = [] # start of new section elif ":" in line: name, values = line.split(":") if name == "seeds": seeds_input = [int(x) for x in values.strip().split()] for start, range_len in batched(seeds_input, 2): current_source_ranges.append(Range(start, start + range_len)) # parse map else: destination, source, range_len = (int(i) for i in line.split()) ranges.append( ( Range(source, source + range_len), Range(destination, destination + range_len), ) ) return min(s.start for s in current_source_ranges) def update_current_source_ranges(current_source_ranges, ranges) -> None: for i, curr_source_range in enumerate(current_source_ranges): for source_range, dest_range in ranges: if intersect := curr_source_range.intersection(source_range): current_source_ranges[i] = Range( dest_range.start + (intersect.start - source_range.start), dest_range.end + (intersect.end - source_range.end), ) if remainder := curr_source_range.remainder(source_range): current_source_ranges.extend(remainder) break INPUT_S = """\ seeds: 79 14 55 13 seed-to-soil map: 50 98 2 52 50 48 soil-to-fertilizer map: 0 15 37 37 52 2 39 0 15 fertilizer-to-water map: 49 53 8 0 11 42 42 0 7 57 7 4 water-to-light map: 88 18 7 18 25 70 light-to-temperature map: 45 77 23 81 45 19 68 64 13 temperature-to-humidity map: 0 69 1 1 0 69 humidity-to-location map: 60 56 37 56 93 4 """ EXPECTED = 46 def read_input() -> str: with open(INPUT_TXT) as f: return f.read() def test_day_input() -> None: assert compute(INPUT_S) == EXPECTED input_data = read_input() assert compute(input_data) == 20283860 def test_range(): sample_range = Range(12, 55) # test __contains__ assert 12 in sample_range assert 54 in sample_range assert 55 not in sample_range assert -1 not in sample_range # test __len__ assert len(sample_range) == 43 # test has_intersection assert sample_range.has_intersection(Range(-1, 13)) assert sample_range.has_intersection(Range(12, 55)) assert not sample_range.has_intersection(Range(55, 100)) # test intersection assert sample_range.intersection(Range(20, 30)) == Range(20, 30) assert not sample_range.intersection(Range(100, 200)) # test remainder assert sample_range.remainder(Range(20, 30)) == [ Range(12, 20), Range(30, 55), ] if __name__ == "__main__": test_day_input() test_range()