<jupyter_start><jupyter_text><jupyter_code>import pandas as pd
import numpy as np
import itertools
import re
from copy import deepcopy
## Day 1
data = pd.read_csv('day1input.txt', header=None)

# Part 1
x = list(itertools.combinations(data[0], 2))
for combo in x:
  if (sum(combo)) == 2020:
    print(combo[0]*combo[1])
    
# Part 2
x = list(itertools.combinations(data[0], 3))
for combo in x:
  if (sum(combo)) == 2020:
    print(combo[0]*combo[1]*combo[2])
## Day 2
data = pd.read_csv('day2input.txt', header=None, names=['raw'])
data = data.reindex(columns=['raw', 'min', 'max', 'letter', 'password', 'count'], fill_value='')
valid = 0

# Part 1
for i, row in data.iterrows():
  split = re.split('[ -]|: ', row.raw)
  min = int(split[0])
  max = int(split[1])
  letter = split[2]
  pwd = split[3]
  count = pwd.count(letter)
  if (count >= min) & (count <= max):
    valid += 1
  data.at[i, 'min'] = min
  data.at[i, 'max']= max
  data.at[i, 'letter'] = letter
  data.at[i, 'password'] = pwd
  data.at[i, 'count'] = count
print(valid)

# Part 2
valid = 0
for i, row in data.iterrows():
  match = 0
  first = row.password[row['min'] - 1]
  last = row.password[row['max'] - 1]
  if first == row.letter:
    match += 1
  if last == row.letter:
    match += 1
  if match == 1:
    valid += 1
print(valid)
# Day 3
data = pd.read_csv('day3input.txt', header=None)

# Part 1
x1 = 0
trees1 = 0
for i, row in data.itertuples():
  if row[(x1%31)] == '#':
    trees1 += 1
  x1 += 3
print(trees1)

# Part 2
trees0, x0, trees2, x2, trees3, x3, trees4, x4 = 0, 0, 0, 0, 0, 0, 0, 0
for i, row in data.itertuples():
  if row[(x0%31)] == '#':
    trees0 += 1
  x0 += 1
  if row[(x2%31)] == '#':
    trees2 += 1
  x2 += 5
  if row[(x3%31)] == '#':
    trees3 += 1
  x3 += 7
  if i%2 == 0:
    if row[(x4%31)] == '#':
      trees4 += 1
    x4 += 1
print(trees0*trees1*trees2*trees3*trees4)
# Day 4
input = pd.read_csv('day4input.txt', header=None, skip_blank_lines=False)
rows_list = []
index = 0
d = {}
for i, row in input.itertuples():
  if pd.isna(row):
    rows_list.append(d)
    index += 1
    d = {}
    continue
  fields = row.split()
  for field in fields:
    key = field.split(':')[0]
    value = field.split(':')[1]
    d[key] = value
data = pd.DataFrame(rows_list, columns=['byr', 'iyr', 'eyr', 'hgt', 'hcl', 'ecl', 'pid', 'cid'])

# Part 1
valid = 0
for i, row in data.iterrows():
  if (pd.isna(row.byr) | pd.isna(row.iyr) | pd.isna(row.eyr) | pd.isna(row.hgt) | pd.isna(row.hcl) | pd.isna(row.ecl) | pd.isna(row.pid)):
    continue
  valid += 1
print(valid)

# Part 2
valid = 0
for i, row in data.iterrows():
  if (pd.isna(row.byr) | pd.isna(row.iyr) | pd.isna(row.eyr) | pd.isna(row.hgt) | pd.isna(row.hcl) | pd.isna(row.ecl) | pd.isna(row.pid)):
    continue
  if not (len(row.byr) == 4 and row.byr.isdigit() and int(row.byr) >= 1920 and int(row.byr) <= 2002):
    continue
  if not (len(row.iyr) == 4 and row.iyr.isdigit() and int(row.iyr) >= 2010 and int(row.iyr) <= 2020):
    continue
  if not (len(row.eyr) == 4 and row.eyr.isdigit() and int(row.eyr) >= 2020 and int(row.eyr) <= 2030):
    continue
  if not (re.search('^[0-9]+(cm|in)$',row.hgt)):
    continue
  num = int(re.findall('[0-9]+', row.hgt)[0])
  if not ((re.search('cm$', row.hgt) and num >= 150 and num <= 193) or (re.search('in$', row.hgt) and num >= 59 and num <= 76)):
    continue
  if not (re.search('#[0-9a-f]{6}', row.hcl)):
    continue
  if not (row.ecl in ['amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth']):
    continue
  if not (re.search('^[0-9]{9}$', row.pid)):
    continue
  valid += 1
print(valid)
# Day 5
data = pd.read_csv('day5input.txt', header=None, names=['seat'])
data = data.reindex(columns=['seat', 'seat_row', 'seat_col', 'id'], fill_value=0)
def get_seat(bin, num):
  x = 0
  for letter in bin:
    if letter == 'B' or letter == 'R':
      x = x + num/2
      num = num/2
    if letter == 'F' or letter == 'L':
      num = num/2
  return x

# Part 1
for i, row in data.iterrows():
  bin_row = row.seat[0:7]
  bin_col = row.seat[7:10]
  data.at[i, 'seat_row'] = get_seat(bin_row, 128)
  data.at[i, 'seat_col'] = get_seat(bin_col, 8)
  data.at[i, 'id'] = data.at[i, 'seat_row'] * 8 + data.at[i, 'seat_col']
ids = sorted(data['id'])
max = ids[-1]
print(max)

# Part 2
missing = sorted(set(range(ids[0], ids[-1] + 1)).difference(ids))[0]
print(missing)
# Day 6
input = pd.read_csv('day6input.txt', header=None, skip_blank_lines=False)
rows_list = []
d = {}
people = 0
for i, row in input.itertuples():
  if pd.isna(row):
    d['people'] = people
    rows_list.append(d)
    d = {}
    people = 0 
    continue
  people += 1
  for letter in row:
    if letter not in d:
      d[letter] = 1
    else:
      d[letter] += 1
  if i == len(input) - 1:
    d['people'] = people
    rows_list.append(d)

# Part 1
num = 0
for row in rows_list:
  num = num + len(row) - 1
print(num)

# Part 2
num = 0
for row in rows_list:
  for answer in row:
    if answer == 'people':
      continue
    if row['people'] == row[answer]:
      num += 1
