Last active
February 11, 2025 20:09
-
-
Save yakimka/2b0bb1581553bbf625aeef2fc8c5fa6d to your computer and use it in GitHub Desktop.
Python versions evolution
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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 1.6.1. | |
The code was ported from version 3.11, with the following modifications: | |
- Replaced the use of f-strings with the older formatting method using %. | |
- Removed the usage of :=, annotations, generators, and list comprehensions. | |
- Since itertools and pathlib are not available, I had to write my own alternatives. | |
- There is no __file__ in the global namespace. | |
- i += 1 is also not available. | |
- __eq__ does not work, dunder methods for comparison only appeared in version 2.1. | |
- __slots__ does not work, they only appeared in version 2.2. | |
""" | |
import os | |
INPUT_TXT = os.path.join(".", "input.txt") | |
class Range: | |
def __init__(self, start, end): | |
self.start = start | |
self.end = end | |
if self.start >= self.end: | |
raise ValueError("%s must be < %s" % (self.start, self.end)) | |
def __repr__(self): | |
return "%s(%s, %s)" % (self.__class__.__name__, self.start, self.end) | |
def __contains__(self, n): | |
return self.start <= n < self.end | |
def __len__(self): | |
return self.end - self.start | |
def to_tuple(self): | |
return (self.start, self.end) | |
def has_intersection(self, other): | |
return self.start < other.end and other.start < self.end | |
def intersection(self, other): | |
if not self.has_intersection(other): | |
return None | |
return Range(max(self.start, other.start), min(self.end, other.end)) | |
def remainder(self, other): | |
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): | |
if n < 1: | |
raise ValueError("n must be at least one") | |
result = [] | |
batch = [] | |
for item in iterable: | |
batch.append(item) | |
if len(batch) == n: | |
result.append(tuple(batch)) | |
batch = [] | |
if batch: | |
result.append(tuple(batch)) | |
return result | |
def compute(s): | |
current_source_ranges = [] | |
ranges = [] | |
for line in 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 = parse_int_from_text(values) | |
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 = parse_int_from_text(line) | |
ranges.append( | |
( | |
Range(source, source + range_len), | |
Range(destination, destination + range_len), | |
) | |
) | |
return min_range_start(current_source_ranges) | |
def parse_int_from_text(values): | |
result = [] | |
for item in values.strip().split(): | |
result.append(int(item)) | |
return result | |
def min_range_start(ranges): | |
minimum = ranges[0].start | |
for r in ranges: | |
if r.start < minimum: | |
minimum = r.start | |
return minimum | |
def update_current_source_ranges(current_source_ranges, ranges): | |
i = -1 | |
for curr_source_range in current_source_ranges: | |
i = i + 1 | |
for source_range, dest_range in ranges: | |
intersect = curr_source_range.intersection(source_range) | |
if intersect: | |
current_source_ranges[i] = Range( | |
dest_range.start + (intersect.start - source_range.start), | |
dest_range.end + (intersect.end - source_range.end), | |
) | |
remainder = curr_source_range.remainder(source_range) | |
if remainder: | |
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(): | |
file = open(INPUT_TXT) | |
data = file.read() | |
file.close() | |
return data | |
def test_day_input(): | |
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)).to_tuple() == (20, 30) | |
assert not sample_range.intersection(Range(100, 200)) | |
# test remainder | |
remainder = sample_range.remainder(Range(20, 30)) | |
assert remainder[0].to_tuple() == (12, 20) | |
assert remainder[1].to_tuple() == (30, 55) | |
if __name__ == "__main__": | |
test_day_input() | |
test_range() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 1.0.1. | |
# The code was ported from version 1.6.1, with the following modifications: | |
# - There are no multiline strings. | |
# - In strings, there are no attributes; all operations with strings are performed through the `string` module. | |
# - In this version `split` only divides by spaces and is not configurable; `splitfields` must be used instead. | |
# - The `int` function does not convert strings to numbers; `string.atoi` must be used. | |
# - Unpacking is only possible for tuples. | |
# - There is no `tuple` function. | |
# - There is no `assert`. | |
# - It seems that there is no `__contains__`. | |
# - String formatting, when using %s, only accepts strings | |
# and cannot automatically convert, for example, an int to a string. | |
# - Errors are just string variables. `Exception` does not exist, | |
# user-defined errors - just strings. | |
import os | |
import string | |
INPUT_TXT = os.path.join(".", "input.txt") | |
class Range: | |
def __init__(self, start, end): | |
self.start = start | |
self.end = end | |
if self.start >= self.end: | |
raise ValueError("%d must be < %d" % (self.start, self.end)) | |
def __repr__(self): | |
return "%s(%d, %d)" % (self.__class__.__name__, self.start, self.end) | |
def __len__(self): | |
return self.end - self.start | |
def to_tuple(self): | |
return (self.start, self.end) | |
def has_intersection(self, other): | |
return self.start < other.end and other.start < self.end | |
def intersection(self, other): | |
if not self.has_intersection(other): | |
return None | |
return Range(max(self.start, other.start), min(self.end, other.end)) | |
def remainder(self, other): | |
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): | |
if n < 1: | |
raise ValueError("n must be at least one") | |
result = [] | |
batch = [] | |
for item in iterable: | |
batch.append(item) | |
if len(batch) == n: | |
result.append(tuple(batch)) | |
batch = [] | |
if batch: | |
result.append(tuple(batch)) | |
return result | |
def tuple(list): | |
if len(list) == 0: return () | |
if len(list) == 1: return (list[0],) | |
i = len(list)/2 | |
return tuple(list[:i]) + tuple(list[i:]) | |
def compute(s): | |
current_source_ranges = [] | |
ranges = [] | |
for line in string.splitfields(s, "\n") + [""]: | |
if not string.strip(line): | |
if not ranges: | |
continue | |
update_current_source_ranges(current_source_ranges, ranges) | |
ranges = [] | |
# start of new section | |
elif ":" in line: | |
by_semicolon = string.splitfields(line, ":") | |
name, values = by_semicolon[0], by_semicolon[1] | |
if name == "seeds": | |
seeds_input = parse_int_from_text(values) | |
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 = parse_int_from_text(line) | |
ranges.append( | |
( | |
Range(source, source + range_len), | |
Range(destination, destination + range_len), | |
) | |
) | |
return min_range_start(current_source_ranges) | |
def parse_int_from_text(values): | |
result = [] | |
stripped = string.strip(values) | |
for item in string.split(stripped): | |
result.append(string.atoi(item)) | |
return tuple(result) | |
def min_range_start(ranges): | |
minimum = ranges[0].start | |
for r in ranges: | |
if r.start < minimum: | |
minimum = r.start | |
return minimum | |
def update_current_source_ranges(current_source_ranges, ranges): | |
i = -1 | |
for curr_source_range in current_source_ranges: | |
i = i + 1 | |
for source_range, dest_range in ranges: | |
intersect = curr_source_range.intersection(source_range) | |
if intersect: | |
current_source_ranges[i] = Range( | |
dest_range.start + (intersect.start - source_range.start), | |
dest_range.end + (intersect.end - source_range.end), | |
) | |
remainder = curr_source_range.remainder(source_range) | |
if remainder: | |
for rem in remainder: | |
current_source_ranges.append(rem) | |
break | |
INPUT_S = string.joinfields([ | |
"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", | |
], "\n") | |
EXPECTED = 46 | |
def read_input(): | |
file = open(INPUT_TXT, "r") | |
data = file.read() | |
file.close() | |
return data | |
def test_day_input(): | |
assert_true(compute(INPUT_S) == EXPECTED) | |
input_data = read_input() | |
assert_true(compute(input_data) == 20283860) | |
def test_range(): | |
sample_range = Range(12, 55) | |
# test __len__ | |
assert_true(len(sample_range) == 43) | |
# test has_intersection | |
assert_true(sample_range.has_intersection(Range(-1, 13))) | |
assert_true(sample_range.has_intersection(Range(12, 55))) | |
assert_false(sample_range.has_intersection(Range(55, 100))) | |
# test intersection | |
assert_true(sample_range.intersection(Range(20, 30)).to_tuple() == (20, 30)) | |
assert_false(sample_range.intersection(Range(100, 200))) | |
# test remainder | |
remainder = sample_range.remainder(Range(20, 30)) | |
assert_true(remainder[0].to_tuple() == (12, 20)) | |
assert_true(remainder[1].to_tuple() == (30, 55)) | |
def assert_true(val): | |
if not val: | |
raise "Expected trueish value, get %s" % (str(val),) | |
def assert_false(val): | |
if val: | |
raise "Expected falseish value, get %s" % (str(val),) | |
if __name__ == "__main__": | |
test_day_input() | |
test_range() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment