December 1, 2024

December Adventure 2024

While I have enjoyed doing (the first week or two of) Advent of Code the last few years, I think I’m going to not do it this year. I currently have my one-month old son asleep on me, and this is simply not an environment that’s conducive to that kind of problem-solving. I think, instead, I will participate in December Adventure which seems like a much gentler way of having some community and bit of a push to do a bit of coding this month. I have a few plans for what I will work on, but I am deliberately not specifying them up front in case I don’t actually decide to work on them this month. This post will record what I got up to on each day, so I’ll be editing it throughout the month.

First

My first Adventure is a foray into a little bit of Python. This ended up being quite a long post, but I expect most days will be a lot shorter than this! I am planning to do Advent of Code 2024 over the course of next year. So the idea is that I will do the 25 puzzles from this December over the course of January–November next year. But what deadlines should I set myself in order to complete the puzzles by the end of November? I could split the 333 days of those 11 months into 13-ish day intervals, and do one puzzle per fortnight, more or less. But, the earlier puzzles are easier and the later ones harder, so I kind of want to front load the deadlines so I get the easier ones done earlier to give myself more time for the later puzzles. So how do I calculate how much time to devote to each puzzle? My first thought was maybe this is a job for the fibonacci sequence? (Spoiler alert, it was not).

Essentially what I want to do is split up the time between January 1 and November 30 into 25 intervals for each of the 25 days of Advent of Code. The approach I took was to write a function that turns a (increasing) function into a collection of intervals. If you put $f(x) = x$ in as your function (or lambda x: x in python) then you get a list of equal sized intervals of about 13 point something days each. But where could you get the right kind of increasing function to give you sensible increasingly long intervals, given the constraint that there are 25 intervals and the start and end dates are given?

My very naive approach was to take a function, look at the value it returns for input of 25, and then divide our 333 days into that many sections. Then, the deadline for the nth puzzle is just January 1 plus $f(n)$ times the size of the section. So for the case of even sized intervals, the 5th puzzle deadline is 5 times 333/25 days.

from datetime import datetime, timedelta

jan1 = datetime(2025, 1, 1)
dec1 = datetime(2025, 12, 1)
DAYS = (dec1 - jan1).days
PUZZLES = 25


def puzzle_deadlines_totals(fn):
    fn_interval = DAYS/fn(PUZZLES)
    for i in range(1, PUZZLES+1):
        finish = jan1 + timedelta(days=fn_interval * fn(i))
        print(f"Puzzle {i}, finish by: {finish}")

Obviously, this is not doing anything to check that you actually pass in an increasing function, and you’d get nonsensical outputs if you did not. This gives you the following output:

>>> puzzle_deadlines_totals(lambda x: x)
Puzzle 1, finish by: 2025-01-14 08:38:24
Puzzle 2, finish by: 2025-01-27 17:16:48
Puzzle 3, finish by: 2025-02-10 01:55:12
Puzzle 4, finish by: 2025-02-23 10:33:36
Puzzle 5, finish by: 2025-03-08 19:12:00
Puzzle 6, finish by: 2025-03-22 03:50:24
Puzzle 7, finish by: 2025-04-04 12:28:48
Puzzle 8, finish by: 2025-04-17 21:07:12
Puzzle 9, finish by: 2025-05-01 05:45:36
Puzzle 10, finish by: 2025-05-14 14:24:00
Puzzle 11, finish by: 2025-05-27 23:02:24
Puzzle 12, finish by: 2025-06-10 07:40:48
Puzzle 13, finish by: 2025-06-23 16:19:12
Puzzle 14, finish by: 2025-07-07 00:57:36
Puzzle 15, finish by: 2025-07-20 09:36:00
Puzzle 16, finish by: 2025-08-02 18:14:24
Puzzle 17, finish by: 2025-08-16 02:52:48
Puzzle 18, finish by: 2025-08-29 11:31:12
Puzzle 19, finish by: 2025-09-11 20:09:36
Puzzle 20, finish by: 2025-09-25 04:48:00
Puzzle 21, finish by: 2025-10-08 13:26:24
Puzzle 22, finish by: 2025-10-21 22:04:48
Puzzle 23, finish by: 2025-11-04 06:43:12
Puzzle 24, finish by: 2025-11-17 15:21:36
Puzzle 25, finish by: 2025-12-01 00:00:00

So now all we need to do is find a function that gives us intervals we like. A function that increases quicker than linear. For some reason, my first thought was “Fibonacci?”.