print(num)
# Day 7
with open('day7input.txt','r') as f:
    input = f.read().split('\n')

bag_dict = {}
for row in input:
  split_row = re.split(' bags contain | bag[s]?[,.]{1}[ ]?', row) # 'no other' = none
  parent_bag = split_row[0]
  child_bags = split_row[1:len(split_row)-1]

  if child_bags == ['no other']:
    continue
  bag_dict[parent_bag] = []
  for child_bag in child_bags:
    split_child_bag = re.split(' ', child_bag, maxsplit=1)
    num = split_child_bag[0]
    bag = split_child_bag[1]
    bag_dict[parent_bag].append((bag, num))

# Part 1
def find_shiny_gold(bag):
  if bag not in bag_dict:
    return 0
  running_count = 0
  for child_bag in bag_dict[bag]:
    if child_bag[0] == 'shiny gold':
      return 1
    running_count += find_shiny_gold(child_bag[0])
  return running_count

found = 0
for bag in bag_dict:
  if find_shiny_gold(bag) > 0:
    found += 1
print(found)

# Part 2
def count_bags_inside(bag):
  if bag not in bag_dict:
    return 0
  running_count = 0
  for child_bag in bag_dict[bag]:
    bags_in_child = int(child_bag[1])
    running_count += bags_in_child + bags_in_child * count_bags_inside(child_bag[0])
  return running_count

print(count_bags_inside('shiny gold'))
# Day 8
data = pd.read_csv('day8input.txt', header=None, names=['raw'])
data = data.reindex(columns=['raw', 'command', 'num'], fill_value='')
data['repeat'] = 0

for i, row in data.iterrows():
  split = row.raw.split(" ")
  data.at[i, 'command'] = split[0]
  sign = split[1][0]
  num = int(split[1][1:len(split[1])])
  if sign == "+":
    data.at[i, 'num'] = num
  else:
    data.at[i, 'num'] = -num

def get_acc_value(input, part1 = False):
  data = input.copy()
  accumulator = 0
  index = 0
  while (True):
    if index == len(data) - 1:
      return accumulator

    if data['repeat'][index] == 1:
      if part1 == False:
        return -99999
      else:
        return accumulator

    if data['command'][index] == "acc":
      data.at[index, 'repeat'] = 1
      accumulator += data['num'][index]
      index += 1
    elif data['command'][index] == "jmp":
      data.at[index, 'repeat'] = 1
      index += data['num'][index]
    else:
      data.at[index, 'repeat'] = 1
      index += 1

# Part 1
print(get_acc_value(data, True))

# Part 2
for i, row in data.iterrows():
  acc = 0
  test = data.copy()
  if row.command == 'jmp':
    test.at[i, 'command'] = 'nop'
    acc = get_acc_value(test)
    if acc != -99999:
      print(acc)
  if row.command == 'nop':
    test.at[i, 'command'] = 'jmp'
    acc = get_acc_value(test)
    if acc != -99999:
      print(acc) 
# Day 9
with open('day9input.txt','r') as f:
    input = f.read().split('\n')
    input = [int(line) for line in input[0:len(input)-1]]

def get_match(num_list, match):
  x = list(itertools.combinations(num_list, 2))
  for combo in x:
    if (sum(combo)) == match:
      return 1
  return 0

# Part 1
preamble = []
for i in input:
  if len(preamble) == 25:
    match = get_match(preamble, i)
    if match == 0:
      print(i)
      break
  preamble.append(i)
  if len(preamble) < 25:
    continue
  if len(preamble) > 25:
    preamble.pop(0)
    
# Part 2
nums = []
j = 0
while sum(nums) != i:
  if sum(nums) < i:
    nums.append(input[j])
    j += 1
  if sum(nums) > i:
    nums.pop(0)
print(min(nums) + max(nums))
# Day 10
with open('day10input.txt','r') as f:
    input = f.read().split('\n')
    input = sorted([int(line) for line in input[0:len(input)-1]])
    input.append(max(input)+3)
    input.insert(0, 0)
  
# Part1
diff1 = 0
diff3 = 0
for idx, val in enumerate(input):
  prev = input[idx-1]
  if idx == 0:
    prev = 0
  if val - prev == 1:
    diff1 += 1
  if val - prev == 3:
    diff3 += 1
print(diff1*diff3)

# Part 2
def valid(input):
  dropout = []
  for idx, val in enumerate(input):
    prev = input[idx-1]
    if idx == 0:
      continue
    if val - prev > 3:
      return 0
    return 1
  
def get_ways(split):
  global ways
  is_valid = valid(split)
  if is_valid == 0:
    return 0
  if is_valid == 1:
    ways.add(tuple(split))

  for idx, val in enumerate(split):
    if idx == 0:
      continue
    if idx == len(split)-1:
      continue
    get_ways(split[:idx] + split[idx+1:])

split_input = []
first = 0
for idx, val in enumerate(input):
  prev = input[idx-1]
  if idx == 0:
    prev = 0
  if val - prev == 3:
    new = input[first:idx]
    first = idx
    if len(new) >= 3:
      split_input.append(new)

x = 1
ways = set()
for split in split_input:
  get_ways(split)
  x = x * len(ways)
  ways = set()
print(x)
# Day 11
import hashlib
with open('day11input.txt','r') as f:
    input = f.read().split('\n')
    input = input[0:len(input)-1]

def get_val(input, ix, iy, part=1):
    maxrow = len(input)
    maxcol = len(input[0])
    if (ix) not in range(0, maxrow):
        return 0
    if (iy) not in range(0, maxcol):
        return 0
    val = input[ix][iy]
    if val == '#':
        return 1
    if part == 2 and val == 'L':
        return -1
    else:
        return 0

def scan(input, threshold=4, part=1):
    changes = []
    output = input.copy()
    for ix, x in enumerate(input):
        for iy, y in enumerate(x):
            if y == '.':
                continue
            if part == 1:
                occ = get_surrounding(input, ix, iy)
            else:
                occ = get_surrounding_pt2(input, ix, iy)
            if y == 'L' and occ == 0:
                changes.append([ix, iy, '#'])
            if y == '#' and occ >= threshold:
                changes.append([ix, iy, 'L'])
    # apply changes
    for x in changes:
        ix, iy, newval = x[0], x[1], x[2]
        maxcol = len(input[0])
        output[ix] = output[ix][0:iy] + newval + output[ix][iy+1:maxcol]
    return output

