Skip to content

Instantly share code, notes, and snippets.

@yakimka
Last active February 11, 2025 20:09
Show Gist options
  • Save yakimka/2b0bb1581553bbf625aeef2fc8c5fa6d to your computer and use it in GitHub Desktop.
Save yakimka/2b0bb1581553bbf625aeef2fc8c5fa6d to your computer and use it in GitHub Desktop.
Python versions evolution
"""
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 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 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