-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathday-4.py
185 lines (147 loc) · 5.43 KB
/
day-4.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/env python
"""Advent of Code Programming Puzzles
2019 Edition - Day Four
Puzzle Solution in Python
"""
import argparse
import logging
import sys
from collections import Counter
log = logging.getLogger(__name__)
# Common Methods ---------------------------------------------------------------
def decode(argument: str) -> tuple[list[int], ...]:
"""Decode argument string from command line
:param argument: argument string
:return: pair of list of digits
"""
char_lists = map(list, argument.split('-'))
range_ = tuple(list(map(int, clist)) for clist in char_lists)
return range_
# Solving Methods --------------------------------------------------------------
def count_pwd(range_: tuple[list[int], ...],
digits: list[int], length: int) -> int:
"""Recursively count passwords with a list of prefix digits
:param range_: min and max number
:param digits: list of prefix digits
:param length: expected password length
:return: number of passwords matching requirements
"""
digit_index = len(digits)
decreasing_digit = digit_index >= 2 and digits[-1] < digits[-2]
if decreasing_digit:
return 0
stop = digit_index == length
if stop:
same_adjacent_digits = len(set(digits)) < len(digits)
return 1 if same_adjacent_digits else 0
min_digits = range_[0][:1+digit_index]
max_digits = range_[1][:1+digit_index]
pwd_count = 0
for next_digit in range(10):
next_digits = digits.copy()
next_digits.append(next_digit)
if not min_digits <= next_digits <= max_digits:
continue
pwd_count += count_pwd(
range_=range_, digits=next_digits, length=length)
return pwd_count
def solve(contents: tuple[list[int], ...]) -> int:
"""Solve the first part of the puzzle
:param contents: list of offsets per wire
:return: answer of the puzzle
"""
length: int = len(contents[0])
digits = list()
pwd_count = count_pwd(range_=contents, digits=digits, length=length)
return pwd_count
def count_pwd_part_two(range_: tuple[list[int], ...],
digits: list[int], length: int) -> int:
"""Recursively count passwords with a list of prefix digits
:param range_: min and max number
:param digits: list of prefix digits
:param length: expected password length
:return: number of passwords matching requirements
"""
digit_index = len(digits)
decreasing_digit = digit_index >= 2 and digits[-1] < digits[-2]
if decreasing_digit:
return 0
stop = digit_index == length
if stop:
same_adjacent_digits = len(set(digits)) < len(digits)
if not same_adjacent_digits:
return 0
for digit, encounters in Counter(digits).most_common():
if 2 == encounters:
return 1
return 0
min_digits = range_[0][:1+digit_index]
max_digits = range_[1][:1+digit_index]
pwd_count = 0
for next_digit in range(10):
next_digits = digits.copy()
next_digits.append(next_digit)
if not min_digits <= next_digits <= max_digits:
continue
pwd_count += count_pwd_part_two(
range_=range_, digits=next_digits, length=length)
return pwd_count
def solve_part_two(contents: tuple[list[int], ...]) -> int:
"""Solve the second part of the puzzle
:param contents: list of offsets per wire
:return: answer of the puzzle
"""
length: int = len(contents[0])
digits = list()
pwd_count = count_pwd_part_two(
range_=contents, digits=digits, length=length)
return pwd_count
# Support Methods --------------------------------------------------------------
EXIT_SUCCESS = 0
LOG_FORMAT = ('%(asctime)s - %(levelname)s - %(module)s - '
'%(funcName)s - %(message)s')
def configure_logger(verbose: bool):
"""Configure logging
:param verbose: display debug and info messages
:return: nothing
"""
logger = logging.getLogger()
logger.handlers = []
stdout = logging.StreamHandler(sys.stdout)
stdout.setLevel(level=logging.WARNING)
stdout.setFormatter(logging.Formatter(LOG_FORMAT))
logger.addHandler(stdout)
if verbose:
stdout.setLevel(level=logging.DEBUG)
logger.setLevel(level=logging.DEBUG)
def parse_arguments() -> argparse.Namespace:
"""Parse arguments provided by the command-line
:return: list of decoded arguments
"""
parser = argparse.ArgumentParser(description=__doc__)
pa = parser.add_argument
pa('puzzle_input', type=str, help='puzzle input value')
pa('-p', '--part', type=int, help='solve only the given part')
pa('-v', '--verbose', action='store_true', help='print extra messages')
arguments = parser.parse_args()
return arguments
def main() -> int:
"""Script main method
:return: script exit code returned to the shell
"""
args = parse_arguments()
configure_logger(verbose=args.verbose)
log.debug(f'Arguments: {args}')
compute_part_one = not args.part or 1 == args.part
compute_part_two = not args.part or 2 == args.part
if compute_part_one:
contents = decode(argument=args.puzzle_input)
answer = solve(contents=contents)
print(answer)
if compute_part_two:
contents = decode(argument=args.puzzle_input)
answer = solve_part_two(contents=contents)
print(answer)
return EXIT_SUCCESS
if __name__ == "__main__":
sys.exit(main())