def count_occupied(input):
    count = 0
    for x in input:
        for y in x:
            if y == '#':
                count += 1
    return count

# Part 1
def get_surrounding(input, ix, iy):
    # return # of occupied adjacent seats
    a = get_val(input, ix-1, iy-1)
    b = get_val(input, ix  , iy-1)
    c = get_val(input, ix+1, iy-1)
    d = get_val(input, ix-1, iy)
    e = get_val(input, ix+1, iy)
    f = get_val(input, ix-1, iy+1)
    g = get_val(input, ix  , iy+1)
    h = get_val(input, ix+1, iy+1)
    return a+b+c+d+e+f+g+h

states = {}     
while True:
    input = scan(input)
    hash = hashlib.sha1(str(input).encode()).hexdigest()
    if hash in states:
        count = count_occupied(input)
        print(count)
        break
    states[hash] = ''

# Part 2 - Inefficient...takes a minute to run.
with open('day11input.txt','r') as f:
    input = f.read().split('\n')
    input = input[0:len(input)-1]

def get_idxs(maxrow, maxcol, ix, iy, xop, yop):
    while ix in range(0, maxrow) and iy in range(0, maxcol):
        ix = ix + xop
        iy = iy + yop
        val = get_val(input, ix, iy, part=2)
        if val == 1:
            return 1
        if val == -1:
            return 0
    return 0

def get_surrounding_pt2(input, ix, iy):
    # return # of occupied visible seats
    val = 0
    maxrow = len(input)
    maxcol = len(input[0])
    for x in [(-1, -1),(-1, 0),(-1, 1),(0, -1),(0, 1),(1, -1),(1, 0),(1, 1)]:
        val += get_idxs(maxrow, maxcol, ix, iy, x[0], x[1])
    return val

states = {}     
while True:
    input = scan(input, threshold=5, part=2)
    hash = hashlib.sha1(str(input).encode()).hexdigest()
    if hash in states:
        count = count_occupied(input)
        print(count)
        break
    states[hash] = ''
# Day 12
with open('day12input.txt','r') as f:
    input = [(line.strip()) for line in f]
    input = [(x[0], int(x[1:len(x)])) for x in input]

# Part 1
x = 0
y = 0
facing = 'E'
forward={'N': ('y', 1),  'E': ('x', 1), 'S': ('y', -1), 'W': ('x', -1)}
dir_to_degrees={'N': 90, 'E': 0, 'S': 270, 'W': 180}
degrees_to_dir={0: 'E', 90: 'N',180: 'W', 270: 'S'}

for cmd in input:
  dir, num = cmd[0], cmd[1]
  if dir == 'F':
    fcmd = forward[facing]
    if fcmd[0] == 'y':
      y += num * fcmd[1]
    if fcmd[0] == 'x':
      x += num * fcmd[1]
  
  if dir == 'L':
    current_deg = dir_to_degrees[facing]
    current_deg = (current_deg + num + 360)%360
    facing = degrees_to_dir[current_deg]
  if dir == 'R':
    current_deg = dir_to_degrees[facing]
    current_deg = (current_deg - num)%360
    facing = degrees_to_dir[current_deg]

  if dir == 'N':
    y += num
  if dir == 'S':
    y -= num
  if dir == 'E':
    x += num
  if dir == 'W':
    x -= num
print(abs(x)+abs(y))

# Part 2
x = 0
y = 0
wpx = 10
wpy = 1
facing = 'E'
left_rotate={0: (1, 1), 90: (-1, 1),180: (-1, -1), 270: (1, -1)}
right_rotate={0: (1, 1), 90: (1, -1),180: (-1, -1), 270: (-1, 1)}

def rotate(wpx, wpy, deg, way):
  deg = deg%360
  if deg == 90 or deg == 270:
    wpx, wpy = wpy, wpx
  if way == 'L':
    wpx = wpx*left_rotate[deg][0]
    wpy = wpy*left_rotate[deg][1]
  if way == 'R':
    wpx = wpx*right_rotate[deg][0]
    wpy = wpy*right_rotate[deg][1]
  return wpx, wpy

for cmd in input:
  dir, num = cmd[0], cmd[1]
  if dir == 'F':
    x = x + wpx*num
    y = y + wpy*num
  
  if dir == 'L':
    wpx, wpy = rotate(wpx, wpy, num, 'L')
  if dir == 'R':
    wpx, wpy = rotate(wpx, wpy, num, 'R')

  if dir == 'N':
    wpy += num
  if dir == 'S':
    wpy -= num
  if dir == 'E':
    wpx += num
  if dir == 'W':
    wpx -= num
print(abs(x)+abs(y))
# Day 13
with open('day13input.txt','r') as f:
    input = [(line.strip()) for line in f]
    timestamp = int(input[0])
    all_buses = input[1].split(',')
    buses = []
    for idx, bus in enumerate(all_buses):
      if bus.isdigit() == True:
        buses.append(int(bus))

# Part 1
depart_time = timestamp
flag = 0
while flag == 0:
  depart_time += 1  
  for bus in buses:
    if depart_time%bus == 0:
      # print(f'Next bus: {bus} at timestamp {depart_time}')
      flag = 1
      break
print((depart_time-timestamp)*bus)

# Part 2
bus_tuples = []
for idx, bus in enumerate(all_buses):
  if bus == 'x':
    continue
  bus_tuples.append((int(bus), idx%int(bus) if idx != 0 else 0))

def is_valid_time(buses, time):
  passed = 0
  for bus in buses:
    mod = time%bus[0] + bus[1]
    if mod == bus[0] or mod == 0:
      passed += 1
  if passed == len(buses):
    return True
  return False

def mini_check(bus_tuples, check, start, mult):
  time = start
  new_mult = 1
  new_start = start

  check_buses = []
  for i in range(check):
    check_buses.append(bus_tuples[i])
    new_mult = new_mult * bus_tuples[i][0]
  while True:
    time += mult
    if is_valid_time(check_buses, time):
      new_start = time  
      break

  # print(f'New Start: {new_start}, New Mult: {new_mult}')
  return new_start, new_mult

new_mult = 1
new_start = 0
for i in range(2, len(bus_tuples)+1):
  new_start, new_mult = mini_check(bus_tuples, check=i, start = new_start, mult=new_mult)
