tjf's recent activity

  1. Comment on What email client do you use? in ~tech

    tjf
    Link
    On my Linux desktops I use aerc, a terminal-based client. I use its maildir backend, so everything feels snappy. Syncing between my hard drives and the IMAP server is handled by a cronjob running...

    On my Linux desktops I use aerc, a terminal-based client. I use its maildir backend, so everything feels snappy. Syncing between my hard drives and the IMAP server is handled by a cronjob running mbsync. Prior to aerc I used mutt (specifically neomutt).

    On my iPhone I use plain old mail.app. It’s fine, and I do most of email work on desktop anyway. The major third-party mail apps on iOS all seem to have something fundamentally unacceptable to me, such as mail provider lock-in, non-native UI, annoying AI integrations, and other privacy nightmares like fetching your mail onto their own servers. So stock mail.app it is for me.

  2. Comment on Best foreign films and TV shows? in ~tv

    tjf
    Link Parent
    I love Drive My Car, so I'll chime in to recommend Wheel of Fortune and Fantasy (TMDB, IMDb, Letterboxd) which is also directed by Ryusuke Hamaguchi (and released in the same year!). It's an...

    I love Drive My Car, so I'll chime in to recommend Wheel of Fortune and Fantasy (TMDB, IMDb, Letterboxd) which is also directed by Ryusuke Hamaguchi (and released in the same year!). It's an anthology film in three parts, a structure I wish more filmmakers played with.

    1 vote
  3. Comment on When do you listen to podcasts? in ~life

    tjf
    Link
    As others have mentioned, I like to listen to podcasts while doing household chores (washing dishes, folding laundry, cooking dinner). But I'm careful to give my ears a break sometimes, e.g. when...

    As others have mentioned, I like to listen to podcasts while doing household chores (washing dishes, folding laundry, cooking dinner). But I'm careful to give my ears a break sometimes, e.g. when I go for a walk or take a shower or fall asleep. I find I have my best thoughts and ideas in these settings, and my mind can't wander and pay attention to a podcast at the same time.

    1 vote
  4. Comment on LeBron James' path to 40,000 career regular-season points in ~sports.basketball

    tjf
    Link
    Last night LeBron James hit the 40k mark. This is a cool timeline charting his career leading up to that moment. Ironically the Lakers ended up losing the game, which also happened last season...

    Last night LeBron James hit the 40k mark. This is a cool timeline charting his career leading up to that moment. Ironically the Lakers ended up losing the game, which also happened last season when LeBron broke Kareem's record to become the all-time leading scorer. It's hard to see anyone catching this record.

    3 votes
  5. Comment on 50 Algorithms Every Programmer Should Know (Second Edition) in ~comp

    tjf
    Link Parent
    I wouldn't be surprised if this book was itself written by an LLM, seeing how clickbaity that sounds.

    I wouldn't be surprised if this book was itself written by an LLM, seeing how clickbaity that sounds.

    13 votes
  6. Comment on Day 10: Pipe Maze in ~comp.advent_of_code

    tjf
    Link Parent
    I can confirm my input had this property too, and I am thankful for it.

    I can confirm my input had this property too, and I am thankful for it.

    2 votes
  7. Comment on Day 10: Pipe Maze in ~comp.advent_of_code

    tjf
    Link
    Nice ramp up in difficulty from part one to two, which I always appreciate. Seems like there were a couple different ways to solve this, and I'm not convinced mine is a great one. But it works!...

    Nice ramp up in difficulty from part one to two, which I always appreciate.
    Seems like there were a couple different ways to solve this, and I'm not
    convinced mine is a great one. But it works! And it's fast enough to not care
    too much.

    Here are my Python solutions, albeit messy ones:

    Part 1

    Not too bad for a part one. Walk the graph from S all the way back to itself,
    keeping track of steps travelled. Then it works out that the farthest piece
    in the loop from S is half that total loop length.

    #!/usr/bin/env python3
    
    from collections import defaultdict 
    
    DIRECTIONS = (-1 + 0j, 1 + 0j, 0 - 1j, 0 + 1j)
    CONNECTIONS = {'|': (-1 + 0j, 1 + 0j),
                   '-': (0 - 1j, 0 + 1j),
                   'L': (-1 + 0j, 0 + 1j),
                   'J': (-1 + 0j, 0 - 1j),
                   '7': (0 - 1j, 1 + 0j),
                   'F': (0 + 1j, 1 + 0j),
                   '.': (),
                   ' ': ()}
    
    # now that we know the coordinates of 'S', replace it with the pipe that fits
    def normalize_start(g, start):
        v = tuple(d for d in DIRECTIONS if any(start + d + conn == start
                                               for conn in CONNECTIONS[g[start + d]]))
        pipe = next(k for k in CONNECTIONS if set(CONNECTIONS[k]) == set(v))
        g[start] = pipe
    
    # walk around the main loop, return number of steps to make it all the way around
    def walk_loop(g, start):
        steps = 2
        visited = set([start])
        start_conns = tuple(start + conn for conn in CONNECTIONS[g[start]])
        here = start_conns[0]
        while here != start_conns[1]:
            visited.add(here)
            here = next(filter(lambda pos: pos not in visited,
                               (here + conn for conn in CONNECTIONS[g[here]])))
            steps += 1
        return steps
    
    G = defaultdict(lambda: '.')
    for r, line in enumerate(open(0)):
        for c, char in enumerate(line.strip()):
            G[complex(r, c)] = char
    start = next(pos for pos, char in G.items() if char == 'S')
    normalize_start(G, start)
    steps_around = walk_loop(G, start)
    print(steps_around // 2)
    
    Part 2

    The trickiest part in part two was the notion that tiles could squeeze between
    pipes to be considered non-enclosed. My idea to attack this is to "zoom in" on
    the graph, effectively putting empty spaces between every tile in the original
    graph. Because I used complex coordinates, this was as easy as multiplying
    coordinates by 2 and then filling in vertical and horizontal pipe pieces that
    belonged in the newly created empty spaces. I also turned all non-mainloop pipes
    into ground pipes (dots) to make things easier. Then I used flood fill on the
    new zoomed graph on the exterior of the loop found the difference between all
    ground tiles and these exterior ground tiles to end up with the final answer.

    #!/usr/bin/env python3
    
    from collections import defaultdict, deque
    
    DIRECTIONS = (-1 + 0j, 1 + 0j, 0 - 1j, 0 + 1j)
    CONNECTIONS = {'|': (-1 + 0j, 1 + 0j),
                   '-': (0 - 1j, 0 + 1j),
                   'L': (-1 + 0j, 0 + 1j),
                   'J': (-1 + 0j, 0 - 1j),
                   '7': (0 - 1j, 1 + 0j),
                   'F': (0 + 1j, 1 + 0j),
                   '.': (),
                   ' ': ()}
    
    # now that we know the coordinates of 'S', replace it with the pipe that fits
    def normalize_start(g, start):
        v = tuple(d for d in DIRECTIONS if any(start + d + conn == start
                                               for conn in CONNECTIONS[g[start + d]]))
        pipe = next(k for k in CONNECTIONS if set(CONNECTIONS[k]) == set(v))
        g[start] = pipe
    
    # walk around the main loop, return a set containing the coords of all its pieces
    def walk_loop(g, start):
        visited = set([start])
        start_conns = tuple(start + conn for conn in CONNECTIONS[g[start]])
        here = start_conns[0]
        while here != start_conns[1]:
            visited.add(here)
            here = next(filter(lambda pos: pos not in visited,
                               (here + conn for conn in CONNECTIONS[g[here]])))
        visited.add(here)
        return visited
    
    # returns a "zoomed in" graph with empty space between the original's tiles
    def zoom(g, loop, factor=2):
        zg = defaultdict(lambda: ' ')
        for pos, char in g.items():
            zg[pos * factor] = char
        zloop = {pos * factor for pos in loop}
        for pos, char in zg.items():
            if char in ('|-LJ7F') and pos not in zloop:
                zg[pos] = '.'    # set all extra pipes to ground tiles
        max_r = int(max(zg, key=lambda pos: pos.real).real)
        max_c = int(max(zg, key=lambda pos: pos.imag).imag)
        for r in range(0, max_r + 1):
            for c in range(0, max_c + 1):
                pos = complex(r, c)
                if zg[pos] == ' ':
                    up, down, left, right = (pos + d for d in DIRECTIONS)
                    if zg[up] in ('|7F') or zg[down] in ('|JL'):
                        zg[pos] = '|'    # fill in vertical pipe gaps
                    elif zg[left] in ('-LF') or zg[right] in ('-J7'):
                        zg[pos] = '-'    # fill in horizontal pipe gaps
        return zg
    
    # starting from outside of the main loop, flood tiles around the loop
    # return a set containing the coords of all outside tiles
    def flood(g):
        min_pos = complex(min(g, key=lambda pos: pos.real).real - 1,
                          min(g, key=lambda pos: pos.imag).imag - 1)
        max_pos = complex(max(g, key=lambda pos: pos.real).real + 1,
                          max(g, key=lambda pos: pos.imag).imag + 1)
        start = min_pos
        outside = set()
        todo = deque([start])
        while todo:
            here = todo.popleft()
            if here in outside:
                continue
            outside.add(here)
            for d in DIRECTIONS:
                pos = here + d
                if ((min_pos.real <= pos.real <= max_pos.real)
                    and (min_pos.imag <= pos.imag <= max_pos.imag)
                    and g[pos] in ('.', ' ')):
                    todo.append(pos)
        return outside
    
    G = defaultdict(lambda: '.')
    for r, line in enumerate(open(0)):
        for c, char in enumerate(line.strip()):
            G[complex(r, c)] = char
    start = next(pos for pos, char in G.items() if char == 'S')
    normalize_start(G, start)
    loop = walk_loop(G, start)
    ZG = zoom(G, loop)
    outside = flood(ZG)
    num_enclosed = sum(pos not in outside and char == '.' for pos, char in ZG.items())
    print(num_enclosed)
    

    All in all I liked today's problem, as I often do with graph-type problems.
    Also this is my first day this year that I chose to use complex numbers for
    coordinates, which I think worked out well.

    2 votes
  8. Comment on Day 9: Mirage Maintenance in ~comp.advent_of_code

    tjf
    Link
    Simple day 9, so I decided to do a little recursion (with memoization) since most solutions here are iterative. My Python: Part 1 #!/usr/bin/env pypy3 def kth_difference(a, k, n, c={}): if (k, n)...

    Simple day 9, so I decided to do a little recursion (with memoization) since most solutions here are iterative. My Python:

    Part 1
    #!/usr/bin/env pypy3
    
    def kth_difference(a, k, n, c={}):
        if (k, n) not in c:
            if k == 1:
                c[(k, n)] = a[n] - a[n - 1]
            else:
                c[(k, n)] = kth_difference(a, k - 1, n, c) - kth_difference(a, k - 1, n - 1, c)
        return c[(k, n)]
    
    def predict(a):
        k = 1
        c = {}
        prediction = a[-1]
        while not all(kth_difference(a, k, i, c) == 0 for i in range(k, len(a))):
            prediction += kth_difference(a, k, len(a) - 1, c)
            k += 1
        return prediction
    
    print(sum(predict(tuple(map(int, line.split()))) for line in open(0)))
    
    Part 2

    Same as part one but reverse the sequence first.

    #!/usr/bin/env pypy3
    
    def kth_difference(a, k, n, c={}):
        if (k, n) not in c:
            if k == 1:
                c[(k, n)] = a[n] - a[n - 1]
            else:
                c[(k, n)] = kth_difference(a, k - 1, n, c) - kth_difference(a, k - 1, n - 1, c)
        return c[(k, n)]
    
    def predict(a):
        k = 1
        c = {}
        prediction = a[-1]
        while not all(kth_difference(a, k, i, c) == 0 for i in range(k, len(a))):
            prediction += kth_difference(a, k, len(a) - 1, c)
            k += 1
        return prediction
    
    print(sum(predict(tuple(map(int, line.split()))[::-1]) for line in open(0)))
    
    1 vote
  9. Comment on Day 5: If You Give A Seed A Fertilizer in ~comp.advent_of_code

    tjf
    Link
    Posting this a few days late because I waited to sit down and think through a non-bruteforce part two strategy. Not super clean but it does the thing (and doesn't take 45 minutes with 4 cores!)....

    Posting this a few days late because I waited to sit down and think through a non-bruteforce part two strategy. Not super clean but it does the thing (and doesn't take 45 minutes with 4 cores!). Here are my Python solutions:

    Part 1
    #!/usr/bin/env pypy3
    
    import math
    import sys
    
    seeds = map(int, sys.stdin.readline().split(':')[1].split())
    mappings = [[((src, src + length), dst - src)
                 for dst, src, length in (map(int, m.split())
                                          for m in mapgroup.split('\n')
                                          if m and m[0].isdigit())]
                for mapgroup in sys.stdin.read().split('\n\n')]
    
    lowest = math.inf
    for s in seeds:
        for mapgroup in mappings:
            for m, shift in mapgroup:
                if m[0] <= s < m[1]:
                    s += shift
                    break
        lowest = min(lowest, s)
    
    print(lowest)
    
    Part 2
    #!/usr/bin/env pypy3
    
    import sys
    
    def compare_intervals(a, b):
        left, right = (a[0], a[0]), (a[1], a[1])
        if (a[0] <= a[1] < b[0]) or (b[1] < a[0] <= a[1]):
            overlap = (0, 0)
            return left, overlap, right
        if a[0] < b[0]:
            left = (a[0], b[0])
        if a[1] > b[1]:
            right = (b[1], a[1])
        overlap = (left[1], right[0])
        return left, overlap, right
    
    def is_nonempty_interval(a):
        return a[0] != a[1]
    
    def shift_interval(a, shift):
        return (a[0] + shift, a[1] + shift)
    
    def update_interval(r, mapgroup):
        unmapped = set([r])
        mapped = set()
        while unmapped:
            rr = unmapped.pop()
            maps_to_self = True
            for m, shift in mapgroup:
                left, overlap, right = compare_intervals(rr, m)
                if is_nonempty_interval(overlap):
                    maps_to_self = False
                    mapped.add(shift_interval(overlap, shift))
                    unmapped.add(left)
                    unmapped.add(right)
            if maps_to_self:
                mapped.add(rr)
        return filter(is_nonempty_interval, mapped)
    
    def flatten(x):
        return [z for y in x for z in y]
    
    starts_and_lengths = map(int, sys.stdin.readline().split(':')[1].split())
    seed_ranges = [(start, start + next(starts_and_lengths))
                   for start in starts_and_lengths]
    mappings = [[((src, src + length), dst - src)
                 for dst, src, length in (map(int, m.split())
                                          for m in mapgroup.split('\n')
                                          if m and m[0].isdigit())]
                for mapgroup in sys.stdin.read().split('\n\n')]
    
    ranges = seed_ranges
    for mapgroup in mappings:
        ranges = flatten(update_interval(r, mapgroup) for r in ranges)
    
    print(min(ranges)[0])
    
    1 vote
  10. Comment on Day 7: Camel Cards in ~comp.advent_of_code

    tjf
    (edited )
    Link Parent
    Very OOP I love it! I chose to write a cmp-style comparison function instead of operator overloading. I see we both thought to use collections.Counter.

    Very OOP I love it! I chose to write a cmp-style comparison function instead of operator overloading. I see we both thought to use collections.Counter.

    2 votes
  11. Comment on Day 7: Camel Cards in ~comp.advent_of_code

    tjf
    Link
    Posting this a day late, but here are my Python solutions. Got to use some stdlib goodies like functools.cmp_to_key and collections.Counter! Part 1 #!/usr/bin/env pypy3 from collections import...

    Posting this a day late, but here are my Python solutions. Got to use some stdlib goodies like functools.cmp_to_key and collections.Counter!

    Part 1
    #!/usr/bin/env pypy3
    
    from collections import Counter
    from functools import cmp_to_key
    
    CARDS = '23456789TJQKA'
    HAND_TYPES = ([1, 1, 1, 1, 1], [1, 1, 1, 2], [1, 2, 2],
                  [1, 1, 3], [2, 3], [1, 4], [5])
    
    def hand_type(h):
        c = Counter(h)
        return HAND_TYPES.index(sorted(c.values()))
    
    def cmp_hands(h1, h2):
        if hand_type(h1) < hand_type(h2):
            return -1
        if hand_type(h1) > hand_type(h2):
            return 1
        for c1, c2 in zip(h1, h2):
            v1 = CARDS.index(c1)
            v2 = CARDS.index(c2)
            if v1 < v2:
                return -1
            if v1 > v2:
                return 1
        return 0
    
    def cmp_handbids(hb1, hb2):
        return cmp_hands(hb1[0], hb2[0])
    
    ranked = sorted(map(str.split, open(0)), key=cmp_to_key(cmp_handbids))
    winnings = sum(rank * int(bid) for rank, (_, bid) in enumerate(ranked, 1))
    print(winnings)
    
    Pretty straightforward tweaks from part one:
    Part 2
    #!/usr/bin/env pypy3
    
    from collections import Counter
    from functools import cmp_to_key
    
    CARDS = 'J23456789TQKA'
    HAND_TYPES = ([1, 1, 1, 1, 1], [1, 1, 1, 2], [1, 2, 2],
                  [1, 1, 3], [2, 3], [1, 4], [5])
    
    def hand_type(h):
        c = Counter(h)
        jcount = c['J']
        del c['J']
        c[max(c, key=c.get) if c else 'J'] += jcount
        return HAND_TYPES.index(sorted(c.values()))
    
    def cmp_hands(h1, h2):
        if hand_type(h1) < hand_type(h2):
            return -1
        if hand_type(h1) > hand_type(h2):
            return 1
        for c1, c2 in zip(h1, h2):
            v1 = CARDS.index(c1)
            v2 = CARDS.index(c2)
            if v1 < v2:
                return -1
            if v1 > v2:
                return 1
        return 0
    
    def cmp_handbids(hb1, hb2):
        return cmp_hands(hb1[0], hb2[0])
    
    ranked = sorted(map(str.split, open(0)), key=cmp_to_key(cmp_handbids))
    winnings = sum(rank * int(bid) for rank, (_, bid) in enumerate(ranked, 1))
    print(winnings)
    
    2 votes
  12. Comment on Day 8: Haunted Wasteland in ~comp.advent_of_code

    tjf
    Link Parent
    Didn't realize that was missing from the stdlib for so long, but thankfully the version of PyPy I'm using has it. Edit: Are the 2000 and 2000000 multipliers you use just arbitrary large numbers?...

    Didn't realize that was missing from the stdlib for so long, but thankfully the version of PyPy I'm using has it.

    Edit: Are the 2000 and 2000000 multipliers you use just arbitrary large numbers? If you look at my solutions there's a neat Python-specific trick for this.

    2 votes
  13. Comment on Day 8: Haunted Wasteland in ~comp.advent_of_code

    tjf
    Link
    Today's problem reminded me of 2022's Day 17, which was a good one! Here are my Python solutions: Part 1 Python's itertools.cycle is really useful here. #!/usr/bin/env pypy3 import itertools...

    Today's problem reminded me of 2022's Day 17, which was a good one! Here are my Python solutions:

    Part 1

    Python's itertools.cycle is really useful here.

    #!/usr/bin/env pypy3
    
    import itertools
    import sys
    
    seq = tuple(int(c == 'R') for c in sys.stdin.readline().strip())
    sys.stdin.readline()
    moves = {}
    for line in sys.stdin:
        k, v = map(str.strip, line.split('='))
        moves[k] = tuple(side.strip('()') for side in v.split(', '))
    
    position = 'AAA'
    steps = 0
    for move in itertools.cycle(seq):
        if position == 'ZZZ':
            break
        position = moves[position][move]
        steps += 1
    
    print(steps)
    

    Let my CPU cook for a few minutes before turning on my brain and thinking. I got to break out my main for loop into a generator function which is always nice.

    Part 2
    #!/usr/bin/env pypy3
    
    import itertools
    import math
    import sys
    
    def nsteps(seq, moves, positions):
        for p in positions:
            steps = 0
            for move in itertools.cycle(seq):
                if p[-1] == 'Z':
                    break
                p = moves[p][move]
                steps += 1
            yield steps
    
    seq = [int(c == 'R') for c in sys.stdin.readline().strip()]
    sys.stdin.readline()
    moves = {}
    for line in sys.stdin:
        k, v = map(str.strip, line.split('='))
        moves[k] = tuple(side.strip('()') for side in v.split(', '))
    
    positions = (p for p in moves if p[-1] == 'A')
    steps = nsteps(seq, moves, positions)
    print(math.lcm(*steps))
    
    3 votes
  14. Comment on Day 6: Wait For It in ~comp.advent_of_code

    tjf
    Link
    Suspiciously easy day, but I'm not complaining. Also I do like days where the input is short enough to enter manually into my source code while first solving. Anyway here are my Python solutions:...

    Suspiciously easy day, but I'm not complaining. Also I do like days where the input is short enough to enter manually into my source code while first solving. Anyway here are my Python solutions:

    Part 1
    #!/usr/bin/env pypy3
    
    product = 1
    for t, d in zip(*(map(int, line.split(':')[1].split()) for line in open(0))):
        product *= sum(x * t - x * x > d for x in range(1, t))
    print(product)
    
    Part 2 No change in computation for part two, only parsing the input:
    #!/usr/bin/env pypy3
    
    t, d = map(int, (line.split(':')[1].replace(' ', '') for line in open(0)))
    print(sum(x * t - x * x > d for x in range(1, t)))
    

    Are we in for something nasty tomorrow?

    2 votes
  15. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    tjf
    Link Parent
    Nice! The most annoying bugs are always in the parsing...

    Nice! The most annoying bugs are always in the parsing...

    1 vote
  16. Comment on Day 3: Gear Ratios in ~comp.advent_of_code

    tjf
    Link Parent
    Very enterprise!

    Very enterprise!

    2 votes
  17. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    tjf
    Link Parent
    Cool comment trick I hadn't seen before. I'm fond of using #if 0 / #endif for this and toggling between 0 and 1.

    Cool comment trick I hadn't seen before. I'm fond of using #if 0 / #endif for this and toggling between 0 and 1.

    2 votes
  18. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    tjf
    Link Parent
    A fellow PyPy user! I always throw it in my shebang for AoC for the free speed boost.

    A fellow PyPy user! I always throw it in my shebang for AoC for the free speed boost.

    3 votes
  19. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    tjf
    Link
    Not too bad today, and I always love an opportunity to use sets (even if they weren't strictly necessary). My Python solutions: Part 1 #!/usr/bin/env pypy3 total = 0 for line in open(0): winning,...

    Not too bad today, and I always love an opportunity to use sets (even if they weren't strictly necessary). My Python solutions:

    Part 1
    #!/usr/bin/env pypy3
    
    total = 0
    for line in open(0):
        winning, mine = (set(map(int, part)) for part in
                         map(str.split, line.strip().split(':')[1].split('|')))
        nmatches = len(winning & mine)
        total += int(2 ** (nmatches - 1))
    
    print(total)
    

    Had to re-read part two's instructions to parse out exactly how copies worked, but straightforward enough once I knew what was being asked.

    Part 2
    #!/usr/bin/env pypy3
    
    from collections import defaultdict
    
    copies = defaultdict(lambda: 1)
    for i, line in enumerate(open(0), start=1):
        copies[i]   # defaultdict hack
        winning, mine = (set(map(int, part)) for part in
                         map(str.split, line.strip().split(':')[1].split('|')))
        nmatches = len(winning & mine)
        for j in range(i + 1, i + nmatches + 1):
            copies[j] += copies[i]
    
    print(sum(copies.values()))
    
    2 votes