>>> puzzle_deadlines_totals(fib)
Puzzle 1, finish by: 2025-01-01 00:03:57.720462
Puzzle 2, finish by: 2025-01-01 00:07:55.440923
Puzzle 3, finish by: 2025-01-01 00:11:53.161385
Puzzle 4, finish by: 2025-01-01 00:19:48.602308
Puzzle 5, finish by: 2025-01-01 00:31:41.763693
Puzzle 6, finish by: 2025-01-01 00:51:30.366001
Puzzle 7, finish by: 2025-01-01 01:23:12.129694
Puzzle 8, finish by: 2025-01-01 02:14:42.495696
Puzzle 9, finish by: 2025-01-01 03:37:54.625390
Puzzle 10, finish by: 2025-01-01 05:52:37.121086
Puzzle 11, finish by: 2025-01-01 09:30:31.746476
Puzzle 12, finish by: 2025-01-01 15:23:08.867562
Puzzle 13, finish by: 2025-01-02 00:53:40.614039
Puzzle 14, finish by: 2025-01-02 16:16:49.481601
Puzzle 15, finish by: 2025-01-03 17:10:30.095640
Puzzle 16, finish by: 2025-01-05 09:27:19.577241
Puzzle 17, finish by: 2025-01-08 02:37:49.672881
Puzzle 18, finish by: 2025-01-12 12:05:09.250122
Puzzle 19, finish by: 2025-01-19 14:42:58.923002
Puzzle 20, finish by: 2025-01-31 02:48:08.173124
Puzzle 21, finish by: 2025-02-18 17:31:07.096126
Puzzle 22, finish by: 2025-03-20 20:19:15.269249
Puzzle 23, finish by: 2025-05-08 13:50:22.365375
Puzzle 24, finish by: 2025-07-26 10:09:37.634625
Puzzle 25, finish by: 2025-12-01 00:00:00

This is obviously growing too quickly: fib(25) is over 100,000 so these last couple of values are swamping the earlier smaller values. Doing the first 12 days of puzzles on Jan 1 seems like a bad idea. A bit of trial and error and I found this fairly reasonable list of deadlines:

>>> puzzle_deadlines_totals(lambda x: x**1.5)
Puzzle 1, finish by: 2025-01-03 16:07:40.800000
Puzzle 2, finish by: 2025-01-08 13:22:52.948761
Puzzle 3, finish by: 2025-01-14 21:13:07.905228
Puzzle 4, finish by: 2025-01-22 09:01:26.400000
Puzzle 5, finish by: 2025-01-30 20:58:22.210700
Puzzle 6, finish by: 2025-02-09 06:29:06.969664
Puzzle 7, finish by: 2025-02-19 11:40:01.849914
Puzzle 8, finish by: 2025-03-02 11:03:03.590085
Puzzle 9, finish by: 2025-03-14 03:27:21.600000
Puzzle 10, finish by: 2025-03-26 11:54:19.504486
Puzzle 11, finish by: 2025-04-08 11:34:25.176414
Puzzle 12, finish by: 2025-04-22 01:45:03.241824
Puzzle 13, finish by: 2025-05-06 05:49:05.874630
Puzzle 14, finish by: 2025-05-20 23:13:48.246912
Puzzle 15, finish by: 2025-06-05 05:30:00.505382
Puzzle 16, finish by: 2025-06-21 00:11:31.200000
Puzzle 17, finish by: 2025-07-07 06:54:38.874648
Puzzle 18, finish by: 2025-07-24 01:17:49.616536
Puzzle 19, finish by: 2025-08-10 07:01:19.047274
Puzzle 20, finish by: 2025-08-27 23:46:57.685599
Puzzle 21, finish by: 2025-09-15 03:17:58.910959
Puzzle 22, finish by: 2025-10-03 17:18:48.962200
Puzzle 23, finish by: 2025-10-22 17:34:58.549155
Puzzle 24, finish by: 2025-11-11 03:52:55.757315
Puzzle 25, finish by: 2025-12-01 00:00:00

Instead of picking a function that defines the total for each n, we could instead look at the function that provides the increment between n-1 and n. On this way of looking at it, to recreate the our “equal intervals” example, we’d need a constant function, for example lambda x: 1. We can then translate this into a function for totals by summing the values.

def puzzle_deadlines_intervals(basefn):
    def sumfn(n):
        return sum([basefn(x) for x in range(1, n+1)])
    puzzle_deadlines_totals(sumfn)

What base function might give us nice intervals? Well, we could just use the linear function lambda x: x.