print(new_start)
# Day 14
with open('day14input.txt','r') as f:
    input = [(line.strip()) for line in f]
    input_part1 = [{int(re.search('(?<=\[)[0-9]+(?=\])', row).group(0)): '{:036b}'.format(int(row.split(' = ')[1]))} 
             if re.search('mem', row) 
             else {'1': [m.start(0) for m in re.finditer('1', row.split(' = ')[1])],
                   '0': [m.start(0) for m in re.finditer('0', row.split(' = ')[1])]} for row in input]
    input_part2 = [{'{:036b}'.format(int(re.search('(?<=\[)[0-9]+(?=\])', row).group(0))): int(row.split(' = ')[1])} 
             if re.search('mem', row) 
             else {'1': [m.start(0) for m in re.finditer('1', row.split(' = ')[1])],
                   'float': [m.start(0) for m in re.finditer('X', row.split(' = ')[1])]} for row in input]
# Part 1
idx0, idx1 = [], []
memory = {}
for row in input_part1:
    if '0' in row or '1' in row:
        idx0, idx1 = row.get('0', []), row.get('1', [])
    else:
        memkey = list(row.keys())[0]
        memval = row[memkey]
        for idx in idx0:
            memval = memval[:idx] + '0' + memval[idx+1:]
        for idx in idx1:
            memval = memval[:idx] + '1' + memval[idx+1:]
        memory[memkey] = memval
sum = 0
for key in memory:
    sum += int(memory[key], 2)
print(sum) # 3059488894985

# Part 2
idx1, idxfloat = [], []
memory = {}
for row in input_part2:
    if '1' in row or 'float' in row:
        idx1, idxfloat = row.get('1', []), row.get('float', [])
    else:
        memkey = list(row.keys())[0]
        memval = row[memkey]
        for idx in idx1:
            memkey = memkey[:idx] + '1' + memkey[idx+1:]
        combos = list(itertools.product(['0', '1'], repeat = len(idxfloat)))
        for i in combos:
            float_memkey = memkey
            for j in range(len(i)):
                float_memkey = float_memkey[:idxfloat[j]] + i[j] + float_memkey[idxfloat[j]+1:]
            memory[float_memkey] = memval
sum = 0
for key in memory:
    sum += memory[key]
print(sum) # 2900994392308
# Day 15
def get_answer(input, stop):
    round = len(input)
    spoken = input[-1]
    prev_idx = {}
    for idx, val in enumerate(input[:-1]):
        prev_idx[val] = idx+1

    while True:
        if round == stop:
            return spoken 
        if spoken not in prev_idx:
            prev_idx[spoken] = round
            next_spoken = 0
        else:
            next_spoken = round-prev_idx[spoken]
            prev_idx[spoken] = round
        spoken = next_spoken
        round += 1

# Part 1
print(get_answer(input=[14,8,16,0,1,17], stop=2020))

# Part 2; ~12 seconds
print(get_answer(input=[14,8,16,0,1,17], stop=30000000))
# Day 16
with open('day16input.txt','r') as f:
    input = f.read().split('\n\n')
    fields = input[0]
    fields = re.sub('[a-z| ]*: ', '', fields)
    my_ticket = input[1].split(':\n')[1]
    my_ticket = [int(line) for line in my_ticket.split(',')]
    nearby = input[2].split(':\n')[1]

# Part 1
x = re.split('\n| or ', fields)
x = [(int(line.split('-')[0]), int(line.split('-')[1])) for line in x]

y = nearby.split('\n')
y = [line.split(',') for line in y[:len(y)-1]]

valid_tix = [my_ticket] # start with my own ticket
sum = 0
for ticket in y:
    valid = True
    for fieldval in ticket:
        failed = 0
        for ranges in x:
            if int(fieldval) < ranges[0] or int(fieldval) > ranges[1]:
                failed += 1
        if failed == len(x):
            sum += int(fieldval)
            valid = False
    if valid == True:
        valid_tix.append(ticket)
print(sum) # 25788

# Part 2
def is_valid(item, condition):
    min1 = int(condition[0][0]) 
    max1 = int(condition[0][1]) 
    min2 = int(condition[1][0]) 
    max2 = int(condition[1][1]) 
    if int(item) not in range(min1, max1+1) and int(item) not in range(min2, max2+1):
        return False
    return True

x = re.split('\n', fields)
x = [line.split(' or ') for line in x]
x = [[(line[0].split('-')[0], line[0].split('-')[1]), (line[1].split('-')[0], line[1].split('-')[1])]  for line in x]
df = pd.DataFrame(valid_tix)
valid_cols = {}

for idx, condition in enumerate(x):
    if idx not in valid_cols:
        valid_cols[idx] = []

    # for each field, check the values on the ticket for that field
    for col, data in df.iteritems():
        valid = True
        
        # for each value, if it is valid for the field, then the ticket is ok
        for item in data.values:
            if not is_valid(item, condition):
                valid = False
                break
        if valid == True:
            valid_cols[idx].append(col)

# manually look through the valid_cols dict to match fields with columns
# turns out the departure fields 0-6 are for columns 13, 10, 7, 17, 19, 16
# multiply the columns matching the six departure fields on my ticket together for the answer
print(my_ticket[17]*my_ticket[13]*my_ticket[16]*my_ticket[19]*my_ticket[10]*my_ticket[7])
# Day 17
input = '''####...#
......##
####..##
##......
..##.##.
#.##...#
....##.#
.##.#.#.'''.split('\n')

# Part 1
world = {
    0: input,
}

def add_new_layer(old_world):
    world = old_world.copy()
    newrow = '.' * len(world[0][0]) # new row to insert
    # Expand Z Axis
    newz = [newrow] * len(world[0])
    world[max(world) + 1], world[min(world) - 1] = newz, newz
    # Expand X Axis
    for z in world:
         world[z] = [newrow] + world[z] + [newrow]
    # Expand Y Axis
    for z in world:
         for x in range(0, len(world[z])):
            world[z][x] = '.' + world[z][x] + '.'
    return world

def count_adj(z, x, y, world, original):
    adj = 0
    for z1 in [-1, 0, 1]:
        for x1 in [-1, 0, 1]:
            for y1 in [-1, 0, 1]:
                try:
                    if world[z+z1][x+x1][y+y1] == '#':
                        adj += 1
                except:
                    pass
    if original == '#':
        adj = adj - 1
    return adj

def count_active_in_world(world):
    active = 0
    for z in world:
        for x in range(0, len(world[z])):
            for y in range(0, len(world[z][x])):
                if world[z][x][y] == '#':
                    active += 1
    return active

