First part
Another interesting problem.
It naturally occurred to use a split('-')
to separate all the contents of a line, and to use a [:-1]
on its result to omit the last element (which would be the ID).
not-a-real-room-404[oarel]
is now ['not', 'a', 'real'. 'room']
.
Since this is a counting exercise, the collections.Counter
class is a good candidate. My preliminary code, however, fell a little short:
def custom_ordering(item):
# Extract the criteria values from the item
criterion1 = item[0] # First criterion
criterion2 = item[1] # Second criterion
# Return a tuple containing the criteria values
return (criterion1, criterion2)
for line in EXAMPLES:
most_common = Counter([char for word in line.split('-')[:-1] for char in sorted(word)]).most_common(5)
print(sorted(most_common, key=custom_ordering))
[('a', 5), ('b', 3), ('x', 1), ('y', 1), ('z', 1)]
[('a', 1), ('b', 1), ('c', 1), ('d', 1), ('e', 1)]
[('a', 2), ('n', 1), ('o', 3), ('r', 2), ('t', 1)]
[('a', 2), ('l', 3), ('o', 3), ('r', 2), ('t', 2)]
It will not handle the two sorts at the same time. But there is a small hack one can do: "".join(x.split('-')[:-1])
will get all the contents of a line together (not-a-real-room-404[oarel]
is now 'notarealroom'
), and can later be wrapped under a sorted()
call (thus producing ['a', 'a', 'e', 'l', 'm', 'n', 'o', 'o', 'o', 'r', 'r', 't']
). Since the Counter
class will index by order of appearance, in the case of same number of occurrences, the earliest in the alphabet will necessarily appear first.
This one liner (Counter([char for word in sorted("".join(line.split('-')[:-1])) for char in word]).most_common(5)
) will produce the checksum, which must then be compared.
[('a', 5), ('b', 3), ('x', 1), ('y', 1), ('z', 1)]
[('a', 1), ('b', 1), ('c', 1), ('d', 1), ('e', 1)]
[('o', 3), ('a', 2), ('r', 2), ('e', 1), ('l', 1)]
[('l', 3), ('o', 3), ('a', 2), ('r', 2), ('t', 2)]
I took a break, and came back with some further improvements. With a function to parse the input in the necessary parts, I get
def parse_input_line(line: str):
content = line.split('-')
sorted_lowercase = sorted("".join(content[:-1]))
sector_id = int(content[-1][:-7])
checksum = content[-1][-6:-1]
return sorted_lowercase, sector_id, checksum
def confirm_checksum_with_most_common(
sorted_lowercase: str,
checksum
) -> bool:
return ("".join(list(dict(Counter(sorted_lowercase)
.most_common(5)).keys())) == checksum)
def iterate_over_input_and_get_count(
count: int,
lines: list[str]
) -> int:
if lines:
sorted_lowercase, sector_id, checksum = parse_input_line(lines[0])
if confirm_checksum_with_most_common(sorted_lowercase, checksum):
return iterate_over_input_and_get_count(count + sector_id, lines[1:])
else:
return iterate_over_input_and_get_count(count, lines[1:])
else:
return count
I'm not very happy with the consecutive transformations from Counter
to dict
to list
, but...it's solved.
Second part
I was struggling to find time for this, but it's also done.
def produce_unencrypted_string(
original: str,
sector_id: int
) -> str:
return "".join([lwr[(lwr.index(char) + sector_id) % 26] \
if char != ' ' else ' '
for char in original \
])
def iterate_over_input_and_print_unencrypted(
lines: list[str]
) -> int:
if lines:
original, sorted_lowercase, sector_id, checksum = parse_input_line(lines[0])
if confirm_checksum_with_most_common(sorted_lowercase, checksum) and \
unencrypted = produce_unencrypted_string(original, sector_id)
if 'north' in unencrypted:
print(unencrypted, sector_id)
return iterate_over_input_and_print_unencrypted(lines[1:])
else:
return