Day 19 (Python)
Day 19.1 test implementation Day 19.1 (Python) Day 19.2 (Python) Day 19 comments and formatting (Python) Day 19 READMEs
This commit is contained in:
parent
d3c15289ac
commit
b5c226594b
8 changed files with 346 additions and 1 deletions
2
.github/README.md
vendored
2
.github/README.md
vendored
|
@ -34,7 +34,7 @@ Puzzle inputs and descriptions are not included in this repository. You'll have
|
|||
| [16](/16-ticketTranslation) | ![Partially complete][partial] | [Link](/16-ticketTranslation/python) | |
|
||||
| [17](/17-conwayCubes) | ![Partially complete][partial] | [Link](/17-conwayCubes/python) | |
|
||||
| [18](/18-operationOrder) | ![Partially complete][partial] | [Link](/18-operationOrder/python) | |
|
||||
| 19 | ![Not yet attempted][pending] | | |
|
||||
| [19](/19-monsterMessages) | ![Partially complete][partial] | [Link](/19-monsterMessages/python) | |
|
||||
| 20 | | | |
|
||||
| 21 | | | |
|
||||
| 22 | | | |
|
||||
|
|
35
19-monsterMessages/README.md
Normal file
35
19-monsterMessages/README.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# [Day 19: Monster Messages](https://adventofcode.com/2020/day/19)
|
||||
|
||||
Regex, but this time it's not actually too much of a problem.
|
||||
|
||||
### Part one
|
||||
|
||||
The set of rules that our challenge input provides can pretty easily be parsed and converted into regular expressions, which we can use along with our language's built in regex package in order to parse and validate each message in our input.
|
||||
|
||||
### Part two
|
||||
|
||||
Part two is also done with regular expressions, however they are modified after they are generated. Since there are only two rules that have recursive properties, conditions for each are hardcoded into the script.
|
||||
|
||||
If the recursive rule is rule number `8`, a plus sign is added after the rule for `42`, to denote that sequence can be present one or more times.
|
||||
|
||||
If the recursive rule is rule number `11`, a marker is added in place of the full regular expression. When the rest of the rule has been entirely generated, rule 11 is generated alone. This is then put in the marker in the original regular expression. Since this will add another marker, this replacement is repeated 10 times.
|
||||
|
||||
The regular expression can then be compared against the messages as in part one.
|
||||
|
||||
<details><summary>Script output</summary>
|
||||
|
||||
```
|
||||
❯ python .\python\
|
||||
AoC 2020: day 19 - Monster Messages
|
||||
Python 3.8.5
|
||||
|
||||
Test cases
|
||||
1.1 pass
|
||||
2.1 pass
|
||||
|
||||
Answers
|
||||
Part 1: 222
|
||||
Part 2: 339
|
||||
```
|
||||
|
||||
</details>
|
19
19-monsterMessages/info.json
Normal file
19
19-monsterMessages/info.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"year": "2020",
|
||||
"day": "19",
|
||||
"title": "Monster Messages",
|
||||
"testCases": {
|
||||
"one": [
|
||||
{
|
||||
"input": "0: 4 1 5\n1: 2 3 | 3 2\n2: 4 4 | 5 5\n3: 4 5 | 5 4\n4: \"a\"\n5: \"b\"\n\nababbb\nbababa\nabbbab\naaabbb\naaaabbb\n",
|
||||
"expected": 2
|
||||
}
|
||||
],
|
||||
"two": [
|
||||
{
|
||||
"input": "42: 9 14 | 10 1\n9: 14 27 | 1 26\n10: 23 14 | 28 1\n1: \"a\"\n11: 42 31\n5: 1 14 | 15 1\n19: 14 1 | 14 14\n12: 24 14 | 19 1\n16: 15 1 | 14 14\n31: 14 17 | 1 13\n6: 14 14 | 1 14\n2: 1 24 | 14 4\n0: 8 11\n13: 14 3 | 1 12\n15: 1 | 14\n17: 14 2 | 1 7\n23: 25 1 | 22 14\n28: 16 1\n4: 1 1\n20: 14 14 | 1 15\n3: 5 14 | 16 1\n27: 1 6 | 14 18\n14: \"b\"\n21: 14 1 | 1 14\n25: 1 1 | 1 14\n22: 14 14\n8: 42\n26: 14 22 | 1 20\n18: 15 15\n7: 14 5 | 1 21\n24: 14 1\n\nabbbbbabbbaaaababbaabbbbabababbbabbbbbbabaaaa\nbbabbbbaabaabba\nbabbbbaabbbbbabbbbbbaabaaabaaa\naaabbbbbbaaaabaababaabababbabaaabbababababaaa\nbbbbbbbaaaabbbbaaabbabaaa\nbbbababbbbaaaaaaaabbababaaababaabab\nababaaaaaabaaab\nababaaaaabbbaba\nbaabbaaaabbaaaababbaababb\nabbbbabbbbaaaababbbbbbaaaababb\naaaaabbaabaaaaababaa\naaaabbaaaabbaaa\naaaabbaabbaaaaaaabbbabbbaaabbaabaaa\nbabaaabbbaaabaababbaabababaaab\naabbbbbaabbbaaaaaabbbbbababaaaaabbaaabba\n",
|
||||
"expected": 12
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
77
19-monsterMessages/python/__main__.py
Normal file
77
19-monsterMessages/python/__main__.py
Normal 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))
|
99
19-monsterMessages/python/common.py
Normal file
99
19-monsterMessages/python/common.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from typing import Dict, List, Tuple
|
||||
import re
|
||||
|
||||
|
||||
char_regex = re.compile(r"^\"([a-z])\"$") # group 1 contains interesting information
|
||||
or_rule_regex = re.compile(
|
||||
r"^((\d+ ?)+) \| ((\d+ ?)+)$"
|
||||
) # group 1 and 3 contain interesting info
|
||||
single_rule_regex = re.compile(
|
||||
r"^((\d+ ?)+)$"
|
||||
) # group 0 and 1 both contain the same interesting information
|
||||
|
||||
|
||||
replace_marker = "!!!REPLACETHISMARKER!!!"
|
||||
|
||||
|
||||
def generate_compound_rule_regex(
|
||||
ruleset: Dict[int, str], compound: str, current: int
|
||||
) -> str:
|
||||
|
||||
# Used when you have a rule that looks like `125: 116 33 | 131 113`
|
||||
# compound should be a sequence like `116 33`
|
||||
# current should be the current rule number
|
||||
|
||||
o = ""
|
||||
for x in [int(a) for a in compound.split(" ")]:
|
||||
if x == current:
|
||||
if x == 8:
|
||||
o += generate_rule_regex(ruleset, 42)
|
||||
o += "+"
|
||||
else:
|
||||
o += replace_marker
|
||||
else:
|
||||
o += generate_rule_regex(ruleset, x)
|
||||
|
||||
return o
|
||||
|
||||
|
||||
def generate_rule_regex(ruleset: Dict[int, str], rule: int) -> str:
|
||||
|
||||
# Rule is the integer number of the rule to generate a regular expression for
|
||||
# Said integer should correspond to a key-value pair in the ruleset
|
||||
|
||||
rule_content = ruleset[rule]
|
||||
|
||||
r = char_regex.match(rule_content)
|
||||
if r:
|
||||
# is a single character
|
||||
# `116: "a"`
|
||||
return r.group(1)
|
||||
|
||||
# if we get here, that means there are multiple options for the rule
|
||||
# hence we need to open a new set of brackets
|
||||
output = "("
|
||||
|
||||
r = or_rule_regex.match(rule_content)
|
||||
if r:
|
||||
# rule is an OR rule
|
||||
# `83: 26 131 | 47 116`
|
||||
output += generate_compound_rule_regex(ruleset, r.group(1), rule)
|
||||
output += "|"
|
||||
output += generate_compound_rule_regex(ruleset, r.group(3), rule)
|
||||
|
||||
r = single_rule_regex.match(rule_content)
|
||||
if r:
|
||||
# rule is a simple sequence rule
|
||||
# `92: 67 131`
|
||||
output += generate_compound_rule_regex(ruleset, r.group(1), rule)
|
||||
|
||||
# close original set of brackets
|
||||
output += ")"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def make_ruleset_regex(ruleset: Dict[int, str]) -> str:
|
||||
return "^" + generate_rule_regex(ruleset, 0) + "$"
|
||||
|
||||
|
||||
def run(messages: List[str], regex: re.Pattern) -> int:
|
||||
valid_inputs = 0
|
||||
for i, message in enumerate(messages):
|
||||
if regex.match(message):
|
||||
valid_inputs += 1
|
||||
return valid_inputs
|
||||
|
||||
|
||||
def parse(instr: str) -> Tuple[Dict[int, str], List[str]]:
|
||||
|
||||
rules, messages = instr.strip().split("\n\n")
|
||||
|
||||
rule_dict = {}
|
||||
for x in rules.split("\n"):
|
||||
num, definition = x.split(":")
|
||||
rule_dict[int(num.strip())] = definition.strip()
|
||||
|
||||
messages = messages.strip().split("\n")
|
||||
|
||||
return rule_dict, messages
|
8
19-monsterMessages/python/partOne.py
Normal file
8
19-monsterMessages/python/partOne.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from common import *
|
||||
import re
|
||||
|
||||
|
||||
def partOne(instr: str) -> int:
|
||||
rules, messages = parse(instr)
|
||||
rule_regex = re.compile(make_ruleset_regex(rules))
|
||||
return run(messages, rule_regex)
|
33
19-monsterMessages/python/partTwo.py
Normal file
33
19-monsterMessages/python/partTwo.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
from common import *
|
||||
import re
|
||||
|
||||
|
||||
def recurse_replace(instr: str, old: str, new: str, limit: int, n: int = 0) -> str:
|
||||
# I *think* this is tail recursive?
|
||||
if n + 1 == limit:
|
||||
return instr
|
||||
return recurse_replace(instr.replace(old, new), old, new, limit, n=n + 1)
|
||||
|
||||
|
||||
def partTwo(instr: str) -> int:
|
||||
|
||||
# patch input
|
||||
instr = instr.replace(
|
||||
"8: 42", "8: 42 | 42 8"
|
||||
) # the new rule 8 can be expressed as 42 | 42+
|
||||
instr = instr.replace("11: 42 31", "11: 42 31 | 42 11 31")
|
||||
|
||||
rules, messages = parse(instr)
|
||||
|
||||
# Since we have sections of our ruleset, we have markers in the regex returned by `make_ruleset_regex`
|
||||
# that denote where we need to insert a copy of the rule 11 regular expression (which also happens to
|
||||
# have one of those markers in it)
|
||||
|
||||
rr = make_ruleset_regex(rules)
|
||||
eleven_regex = generate_rule_regex(rules, 11)
|
||||
for _ in range(10):
|
||||
rr = rr.replace(replace_marker, eleven_regex)
|
||||
|
||||
# run as usual
|
||||
rule_regex = re.compile(rr)
|
||||
return run(messages, rule_regex)
|
74
19-monsterMessages/test.py
Normal file
74
19-monsterMessages/test.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
import re
|
||||
from typing import Dict
|
||||
import time
|
||||
|
||||
char_regex = re.compile(r"^\"([a-z])\"$") # group 1 contains interesting information
|
||||
or_rule_regex = re.compile(r"^((\d+ ?)+) \| ((\d+ ?)+)$") # group 1 and 3 contain interesting info
|
||||
single_rule_regex = re.compile(r"^((\d+ ?)+)$") # group 0 and 1 both contain the same interesting information
|
||||
|
||||
rules = {
|
||||
0: "4 1 5",
|
||||
1: "2 3 | 3 2",
|
||||
2: "4 4 | 5 5",
|
||||
3: "4 5 | 5 4",
|
||||
4: "\"a\"",
|
||||
5: "\"b\"",
|
||||
}
|
||||
|
||||
def generate_compound_rule(ruleset:Dict[int, str], compound:str) -> str:
|
||||
o = ""
|
||||
for x in [int(a) for a in compound.split(" ")]:
|
||||
o += generate_rule(ruleset, rule=x)
|
||||
return o
|
||||
|
||||
|
||||
memo = {}
|
||||
|
||||
|
||||
def generate_rule(ruleset:Dict[int, str], rule:int=0) -> str:
|
||||
|
||||
if rule in memo:
|
||||
return memo[rule]
|
||||
|
||||
output = ""
|
||||
|
||||
rule_content = ruleset[rule]
|
||||
|
||||
r = char_regex.match(rule_content)
|
||||
if r:
|
||||
# is a single character
|
||||
return r.group(1)
|
||||
|
||||
# if we get here, that means there are multiple options for the rule
|
||||
# hence we need to open a new set of brackets
|
||||
output += "("
|
||||
|
||||
r = or_rule_regex.match(rule_content)
|
||||
if r:
|
||||
# rule is an OR rule
|
||||
output += generate_compound_rule(ruleset, r.group(1))
|
||||
output += "|"
|
||||
output += generate_compound_rule(ruleset, r.group(3))
|
||||
|
||||
r = single_rule_regex.match(rule_content)
|
||||
if r:
|
||||
# rule is a simple sequence rule
|
||||
output += generate_compound_rule(ruleset, r.group(1))
|
||||
|
||||
# close original set of brackets
|
||||
output += ")"
|
||||
|
||||
memo[rule] = output
|
||||
|
||||
return output
|
||||
|
||||
|
||||
rules, _ = open("input.txt").read().strip().split("\n\n")
|
||||
|
||||
rules_dict = {}
|
||||
for x in rules.split("\n"):
|
||||
num, definition = x.split(":")
|
||||
rules_dict[int(num.strip())] = definition.strip()
|
||||
|
||||
res = generate_rule(rules_dict)
|
||||
print("^" + res + "$")
|
Loading…
Add table
Add a link
Reference in a new issue