# Repeat for 6 rounds
for _ in [0] * 6:
    world = add_new_layer(world)
    becomes_active = []
    becomes_inactive = []
    for z in world:
        for x in range(0, len(world[z])):
            for y in range(0, len(world[z][x])):
                original = world[z][x][y]
                adj = count_adj(z, x, y, world, original)
                if original == '.' and adj == 3:
                    becomes_active.append([z, x, y])
                if original == '#' and not (adj == 2 or adj == 3):
                    becomes_inactive.append([z, x, y])
                
    for point in becomes_active:
        z, x, y = point[0], point[1], point[2]
        world[z][x] = world[z][x][:y] + '#' + world[z][x][y+1:]
    for point in becomes_inactive:
        z, x, y = point[0], point[1], point[2]
        world[z][x] = world[z][x][:y] + '.' + world[z][x][y+1:]
print(count_active_in_world(world))


# Part 2
world = {
    0: {0: input},
}

def add_new_layer(old_world): # DIMS: world[w][z][x][y]
    world = old_world.copy()
    newx = '.' * len(world[0][0][0])
    newz = [newx] * len(world[0][0])
    neww = {}
    
    # Expand W Axis
    newmax = max(world) + 1
    newmin = min(world) - 1
    for z in world[0]:
        neww[z] = newz
    world[newmax], world[newmin] = neww, neww

    # Expand Z Axis
    newmax = max(world[0]) + 1
    newmin = min(world[0]) - 1
    for w in world:
        world[w][newmax], world[w][newmin] = newz, newz

    # Expand X Axis
    x_copy = deepcopy(world)
    for w in x_copy:
        for z in x_copy[w]:
            world[w][z] = [newx] + x_copy[w][z] + [newx]

    # Expand Y Axis
    y_copy = deepcopy(world)
    for w in y_copy:
        for z in y_copy[w]:
            for x in range(0, len(y_copy[w][z])):
                world[w][z][x] = '.' + y_copy[w][z][x] + '.'
    return world

def count_adj(w, z, x, y, world, original):
    adj = 0
    for w1 in [-1, 0, 1]:
        for z1 in [-1, 0, 1]:
            for x1 in [-1, 0, 1]:
                for y1 in [-1, 0, 1]:
                    try:
                        if world[w+w1][z+z1][x+x1][y+y1] == '#':
                            adj += 1
                    except:
                        pass
    if original == '#':
        adj = adj - 1
    return adj

def count_active_in_world(world):
    active = 0
    for w in world:
        for z in world:
            for x in range(0, len(world[w][z])):
                for y in range(0, len(world[w][z][x])):
                    if world[w][z][x][y] == '#':
                        active += 1
    return active

# Repeat for 6 rounds
for _ in [0] * 6:
    world = add_new_layer(world)
    becomes_active = []
    becomes_inactive = []
    for w in world:
        for z in world[w]:
            for x in range(0, len(world[w][z])):
                for y in range(0, len(world[w][z][x])):
                    original = world[w][z][x][y]
                    adj = count_adj(w, z, x, y, world, original)
                    if original == '.' and adj == 3:
                        becomes_active.append([w, z, x, y])
                    if original == '#' and not (adj == 2 or adj == 3):
                        becomes_inactive.append([w, z, x, y])
                
    for point in becomes_active:
        w, z, x, y = point[0], point[1], point[2], point[3]
        world[w][z][x] = world[w][z][x][:y] + '#' + world[w][z][x][y+1:]
    for point in becomes_inactive:
        w, z, x, y = point[0], point[1], point[2], point[3]
        world[w][z][x] = world[w][z][x][:y] + '.' + world[w][z][x][y+1:]
print(count_active_in_world(world)) # 2448
# Day 18
with open('day18input.txt','r') as f:
    input = [(line.strip()) for line in f]
    
def eval_no_parens(state):
    try:
        state = state.group()
    except:
        pass
    state = re.sub('\(|\)', '', state)
    split = state.split(" ")
    while len(split) > 2:
        first_op = str(eval(''.join(split[0:3])))
        split = [first_op] + split[3:]
    return ''.join(split)

def get_solution(input):
    while '(' in input:
        input = re.sub(r'\([^()]*\)', eval_no_parens, input)
    input = eval_no_parens(input)
    return input

sum = 0
for line in input:
    solution = get_solution(line)
    sum += int(solution)
print(sum)

# Part 2
def solve_part_two(input):
    input = '(' + input + ')'
    input = re.sub('\*', ') * (', input)
    ans = eval(input)
    return ans

sum = 0
for line in input:
    solution = solve_part_two(line)
    sum += int(solution)
    count += 1
print(sum)

# Day 19
with open('day19input.txt','r') as f:
    input = f.read().split('\n\n')
    rules = input[0].split('\n')
    messages = input[1].split('\n')
    messages = messages[:len(messages)-1]

r = {}
for row in rules:
    s = row.split(': ')
    key = (s[0])
    if 'a' in row:
        r[key] = 'a'
        continue
    if 'b' in row:
        r[key] = 'b'
        continue
    val = s[1].split(' | ')
    val = [t.split(' ') for t in val]
    r[key] = val

# Check to see if a string matches a specific rule option
def run_seq(r, rule, string):
    if not rule:
        yield string
    else:
        k, *rule = rule
        for string in run(r, k, string):
            yield from run_seq(r, rule, string) 


# Check each option that produces a valid output
def run_alt(r, rule_options, string):
    for rule in rule_options:
        yield from run_seq(r, rule, string)


# Top-level check
# If rule is a list, then move on to the next step. 
# Otherwise, return string leftovers.
def run(r, rule, string):
    if isinstance(r[rule], list):
        yield from run_alt(r, r[rule], string)
    else:
        if string and (string[0] == r[rule]):
            yield string[1:]

# Check if a string matches rule 0
def match(r, string):
    return(any(m == '' for m in run(r, '0', string)))


# Part 1
sum = 0 
for m in messages:
    if match(r, m):
        sum += 1
print(sum)

# Part 2
r['8'] = [['42'], ['42', '8']]
r['11'] = [['42', '31'], ['42', '11', '31']]
sum = 0 
for m in messages:
    if match(r, m):
        sum += 1
print(sum)
# Day 20
with open('day20input.txt','r') as f:
    input = f.read().split('\n\n')
    input = input[:len(input)-1]

tiles = {}
positions = {}

