From 3f24372cc46a1671fbf466cedf04b4a24406d1b8 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 16 Dec 2020 16:58:17 +0000 Subject: [PATCH] Day 16 (Python) --- .github/README.md | 2 +- 16-ticketTranslation/README.md | 21 +++++++ 16-ticketTranslation/info.json | 19 ++++++ 16-ticketTranslation/python/__main__.py | 77 +++++++++++++++++++++++++ 16-ticketTranslation/python/common.py | 51 ++++++++++++++++ 16-ticketTranslation/python/partOne.py | 9 +++ 16-ticketTranslation/python/partTwo.py | 53 +++++++++++++++++ 7 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 16-ticketTranslation/README.md create mode 100644 16-ticketTranslation/info.json create mode 100644 16-ticketTranslation/python/__main__.py create mode 100644 16-ticketTranslation/python/common.py create mode 100644 16-ticketTranslation/python/partOne.py create mode 100644 16-ticketTranslation/python/partTwo.py diff --git a/.github/README.md b/.github/README.md index d9ceca6..e0b4a1a 100644 --- a/.github/README.md +++ b/.github/README.md @@ -31,7 +31,7 @@ Puzzle inputs and descriptions are not included in this repository. You'll have | [13](/13-shuttleSearch) | ![Partially complete][partial] | [Link](/13-shuttleSearch/python) | | | [14](/14-dockingData) | ![Completed][check] | [Link](/14-dockingData/python) | [Link](/14-dockingData/go) | | [15](/15-rambunctiousRecitation) \* | ![Completed][check] | [Link](/15-rambunctiousRecitation/python) | [Link](/15-rambunctiousRecitation/go) | -| 16 | ![Not yet attempted][pending] | | | +| [16](/16-ticketTranslation) | ![Partially complete][partial] | [Link](/16-ticketTranslation/python) | | | 17 | | | | | 18 | | | | | 19 | | | | diff --git a/16-ticketTranslation/README.md b/16-ticketTranslation/README.md new file mode 100644 index 0000000..4c27d9a --- /dev/null +++ b/16-ticketTranslation/README.md @@ -0,0 +1,21 @@ +# [Day 16: Ticket Translation](https://adventofcode.com/2020/day/16) + +It took me a long while to realise each column could have multiple different possibilities for which ticket field it could be. + +
Script output + +``` +❯ python .\python\ +AoC 2020: day 16 - Ticket Translation +Python 3.8.5 + +Test cases +1.1 pass +2.1 pass + +Answers +Part 1: 29019 +Part 2: 517827547723 +``` + +
diff --git a/16-ticketTranslation/info.json b/16-ticketTranslation/info.json new file mode 100644 index 0000000..dfe3a99 --- /dev/null +++ b/16-ticketTranslation/info.json @@ -0,0 +1,19 @@ +{ + "year": "2020", + "day": "16", + "title": "Ticket Translation", + "testCases": { + "one": [ + { + "input": "class: 1-3 or 5-7\nrow: 6-11 or 33-44\nseat: 13-40 or 45-50\n\nyour ticket:\n7,1,14\n\nnearby tickets:\n7,3,47\n40,4,50\n55,2,20\n38,6,12", + "expected": 71 + } + ], + "two": [ + { + "input":"departure class: 0-1 or 4-19\nrow: 0-5 or 8-19\nseat: 0-13 or 16-19\n\nyour ticket:\n11,12,13\n\nnearby tickets:\n3,9,18\n15,1,5\n5,14,9\n", + "expected": 12 + } + ] + } +} \ No newline at end of file diff --git a/16-ticketTranslation/python/__main__.py b/16-ticketTranslation/python/__main__.py new file mode 100644 index 0000000..da7ce4c --- /dev/null +++ b/16-ticketTranslation/python/__main__.py @@ -0,0 +1,77 @@ +import json +import platform +import sys + +from rich import print + +from partOne import partOne +from partTwo import partTwo + + +def run_tests(test_cases): + do_tests = True + if len(test_cases) == 0: + do_tests = False + elif len(test_cases["one"]) == 0 and len(test_cases["two"]) == 0: + do_tests = False + + if not do_tests: + print("Info: no test cases specified. Skipping tests\n") + return + + print("Test cases") + + def rt(tcs, f, n): + for i, tc in enumerate(tcs): + print(f"{n}.{i+1} ", end="") + expectedInt = tc["expected"] + result = f(str(tc["input"])) + if result == expectedInt: + print("[green]pass[/green]") + else: + print(f"[red]fail[/red] (got {result}, expected {expectedInt})") + + rt(test_cases["one"], partOne, 1) + rt(test_cases["two"], partTwo, 2) + + print() + + +if __name__ == "__main__": + try: + info = open("info.json").read() + except FileNotFoundError: + print("Error: could not open info.json") + sys.exit(-1) + + info = json.loads(info) + + year = info["year"] + day = info["day"] + title = info["title"] + + print(f"[yellow]AoC {year}[/yellow]: day {day} - {title}") + print(f"Python {platform.python_version()}\n") + + try: + challenge_input = open("input.txt").read() + except FileNotFoundError: + print("Error: could not open input.txt") + sys.exit(-1) + + if "vis" in sys.argv: + import visualise + + print("[green]Running visualisation....[/green]") + + visualise.visualise(challenge_input) + sys.exit() + + run_tests(info["testCases"]) + + if "debug" in sys.argv: + sys.exit() + + print("Answers") + print("Part 1:", partOne(challenge_input)) + print("Part 2:", partTwo(challenge_input)) diff --git a/16-ticketTranslation/python/common.py b/16-ticketTranslation/python/common.py new file mode 100644 index 0000000..dc489eb --- /dev/null +++ b/16-ticketTranslation/python/common.py @@ -0,0 +1,51 @@ +from typing import List, Tuple + +class Rule: + name: str + ranges: Tuple[int, ...] + + def __init__(self, instr:str) -> None: + # arrival location: 26-482 or 504-959 + field, conditions = instr.strip().split(": ") + self.name = field + ranges = conditions.split("or") + self.ranges = tuple([int(x) for x in ranges[0].strip().split("-")] + [int(x) for x in ranges[1].strip().split("-")]) + +class Ticket: + fields: Tuple[int] + + def __init__(self, instr:str) -> None: + self.fields = tuple([int(x) for x in instr.strip().split(",")]) + +def parse(instr: str) -> Tuple[List[Rule], Ticket, List[Ticket]]: + + raw_rules, raw_my_ticket, raw_other_tickets = instr.strip().split("\n\n") + + rules = [Rule(x) for x in raw_rules.split("\n")] + + my_ticket = Ticket(raw_my_ticket.split("\n")[-1]) + + other_tickets = [Ticket(x) for x in raw_other_tickets.strip("nearby tickets:\n").split("\n")] + + return rules, my_ticket, other_tickets + +def test_value(value:int, condition:Tuple[int, ...]) -> bool: + return condition[0] <= value <= condition[1] or condition[2] <= value <= condition[3] + +def find_invalid(rules:List[Rule], tickets:List[Ticket]) -> Tuple[List[int], List[int]]: + # returns invalid values and indexes of invalid tickets + + invalid_values = [] + invalid_indexes = [] + + for i, ticket in enumerate(tickets): + for field in ticket.fields: + field_is_valid = False + for rule in rules: + if test_value(field, rule.ranges): + field_is_valid = True + if not field_is_valid: + invalid_values.append(field) + invalid_indexes.append(i) + + return invalid_values, invalid_indexes \ No newline at end of file diff --git a/16-ticketTranslation/python/partOne.py b/16-ticketTranslation/python/partOne.py new file mode 100644 index 0000000..2beed70 --- /dev/null +++ b/16-ticketTranslation/python/partOne.py @@ -0,0 +1,9 @@ +from common import * + + +def partOne(instr: str) -> int: + rules, _, tickets = parse(instr) + + invalid_values, _ = find_invalid(rules, tickets) + + return sum(invalid_values) diff --git a/16-ticketTranslation/python/partTwo.py b/16-ticketTranslation/python/partTwo.py new file mode 100644 index 0000000..249d687 --- /dev/null +++ b/16-ticketTranslation/python/partTwo.py @@ -0,0 +1,53 @@ +from common import * +from pprint import pprint + + +def partTwo(instr: str) -> int: + rules, my_ticket, tickets = parse(instr) + + # purge bad indexes + _, invalid_indexes = find_invalid(rules, tickets) + invalid_indexes = list(reversed(sorted(invalid_indexes))) + + for idx in invalid_indexes: + tickets.pop(idx) + + ticket_length = len(tickets[0].fields) + num_tickets = len(tickets) + num_rules = len(rules) + + candidates = {rule.name: set() for rule in rules} + + for col in range(ticket_length): + values = [ticket.fields[col] for ticket in tickets] + + for rule_idx, rule in enumerate(rules): + complete_match = True + for v in values: + if not test_value(v, rule.ranges): + complete_match = False + break + + if complete_match: + candidates[rule.name].add(col) + + parameter_indexes = {} + removed = set() + + product = 1 + + for col in range(ticket_length): + for name in candidates: + candidates_set = candidates[name] - removed + + if len(candidates_set) == 1: + idx = candidates_set.pop() + parameter_indexes[name] = idx + removed.add(idx) + + product = 1 + for param in parameter_indexes: + if "departure" in param: + product *= my_ticket.fields[parameter_indexes[param]] + + return product