>>> puzzle_deadlines_intervals(lambda x: x)
Puzzle 1, finish by: 2025-01-02 00:39:52.615385
Puzzle 2, finish by: 2025-01-04 01:59:37.846154
Puzzle 3, finish by: 2025-01-07 03:59:15.692308
Puzzle 4, finish by: 2025-01-11 06:38:46.153846
Puzzle 5, finish by: 2025-01-16 09:58:09.230769
Puzzle 6, finish by: 2025-01-22 13:57:24.923077
Puzzle 7, finish by: 2025-01-29 18:36:33.230769
Puzzle 8, finish by: 2025-02-06 23:55:34.153846
Puzzle 9, finish by: 2025-02-16 05:54:27.692308
Puzzle 10, finish by: 2025-02-26 12:33:13.846154
Puzzle 11, finish by: 2025-03-09 19:51:52.615385
Puzzle 12, finish by: 2025-03-22 03:50:24
Puzzle 13, finish by: 2025-04-04 12:28:48
Puzzle 14, finish by: 2025-04-18 21:47:04.615385
Puzzle 15, finish by: 2025-05-04 07:45:13.846154
Puzzle 16, finish by: 2025-05-20 18:23:15.692308
Puzzle 17, finish by: 2025-06-07 05:41:10.153846
Puzzle 18, finish by: 2025-06-25 17:38:57.230769
Puzzle 19, finish by: 2025-07-15 06:16:36.923077
Puzzle 20, finish by: 2025-08-04 19:34:09.230769
Puzzle 21, finish by: 2025-08-26 09:31:34.153846
Puzzle 22, finish by: 2025-09-18 00:08:51.692308
Puzzle 23, finish by: 2025-10-11 15:26:01.846154
Puzzle 24, finish by: 2025-11-05 07:23:04.615385
Puzzle 25, finish by: 2025-12-01 00:00:00

This would be the same as doing puzzle_deadlines_totals(lambda x: x*(x+1)/2) but I didn’t realise this until I’d implemented it. I haven’t actually decided which set of deadlines I will work to yet, but I have time to figure it out. Tidying this up, I’d want to get rid of the times and just print the date (using finish.date()) and then replace dec1 with dec1 - timedelta(second=1) or something so it’s just before midnight on November 30. But I’m pretty happy with what I managed in the time I had available.

Second

Today, rather than actually doing any coding, I spent a while setting up a new repo for a project that I’ll probably be playing with during this month. I’m writing a little command line app for rolling dice. The idea is to implement some (but probably not all) of the functionality of this app. But on top of that, the idea is to also include some functionality for drawing cards from a deck. I haven’t figured out what the syntax for that would be yet.

I always get caught out by this issue where, if you have a local git repo, but then you want to set up a remote on, say, codeberg, it’s kind of awkward to get it right. I am fairly sleep deprived, so I went for the simplest, dumbest approach. I nuked the local git repo, cloned the new codeberg repo into a temporary directory, and then moved the repo into the top level. That’s enough for today.

Third

My original plan today was to make some progress on the rust dicr project I mentioned yesterday. Instead, I installed uiua and got to the stage of solving the first Project Euler puzzle with it. I had this plan at one stage to try solving the Project Euler puzzles in several different languages. I got to the point of solving the first puzzle in Python, Haskell and J before… I don’t remember what happened really? I got distracted learning rust maybe? Anyway, that’s a project I might go back to at some stage. The first puzzle is to find the sum of all the multiples of 3 or 5 less than 1000.

Uiua (pronounced wee-waa) is an array-based programming language like APL or J, but also a stack-based programming language like Forth or Web Assembly. (There might be more obvious examples of stack based programming, but those are the ones I know about). I quite like it so far, although I don’t really like the syntax that requires esoteric symbols aspect. Maybe I just need to get used to it, but I find it hard to read because I don’t find it easy to follow what the symbols mean. Here’s my solution to the first Project Euler problem.

/+×↥ ∩(=0) ⊃(◿ 3) (◿ 5) . ⇡ 1000

(I don’t even know if this will display correctly when I publish, but we’ll see). This is pretty neat, and honestly, I’m much happier with this than I am with my solution in J. I think there must be a better way to do it in J, because my version is horrible. I won’t explain all the uiua syntax here, but I’ll explain roughly how it works, and then you can explore the fairly good docs to follow along more completely.

The strategy is more or less the same as the approach I took in my Haskell solution:

euler :: Int -> Int
euler n = foldl1 (+) $ filter (\x -> (rem x 3 == 0) || (rem x 5 == 0)) [1..n-1]

Reading right to left: get the range of numbers from 1 to n-1 (because Haskell ranges are inclusive, so if we want euler 1000 to give us our result, we need to do n-1); filter the range to the values divisible by 3 or 5; sum those values.

The uiua solution is the same, except for a couple of tweaks. First, I hardcode the value 1000 in the range, because I haven’t got to the part of the tutorial about functions yet. Second, instead of doing the filter step with a lambda, I copy the range so I have two copies on the stack (we’ll come back to this), then I do fork () to do two different operations on the same data (the two modulus operations) and then use both () to do the same operation (checking if ==0) on two different arrays (the two modulus-ed arrays). Uiua doesn’t have explicit AND and OR operators (or a boolean type), so you just use min and max on arrays of 0s and 1s to do the same thing. So now I have a boolean array of the positions in the array where the filter condition is true. Now since I am only interested in the sum of the values, I don’t need to filter the array per se, I just need to only keep the values where the condition is true (multiply them by 1) and drop the others (multiply them by 0). This was why I copied the array at the start. I multiply the boolean array by the original array, and then reduce with addition to get the sum of the filtered values.

This wasn’t at all what I expected to be adventuring with today, but here we are. I like uiua and might play with it some more.

© Seamus Bradley 2021–3

Powered by Hugo & Kiss.