for tile in input:
    tile_num = re.findall('(?<= )[0-9]{4}(?=:)', tile)[0]
    tile_content = tile.split(':\n')[1].split('\n')
    
    top = tile_content[0]
    bottom = tile_content[-1]
    left = ''.join([x[0] for x in tile_content])
    right = ''.join([x[-1] for x in tile_content])

    tiles[tile_num] = {
        'full': tile_content,
        'sides': [top, bottom, left, right, top[::-1], bottom[::-1], left[::-1], right[::-1]]
    }
    positions[tile_num] = set()

# Part 1
for t1 in tiles:
    for t2 in tiles:
        # don't check against itself
        if tiles[t1]['full'] == tiles[t2]['full']: 
            continue
        for side1 in tiles[t1]['sides']:
            for side2 in tiles[t2]['sides']:
                if side1 == side2:
                    positions[t1].add(t2)
                    continue

solution = 1
edges = {}
middle = {}
start = ''
for tile in positions:
    if len(positions[tile]) == 2:
        solution = solution * int(tile)
        start = tile

    if len(positions[tile]) <= 3:
        edges[tile] = positions[tile]
    else:
        middle[tile] = positions[tile]
print(solution)

# Part 2
def get_side_contents(row, positions, completed=[]):
    done = False
    while done == False:
        # check the last tile
        current_tile = row[-1]

        for tile in positions[current_tile]:
            
            num_borders = len(positions[tile]) if tile in positions else None
            if not num_borders:
                continue
            # end of row
            if num_borders == 2 and tile not in row:
                row.append(tile)
                done = True
                break
            if num_borders == 3 and tile not in completed and tile not in row:
                row.append(tile)
                break
    return row

# Get edges of picture
top_row = get_side_contents([start], edges)
left_col = get_side_contents([top_row[0]], edges, completed = top_row)
right_col = get_side_contents([top_row[-1]], edges, completed = top_row + left_col)
bottom_row = get_side_contents([left_col[-1]], edges, completed = top_row + left_col + right_col)

# Form edges of picture
picture = [[0] * len(top_row)] * len(left_col)
picture[0], picture[-1] = top_row, bottom_row

picture = pd.DataFrame(picture)
picture[0] = left_col
picture[(len(picture)-1)] = right_col

# Fill in middle of picture
delete_these = []
for i, row in picture.iterrows():
    if i == 0 or i == len(picture) - 1:
        continue
    for j in [1, 10, 2, 9, 3, 8, 4, 7, 5, 6]: 
        surrounding_vals = [picture[i+1][j], picture[i-1][j], picture[i][j+1], picture[i][j-1]]
        surrounding_vals = [x for x in surrounding_vals if x != 0]
        # check all middle tiles for match
        for middle_tile in middle:
            match = 0
            for middle_tile_border in middle[middle_tile]:
                if middle_tile_border in surrounding_vals:
                    match += 1
            if match >= len(surrounding_vals):
                picture.loc[j, i] = middle_tile
                delete_these.append(middle_tile)
    for i in delete_these:
        try:
            middle.pop(i)
        except:
            pass

# picture ## This is the orientation of tile indexes

# ----------------------------------------------------
# Change all tiles to correct orientation
def flip_horizontal(tile):
    return [x[::-1] for x in tile]

def rotate_90(tile):
    return [''.join(x) for x in list(zip(*reversed(tile)))]

def find_match(right_side, tile_pic):
    # rotate until we find a match
    for _ in [0] * 5:
        left_side = ''.join([x[0] for x in tile_pic])
        if left_side == right_side:
            return tile_pic
        tile_pic = rotate_90(tile_pic)

    tile_pic = flip_horizontal(tile_pic)
    for _ in [0] * 5:
        left_side = ''.join([x[0] for x in tile_pic])
        if left_side == right_side:
            return tile_pic
        tile_pic = rotate_90(tile_pic)
    print("No Match")
    return 

def find_match_2(bottom_side, tile_pic):
    # rotate until we find a match
    for _ in [0] * 5:
        top_side = tile_pic[0]
        if top_side == bottom_side:
            return tile_pic
        tile_pic = rotate_90(tile_pic)

    tile_pic = flip_horizontal(tile_pic)
    for _ in [0] * 5:
        top_side = tile_pic[0]
        if top_side == bottom_side:
            return tile_pic
        tile_pic = rotate_90(tile_pic)
    print("No Match")
    return 

# Go through all tiles, and change orientation in the tiles dictionary
for row in range(0, len(picture)):
    for col in range(0, len(picture)):
        n = picture[col][row]
        
        # for the first column, check it with the one above
        if col == 0:
            if row == 0:
                # getting orientation of first tile - not generalizable to other inputs
                tiles[n]['full'] = flip_horizontal(tiles[n]['full']) 
                continue
            else:
                prev_pic = tiles[picture[col][row-1]]['full']
                bottom_side = prev_pic[-1]
                tile_pic = tiles[n]['full']
                tile_pic = find_match_2(bottom_side, tile_pic)
                tiles[n]['full'] = tile_pic
                continue
        
        # for all other columns
        prev_pic = tiles[picture[col-1][row]]['full']
        right_side = ''.join([x[-1] for x in prev_pic])
        tile_pic = tiles[n]['full']
        tile_pic = find_match(right_side, tile_pic)

        tiles[n]['full'] = tile_pic

# Remove borders 
def remove_borders(tile): # tile is a list of strings
    tile = [x[1:len(x)-1] for x in tile]
    tile.pop(0)
    tile.pop(len(tile)-1)
    return tile

for t in tiles:
    tiles[t]['full'] = remove_borders(tiles[t]['full'])

# Combine all tiles
full_picture = []
for row in range(0, len(picture)):
    full_picture_row = [''] * 8
    for col in range(0, len(picture)):
        n = picture[col][row]
        for idx, line in enumerate(tiles[n]['full']):
            full_picture_row[idx] += line

    full_picture = full_picture + full_picture_row

# Rotate picture until we see monsters (not generalizable to other inputs)
full_picture = rotate_90(rotate_90(full_picture))
# full_picture ## This is the full picture

# ----------------------------------------
# Find the number of monsters
monster = [(0,0), (1,-1), (4,-1), (5,0), (6,0), (7,-1), (10,-1), (11,0), (12,0), (13,-1), (16,-1), (17,0), (18,0), (19,0), (18,1)]
num_monsters = 0
for rowix, row in enumerate(full_picture):
    for charix, char in enumerate(row):
        count = 0
        for i, j in monster:
            try:
                if full_picture[charix+i][rowix+j] == '#':
                    count += 1
            except:
                pass
        if count and count == len(monster):
            # print("MONSTER FOUND")
            num_monsters += 1

