Day 18 (Python)

Day 18, part one (Python)

Day 18, part two (Python)

Day 18 READMEs
This commit is contained in:
akp 2020-12-19 00:34:26 +00:00
parent fa8bda45d8
commit 8d2eeee6fa
No known key found for this signature in database
GPG key ID: D3E7EAA31B39637E
6 changed files with 297 additions and 1 deletions

2
.github/README.md vendored
View file

@ -33,7 +33,7 @@ Puzzle inputs and descriptions are not included in this repository. You'll have
| [15](/15-rambunctiousRecitation) \* | ![Completed][check] | [Link](/15-rambunctiousRecitation/python) | [Link](/15-rambunctiousRecitation/go) |
| [16](/16-ticketTranslation) | ![Partially complete][partial] | [Link](/16-ticketTranslation/python) | |
| [17](/17-conwayCubes) | ![Partially complete][partial] | [Link](/17-conwayCubes/python) | |
| 18 | ![Not yet attempted][pending] | | |
| [18](/18-operationOrder) | ![Partially complete][partial] | [Link](/18-operationOrder/python) | |
| 19 | | | |
| 20 | | | |
| 21 | | | |

View file

@ -0,0 +1,59 @@
# [Day 18: Operation Order](https://adventofcode.com/2020/day/18)
### Part one
Each expression is represented by a list. Each set of brackets in an input is a nested list within that list. For example:
```py
>>> parse_expression("1 + 1 + (3 * 4) + 8".replace(" ", ""))[0]
['1', '+', '1', '+', ['3', '*', '4'], '+', '8']
```
In order to calculate this, a recursive function is used to step through each item and perform any operations found in a linear manner (ie, ignoring [operator precedence](https://en.wikipedia.org/wiki/Order_of_operations)).
### Part two
Attempts to use the same data structure for part two, while possible, were difficult and I never actually ended up getting any of them working. I tried two different methods.
* Encasing every single addition operation in a set of brackets (this was done after parsing, so it was actually replacing three items from a list with a list of those three items)
* Selectivley calculating only the additions in an expression before then going on to calculate the multiplication
* This isn't actually possible - how are you meant to calculate `1 + (2 + 3 * 4)` without multiplication?
After I started wanting to punch myself repeatedly in the head because of the amount of issues I was having with my own solution, I gave up and found an implentation of the shunting-yard algorithm ([this one](http://www.martinbroadhurst.com/shunting-yard-algorithm-in-python.html), to be precise), modified the operator precendences and used that.
It worked first try. *sigh*
At some point, I'd like to come back to this and write my own implementation, and convert the first part to using that new implementation.
### Related
* [Wikipedia - Shunting-yard algorithm](https://en.wikipedia.org/wiki/Shunting-yard_algorithm)
* [Computerphile - Reverse Polish Notation and The Stack](https://www.youtube.com/watch?v=7ha78yWRDlE)
<details><summary>Script output</summary>
```
python .\python\
AoC 2020: day 18 - Operation Order
Python 3.8.5
Test cases
1.1 pass
1.2 pass
1.3 pass
1.4 pass
1.5 pass
1.6 pass
2.1 pass
2.2 pass
2.3 pass
2.4 pass
2.5 pass
2.6 pass
Answers
Part 1: 18213007238947
Part 2: 388966573054664
```
</details>

View file

@ -0,0 +1,23 @@
{
"year": "2020",
"day": "18",
"title": "Operation Order",
"testCases": {
"one": [
{"input":"1 + 2 * 3 + 4 * 5 + 6", "expected":71},
{"input":"1 + (2 * 3) + (4 * (5 + 6))", "expected":51},
{"input":"2 * 3 + (4 * 5)", "expected":26},
{"input":"5 + (8 * 3 + 9 + 3 * 4 * 3)", "expected":437},
{"input":"5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))", "expected":12240},
{"input":"((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2", "expected":13632}
],
"two": [
{"input":"1 + 2 * 3 + 4 * 5 + 6", "expected":231},
{"input":"1 + (2 * 3) + (4 * (5 + 6))", "expected": 51},
{"input":"2 * 3 + (4 * 5)", "expected": 46},
{"input":"5 + (8 * 3 + 9 + 3 * 4 * 3)", "expected": 1445},
{"input":"5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))", "expected": 669060},
{"input":"((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2", "expected": 23340}
]
}
}

View file

@ -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))

View file

@ -0,0 +1,67 @@
from typing import List, Tuple, Union
def parse_expression(expression: str) -> Tuple[List, int]:
o = []
if len(expression) == 0:
return o, 0
ptr = 0
while ptr < len(expression):
char = expression[ptr]
if char == "(":
nl, idx = parse_expression(expression[ptr + 1 :])
o.append(nl)
ptr += idx
elif char == ")":
return o, ptr + 1
else:
o.append(char)
ptr += 1
return o, 0
def calc_to_int(x: Union[list, str]) -> int:
if type(x) == list:
return calculate(x)
else:
return int(x)
def calculate(expression: List) -> int:
n = calc_to_int(expression[0])
ptr = 1
while ptr < len(expression):
op = expression[ptr]
x = calc_to_int(expression[ptr + 1])
if op == "+":
n += x
elif op == "-":
n -= x
elif op == "*":
n *= x
elif op == "/":
n /= x
else:
raise ValueError(f"unknown operator '{op}'")
ptr += 2
return n
def partOne(instr: str) -> int:
expressions = [
parse_expression(list(x.replace(" ", "")))[0] for x in instr.strip().split("\n")
]
sigma = 0
for expression in expressions:
sigma += calculate(expression)
return sigma

View file

@ -0,0 +1,70 @@
import re
def is_number(str):
try:
int(str)
return True
except ValueError:
return False
def is_name(str):
return re.match("\w+", str)
def peek(stack):
return stack[-1] if stack else None
def apply_operator(operators, values):
operator = operators.pop()
right = values.pop()
left = values.pop()
values.append(eval("{0}{1}{2}".format(left, operator, right)))
def greater_precedence(op1, op2):
precedences = {"+": 2, "-": 0, "*": 1, "/": 1}
return precedences[op1] > precedences[op2]
def evaluate(expression):
tokens = re.findall("[+/*()-]|\d+", expression)
values = []
operators = []
for token in tokens:
if is_number(token):
values.append(int(token))
elif token == "(":
operators.append(token)
elif token == ")":
top = peek(operators)
while top is not None and top != "(":
apply_operator(operators, values)
top = peek(operators)
operators.pop() # Discard the '('
else:
# Operator
top = peek(operators)
while (
top is not None and top not in "()" and greater_precedence(top, token)
):
apply_operator(operators, values)
top = peek(operators)
operators.append(token)
while peek(operators) is not None:
apply_operator(operators, values)
return values[0]
def partTwo(instr: str) -> int:
expressions = instr.strip().split("\n")
sigma = 0
for expression in expressions:
sigma += evaluate(expression)
return sigma