# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3.9.7 64-bit
#     name: python3
# ---
# default_exp aoc
from nbdev import *
from nbdev.showdoc import *
# from __future__  import annotations
# # <NAME> AoC helper functions
#
# > All of these are taken from github of <NAME>
# > https://github.com/norvig/pytudes/blob/master/ipynb/Advent-2020.ipynb
#exporti
from collections import defaultdict
from itertools   import chain
import numpy as np
import re
from typing import Union
from functools import partial
import string

# +
#export
def data(filename='input', parser=str, sep='\n') -> list:
    "Split the day's input file into sections separated by `sep`, and apply `parser` to each."
    sections = open(f'{filename}.txt').read().rstrip().split(sep)
    return [parser(section) for section in sections]
     
def do(day, *answers) -> dict[int, int]:
    "E.g., do(3) returns {1: day3_1(in3), 2: day3_2(in3)}. Verifies `answers` if given."
    g = globals()
    got = []
    for part in (1, 2):
        fname = f'day{day}_{part}'
        if fname in g: 
            got.append(g[fname](g[f'in{day}']))
            if len(answers) >= part: 
                assert got[-1] == answers[part - 1], (
                    f'{fname}(in{day}) got {got[-1]}; expected {answers[part - 1]}')
    return got

# -
#export
def quantify(iterable, pred=bool) -> int:
    "Count the number of items in iterable for which pred is true."
    return sum(1 for item in iterable if pred(item))

intchecker = partial(lambda x: isinstance(x, int))
assert quantify(['a','11',11,22], pred=intchecker) == 2
assert quantify([1,2,3,4,5,6,7], lambda x: x==5) == 1

#export
def first(iterable, default=None) -> object:
    "Return first item in iterable, or default."
    return next(iter(iterable), default)

assert first([], default = 2) == 2
assert first(['a','b'], default = 2) == 'a'

#export
def rest(sequence) -> object: return sequence[1:]

# +
assert rest([1,2,3,4]) == [2,3,4]
# -
#export
from collections.abc import Iterable
def multimap(items: Iterable[tuple]) -> dict:
    "Given (key, val) pairs, return {key: [val, ....], ...}."
    result = defaultdict(list)
    for key, val in items:
        result[key].append(val)
    return result

# +
d = {i:char for i, char in enumerate(list(string.ascii_uppercase))}
multimap(d.items())
# -
import math
math.prod([1,2,3])

#export
def atom(text: str) -> Union[float, int, str]:
    "Parse text into a single float or int or str."
    try:
        val = float(text)
        return round(val) if round(val) == val else val
    except ValueError:
        return text
atom('11') == 11

#export
def atoms(text: str, ignore=r'', sep=None) -> tuple[Union[int, str]]:
    "Parse text into atoms (numbers or strs), possibly ignoring a regex."
    if ignore:
        text = re.sub(ignore, '', text)
    return tuple(map(atom, text.split(sep)))
atoms('abc 111 def')

#export
def list_atoms(inp: list):
    return tuple(map(atom, inp))

list_atoms(['1', '3.2', 'a', 1])

#export   
def dotproduct(A, B) -> float: return sum(a * b for a, b in zip(A, B))
dotproduct([1,2],[7,8]) == 23

#export
def mapt(fn, *args):
    "map(fn, *args) and return the result as a tuple."
    return tuple(map(fn, *args))
mapt(lambda x: x+'z', list('abcde'))
#export 
cat = ''.join
flatten = chain.from_iterable