# Count the number of #'s that aren't part of sea monsters
waves = 0
for line in full_picture:
    waves += line.count('#')
print(waves - 15 * len(monster))
# Day 21
with open('day21input.txt','r') as f:
    input = [(line.strip()) for line in f]
    ingredients = [x.split(' (')[0] for x in input]
    allergens = [re.search('(?<=\(contains ).+?(?=\))', x).group(0) for x in input]

# Identify potential ingredients for each allergen
r = {}
for i in range(0, len(input)):
    food_list      = ingredients[i].split(' ')
    allergens_list = allergens[i].split(', ')

    for allerg in allergens_list:
        if allerg not in r:
            r[allerg] = food_list
        else:
            r[allerg] = list(set(r[allerg]) & set(food_list))

# Create a list of all potential allergens
potential_allergens = []
for allergen in r:
    for potential_ingredient in r[allergen]:
        potential_allergens.append(potential_ingredient)

# Part 1: Count the total number of non-allergen ingredients
count = 0
for recipe in ingredients:
    recipe = recipe.split(' ')
    for x in recipe:
        if x not in potential_allergens:
            count += 1
print(count)

# Part 2: List ingredients in alphabetical order
map = { # since the list is short, we can create the map just by looking at 'r', the potential ingredients for each allergen
    'nuts': 'kfxr',
    'peanuts': 'xmhsbd',
    'soy': 'chbtp',
    'shellfish': 'xzhxj',
    'wheat': 'cqvc',
    'eggs': 'ntft',
    'fish': 'nhx',
    'sesame': 'rrjb'
}
all_allergens = ['nuts', 'peanuts', 'soy', 'shellfish', 'wheat', 'eggs', 'fish', 'sesame']
all_allergens = sorted(all_allergens)

solution = ''
for x in all_allergens:
    solution += map[x] + ','
solution = solution[:len(solution)-1]
print(solution)
# Day 22
with open('day22input.txt','r') as f:
    input = f.read().split('\n\n')

# Part 1
player_one = [int(x) for x in input[0].split(':\n')[1].split('\n')]
player_two = [int(x) for x in input[1][:-1].split(':\n')[1].split('\n')]

def play_one_round(p1, p2):
    card1 = p1.pop(0)
    card2 = p2.pop(0)
    if card1 > card2:
        p1 += [card1] + [card2]
    else:
        p2 += [card2] + [card1]
    return p1, p2

def get_score(p1, p2):
    score = 0
    p = p1 if len(p1) > 0 else p2
    for idx, x in enumerate(p):
        score += x * (len(p) - idx)
    return score

while len(player_one) > 0 and len(player_two) > 0:
    player_one, player_two = play_one_round(player_one, player_two)
print(get_score(player_one, player_two))


# Part 2 - takes a couple secs to run
player_one = [int(x) for x in input[0].split(':\n')[1].split('\n')]
player_two = [int(x) for x in input[1][:-1].split(':\n')[1].split('\n')]

def play_one_round(p1, p2, prev_rounds):
    # Check if this has been played before
    cards = str(p1) + str(p2)
    if cards in prev_rounds:
        return p1, [], prev_rounds # give p1 the win
    
    prev_rounds.add(cards)
    card1 = p1.pop(0)
    card2 = p2.pop(0)

    # Do a recursive combat
    recursive, recursive_p2_win = False, False
    if card1 <= len(p1) and card2 <=len(p2):
        recursive = True
        xp1, _, _ = play_game(p1[:card1], p2[:card2], prev_rounds = set())
        if len(xp1) == 0:
            recursive_p2_win = True

    # Continue with game
    if recursive and recursive_p2_win:
        p2 += [card2] + [card1]
    elif recursive and not recursive_p2_win:
        p1 += [card1] + [card2]
    elif (card1 > card2):
        p1 += [card1] + [card2]
    else:
        p2 += [card2] + [card1]
    return p1, p2, prev_rounds
    

def get_score(p1, p2):
    score = 0
    p = p1 if len(p1) > 0 else p2
    for idx, x in enumerate(p):
        score += x * (len(p) - idx)
    return score


def play_game(p1, p2, prev_rounds=set()):
    while len(p1) > 0 and len(p2) > 0:
        p1, p2, prev_rounds = play_one_round(p1, p2, prev_rounds)
    return p1, p2, prev_rounds

player_one, player_two, _ = play_game(player_one, player_two)
print(get_score(player_one, player_two))
# Day 23
input = [5,3,8,9,1,4,7,6,2]
#input = [3,8,9,1,2,5,4,6,7]

# Part 1
def get_cups(crab, input):
    x = input.pop((input.index(crab) + 1) % len(input))
    y = input.pop((input.index(crab) + 1) % len(input))
    z = input.pop((input.index(crab) + 1) % len(input))
    return x, y, z, input

def find_dest_idx(crab, input):
    dest = crab - 1
    xmin, xmax = min(input), max(input)
    while dest not in input:
        dest = dest - 1
        if dest < xmin:
            dest = xmax
    return input.index(dest)

def score(input):
    one = input.index(1)
    s = input[one+1:] + input[:one]
    s = [str(x) for x in s]
    return ''.join(s)

crab = input[0]
moves = 0
while moves < 100:
    x, y, z, input = get_cups(crab, input)
    dest_idx = find_dest_idx(crab, input)
    input = input[:dest_idx + 1] + [x] + [y] + [z] + input[dest_idx + 1:]
    crab = input[(input.index(crab)+1)%len(input)]
    moves += 1
print(score(input))

# Part 2 Solution, taken from https://github.com/sophiebits/adventofcode/blob/main/2020/day23.py

line = [5,3,8,9,1,4,7,6,2]
line.extend(range(10, 10 ** 6 + 1))

class Node:
    __slots__ = ('value', 'next')
    def __init__(self, value):
        self.value = value
        self.next = None

def run(line, steps=100):
    nodes = {num: Node(num) for num in line}
    for num, num2 in zip(line, line[1:] + line[:1]):
        nodes[num].next = nodes[num2]
    front = nodes[line[0]]
    for stepno in range(steps):
        a = front.next
        b = a.next
        c = b.next

        look = front.value
        while True:
            look -= 1
            if look == 0:
                look = len(line)
            if look not in (a.value, b.value, c.value):
                break

        after = nodes[look]
        front.next, front, after.next, c.next = c.next, c.next, a, after.next

        if stepno % 1000000 == 0:
            print(stepno)

    print(nodes[1].next.value * nodes[1].next.next.value)

run(line, 10 ** 7)
# Day 23 Part 2 - Failed Attempt
input = [3,8,9,1,2,5,4,6,7]
labelslen = len(input)
r = [0] * (labelslen+1)
r[0] = input[0]
for idx, x in enumerate(input):
    if idx < len(input) - 1:
        r[x] = input[idx+1]
    else:
        r[x] = 0
        last_label = x

def move(crab, r, labelslen, last_label):
    print("Starting game...", build(r))
    print(f"Linked: {r}")
    # get next 3 values
    x = r[crab] if r[crab] != 0 else r[0]
    y = r[x]    if r[x] != 0 else r[0]
    z = r[y]    if r[y] != 0 else r[0]
    if last_label in [x, y, z]:
        r[last_label] = r[z]
        last_label = XXXXXXX # NEEDS TO BE FIXED
    # "remove" from the list
    r[crab] = r[z] if r[z] != 0 else r[0]
    print(f"Removed {x}, {y}, {z} from r, which is now {r}")

    # find destination
    dest = crab - 1
    while dest in [x, y, z] or dest <= 0: 
        print(dest)   
        if dest == 0:
            dest = labelslen
        else:
            dest = dest - 1
    if dest == 0:
        dest = r[dest]
    print(f"Destination found at {dest}")
    # insert values at destination
    dest_next = r[dest] if r[dest] != 0 else r[0]
    if r[dest] != 0:
        r[dest] = x
    else:
        r[r[dest]] = x
    if r[z] != 0:
        r[z] = dest_next
    else:
        r[r[z]] = dest_next
    # new crab cup
    crab = r[crab] if r[crab] != 0 else r[0]
    #print(f'New crab at {crab}, in list {build(r)}, linked list {r}')
    return crab, r

# Part 2
crab = input[0]
moves = 0
while moves < 100:
    crab, r = move(crab, r, labelslen, last_label)
    if len(r) != len(set(r)):
        print(f"YEET, {moves}")
        break
    moves += 1
# Day 24
with open('day24input.txt','r') as f:
    input = [(line.strip()) for line in f]
    
# Part 1
r = []
locs = []
for tile in input:
    loc = [0,0,0]

    ne = re.findall('ne', tile)
    tile = re.sub('ne', '', tile)

    nw = re.findall('nw', tile)
    tile = re.sub('nw', '', tile)

    se = re.findall('se', tile)
    tile = re.sub('se', '', tile)

    sw = re.findall('sw', tile)
    tile = re.sub('sw', '', tile)

    e = re.findall('e', tile)
    w = re.findall('w', tile)

    for i in ne + nw + se + sw + e + w:
        if i == 'e':
            loc[0] += 1
            loc[1] += -1
        if i == 'w':
            loc[0] += -1
            loc[1] += +1
        if i == 'ne':
            loc[0] += 1
            loc[2] += -1
        if i == 'nw':
            loc[1] += 1
            loc[2] += -1
        if i == 'se':
            loc[1] += -1
            loc[2] += 1
        if i == 'sw':
            loc[0] += -1
            loc[2] += 1
    locs.append(tuple(loc))
locs = [x for x in locs if locs.count(x) == 1]
print(len(locs)) # 287

# Part 2
world = {
    (0,0,0): 1
}

def expand_world(world):
    new_world = deepcopy(world)
    for tile in world:
        for x in [(0,-1,1),(1,-1,0),(1,0,-1),(0,1,-1),(-1,1,0),(-1,0,1)]:
            border = (tile[0]+x[0], tile[1]+x[1], tile[2]+x[2])
            if border not in world:
                new_world[border] = 0
    return new_world

def count_black_tiles(world):
    num = 0
    for tile in world:
        num += world[tile]
    return num

def num_adjacent_black(world, tile):
    num = 0
    for x in [(0,-1,1),(1,-1,0),(1,0,-1),(0,1,-1),(-1,1,0),(-1,0,1)]:
        border = (tile[0]+x[0], tile[1]+x[1], tile[2]+x[2])
        try:
            num += world[border]
        except:
            pass
    return num

def day(world):
    world = expand_world(world)
    to_white = []
    to_black = []
    for tile in world:
        num_adj = num_adjacent_black(world, tile)
        if (world[tile] == 1 and num_adj == 0) or (world[tile] == 1 and num_adj > 2):
            to_white.append(tile)
        if world[tile] == 0 and num_adj == 2:
            to_black.append(tile)

    for x in to_white:
        world[x] = 0
    for x in to_black:
        world[x] = 1
    return world

# Create initial world on Day 0 - black tiles
for x in locs:
    world[x] = 1

# Repeat for 100 days - takes ~10 seconds
for _ in [0]*100:
    world = day(world)
print(count_black_tiles(world))
# Day 25!
public_key_door = 9033205
public_key_card = 9281649

subject = 7
big_number = 20201227

# Get loop size of door
val = 1
door_loop_size = 0
while val != public_key_door:
    val = val * subject
    val = val % big_number
    door_loop_size += 1

# Get loop size of card
val = 1
card_loop_size = 0
while val != public_key_card:
    val = val * subject
    val = val % big_number
    card_loop_size += 1

# Get encryption key via door
val = 1
for _ in [0] * door_loop_size:
    val = val * public_key_card
    val = val % big_number
print(val)

# Get encryption key via card
val = 1
for _ in [0] * card_loop_size:
    val = val * public_key_door
    val = val % big_number
print(val)<jupyter_output>9714832
9714832
{'fraction_non_alphanumeric': 0.16708545458518145, 'fraction_numerical': 0.03205366311971508, 'mean_word_length': 2.3578870139398385, 'pattern_counts': {'":': 3, '<': 25, '<?xml version=': 0, '>': 28, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '1', 'dir': 'jupyter-structured-clean-dedup', 'id': '356581', 'license': 'MIT', 'n_tokens_mistral': 18025, 'n_tokens_neox': 17055, 'n_words': 4742, 'path': 'Advent_of_Code_2020.ipynb', 'repo_name': 'panthonies/adventofcode', 'size': '45767'}