Arjen Wiersma

A blog on Emacs, self-hosting, Clojure and other nerdy things

It is December again and that means it is time for the Advent of Code. Due to my workload and family obligations I will probably not be able to get very far this year, but still I wanted to write a post about it.

This year I am using Java, together with my students. My goal is to write as modern as possible Java, which means using streams and new language constructs where possible.

Day 1 {#day-1}

In day 1 we are parsing 2 lists of numbers, with the lists printed vertically. This means each line has 2 numbers, one for list one and the other for list two. To parse these data structures I used a very nice stream where I map each line onto a String[] using split.

To be sure that the input is valid, the peek method allows you to check if the result is what you intended, and otherwise an exception will terminate everything. From here I map the String[] into a Pair record which holds the 2 numbers. Streaming over the resulting pairs the left and right lists can be extracted quite easily.

I loved this approach, it is very straightforward and does not have a lot of control flow.

@Override
public List<List<Integer>> parseInput(List<String> input) {
    var pairs = input.stream()
        .map(s -> s.split("\\s+"))
        .peek(parts -> {
                if (parts.length != 2)
                    throw new IllegalArgumentException("Invalid input format");
            })
        .map(parts -> new Pair<>(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])))
        .collect(Collectors.toList());

    var left = pairs.stream()
        .map(Pair::left)
        .collect(Collectors.toList());

    var right = pairs.stream()
        .map(Pair::right)
        .collect(Collectors.toList());

    return List.of(left, right);
}

Solving the problem with these lists was quite easy. In part 2 there was a need for a frequency table of a list. I also found a very nice solution to that problem using the groupingBy method from Collectors.

input.get(1).stream()
    .collect(Collectors.groupingBy(n -> n, Collectors.counting()));

Day 2 {#day-2}

I really liked day 2, the first part was quite straightforward. You have to identify increment or decrement only lists and apply some conditions to them.

Part 2 was much more interesting, here you have to account for fault tolerance. In the Python implementations that were posted the common solution is to concatenate 2 parts of the array and then rerun the validation logic.

Using streams we can something very similar. First we use an IntStream to iterate over every int[] (report). Then for every int in that report, we construct a new array by filtering out the index of the current item. After that it is a simple case of determining increment or decrement and applying the conditional logic.

Suppose you have a list of [1,2,4,7], while iterating it will first hit index 0, the filter will prevent that entry from continuing. Next 1 through 3 will continue and as a result of toArray() a new array will be constructed with only those items.

x == 0
     |    map ------.
     v  -------     v
    [1, 2, 4, 7]    [2,4,7]
input.stream()
    // Loop over the list
    .filter(in -> IntStream.range(0, in.length) // take a report
            // for every entry in that int[]
            .anyMatch(x -> {
                    // create a new list, excluding the one we are on now
                    int[] c = IntStream.range(0, in.length)
                        .filter(i -> i != x)
                        .map(i -> in[i])
                        .toArray();

                    boolean allInc = IntStream.range(0, c.length - 1)
                        .allMatch(i -> c[i] <= c[i + 1]);

                    boolean allDec = IntStream.range(0, c.length - 1)
                        .allMatch(i -> c[i] >= c[i + 1]);

                    boolean good = IntStream.range(0, c.length - 1)
                        .allMatch(i -> Math.abs(c[i] - c[i + 1]) >= 1 &&
                                  Math.abs(c[i] - c[i + 1]) <= 3);

                    // matching the condition
                    return (allInc || allDec) && good;
                })
            )
    .count();

My first solution was nothing like this, but after refining it I am very happy with how clean it came out.

Day 3 {#day-3}

This was the traditional easy puzzle after a more complicated one. Basically simple parsing for which I used regular expressions. Nothing special, on to day 4.

Day 4 {#day-4}

For day 4 I solved the first part with an over engineered path finding solution, which turned out to be quite the overkill, but extremely fun to program.

I really like the pattern in use, below is some of the code of it. First you create a Deque that holds the work, then you load it up with the initial starting points. In the case of the puzzle these are the location of the X characters.

From there you just loop over the work, taking a partial solution and seeing if any cells around it will lead to another partial solution, so from XM to XMA and on the next iteration to XMAS. The dx is a collection of Coord that indicate valid movements across the board.

Deque<Path> work = new ArrayDeque<>();

// Load initial points
for (int y = 0; y < input.length; y++) {
    for (int x = 0; x < input[y].length; x++) {
        if (input[y][x] == 'X') {
            work.add( new Path(List.of(new Coord(x,y)), "X", null) );
        }
    }
}

// Process each outstanding point..
while (!work.isEmpty()) {
    var path = work.pop();
    for (Coord d : dx) {
        if (path.dir() != null && path.dir != d) {
            continue;
        }
        var newCoord = lastStep.add(d);
        // Ensure this is a valid point on the grid
        if (newCoord.x() >= 0 && newCoord.x() < input[0].length &&
            newCoord.y() >= 0 && newCoord.y() < input.length) {
            // ... create new paths and string based on location
            // Check if we have an end case, else add it to the work
            if (target.equals(xmas)) {
                matches.add(newPath);
            } else if (target.startsWith(xmas)) {
                work.add(newPath);
            }

        }
    }
}

A more straightforward approach, which was actually needed for part 2, is to just try to solve it in one step. First you iterate over both y and x coordinates looking for an X, just as above. When you find one, iterate over [-1, 0, 1] on both the x and y axis-es, using dy and dx for the direction. If both direction are 0, we continue as it would give the current position. The beauty of this approach is that you can move outward in steps, x + 3 * dx will give you a value 3 cells in the give direction. From there it is a simple matter of checking if we are in bounds and if the letters spell MAS.

for (int y = 0; y < input.length; y++) {
    for (int x = 0; x < input[y].length; x++) {
        if (input[y][x] != 'X') continue;
        for (int dy = -1; dy <= 1; dy++) {
            for (int dx = -1; dx <= 1; dx++) {
                if (dy == dx && dx == 0) continue;
                if (!(0 <= y + 3 * dy && y + 3 * dy < input.length &&
                      0 <= x + 3 * dx && x + 3 * dx < input[y].length)) continue;

                if (input[y+1*dy][x+1*dx] == 'M' &&
                    input[y+2*dy][x+2*dx] == 'A' &&
                    input[y+3*dy][x+3*dx] == 'S') {
                    matches++;
                }
            }
        }
    }
}

For part 2 a similar approach can be used, however the order is not important. So I chose to create a List and then check against a target list with the containsAll method, it does not care about order.

var x1 = List.of(input[y-1][x-1], input[y][x], input[y+1][x+1]);
var x2 = List.of(input[y-1][x+1], input[y][x], input[y+1][x-1]);
if (x1.containsAll( target ) && x2.containsAll( target )){
    matches++;
}

Another reminder to not over engineer at the start.

Day 5 {#day-5}

Another fun puzzle, when I initially read it my mind jumped to graphs. There is a 2 part input, the first part being a list of rules, numbers that are only valid when they are placed in front of other numbers.

The second part of the input is a list of report structures. The first quest was to validate the reports and find only the valid ones.

My first attempt, in part 1, was to take the rules for a number (a List<Integer>) and see if there is an anyMatch of the sublist before it using order::contains. Basically if the pages is 75,97,47,61,53 and the rule 97|75 (97 should be before 75), the the loop will iterate over the pages, and check to see if [75] is in the list of rules for 97.

boolean isValid(List<Integer> pages, Instructions input) {
    var valid = true;
    for (int i = 0; i < pages.size(); i++) {
        var order = input.order().get(pages.get(i));
        if (order != null) {
            var hasAny = pages.subList(0,i+1).stream().anyMatch(order::contains);

            if (hasAny) {
                valid = false;
            }
        }
    }
    return valid;
}

Part 2 had us fixing the broken pages. After some initial magic with arrays I figured out it is a basic sorting problem. In Java you can use Comparator implementations to create custom sorting rules, as long as it responds with -1,0,1 for to the left, the same, to the right. So the lambda Comparator takes a left hand side and right hand side value, retrieves the rules for the left hand side (if null it is equal 0) and checks to see if the right hand side is in the ruleset (-1). If all checks fail, the value should go to the right hand side.

var answer = 0L;
for (var pages : input.pages()) {
    var valid = isValid(pages, input);
    var work = new ArrayList<>(pages); // pages is immutable
    if (!valid) {
        Collections.sort(work, (lhs, rhs) -> {
                var order = input.order().get(lhs);
                if (order == null) return 0;
                if (order.contains(rhs)) return -1;
                return 1;
            });
        answer += work.get(work.size()/2);
    }
}

A surprisingly easy solution to a messy problem when you want to implement it yourself.

Day 6 {#day-6}

Traditionally the Friday puzzles seem to be somewhat more challenging, this Friday is no exception. We are given a challenge similar to sliding puzzle games.

Instead of sliding over ice we are to map the movements of a guard to ensure we can move safely through the area. For part 1 there was nothing too exciting, just move the guard over the floor and track the places visited. Depending on your loop you might accidentally avoid an edgecase that will show up in part 2.

Lets take a look at the loop:

while (inBounds) {
    visited.add(start);
    var next = start.add(delta.get(sign));
    if (!next.inBound(0, input[0].length, 0, input.length)) {
        inBounds = false;
        continue;
    }
    if (input[next.y()][next.x()] == '#') {
        sign = turns.get(sign);
        continue;
    }
    start = next;
}

While we are *in bounds* we keep moving, adding each step into the visited list. We then get the next position by retrieving the delta (a lookup table of coordinates such as -1,0, which indicate that the guard will move -1 on the x-axis and 0 on the y-axis). If we are out of bounds, flip the switch and break out of the while loop, if the next position is an obstacle, #, we set the sign to the 90 degree turned version (another lookup table) and rerun the loop. If, for some reason, you continue checking and validating at this point you might miss the edge-case that turning can result in facing another wall. When all the conditions are checked, simply reset the start variable to the next coordinates and move on.

Part 2 becomes much more interesting; we are to find infinite loops by placing exactly 1 extra obstacle. Intuitively you will remark that the obstacle can only be placed on one of the cells that were visited in part 1. This already eliminates part of the board. From here you can loop over the list of coordinates, place an obstacle and let the guard run its route. When you visit a coordinate twice in *the same direction* you know you are in a loop.

I looked for a “smart” solution, but the brute force is done in less then 2 seconds. So I will leave it at this, but somehow feel there might be more optimizations possible.

Day 7 {#day-7}

The end of week 1, and easier then the Friday puzzle. We are given a list of numbers per line that we need to either add or multiply to get to a target number. I chose to use some recursion to solve this problem. Each iteration of the recursion will reduce the array of numbers using one of the operations.

In the end the list of numbers will be reduced to either the target number, or something else. So the base case checks to see if it was successful.

If the base case is not hit, the first recursion is to add the numbers. A trick here is to use a LongStream to range over 1 to the end, mapping the numbers. If number 1 is mapped, we add the number at position 0 to reduce the array.

The second case applies the multiplication in the same way.

The third case (part 2) is to concatenate the numbers, this is easil done through number + "" + number in java, coercing the numbers into a String and then using Long.valueOf() to read the value again.

boolean isValid(long target, long[] numbers, boolean third) {
    if (numbers.length == 1) return target == numbers[0];
    if (isValid(target, LongStream.range(1, numbers.length)
                .map(i -> {
                        if (i == 1) return numbers[0] + numbers[1];
                        return numbers[(int)i];
                    })
                .toArray(), third)) return true;
    if (isValid(target, LongStream.range(1, numbers.length)
                .map(i -> {
                        if (i == 1) return numbers[0] * numbers[1];
                        return numbers[(int)i];
                    })
                .toArray(), third)) return true;
    if (third && isValid(target, LongStream.range(1, numbers.length)
                          .map(i -> {
                                  if (i == 1) return Long.valueOf(numbers[0] + "" + numbers[1]);
                                  return numbers[(int)i];
                              })
                          .toArray(), third)) return true;
    return false;
}

One last trick is to use the Stream feature to filter the list, mapping each object to a long value and summing.

@Override public Long solver1(List<Calibration> input) {
    return input.stream().filter(i -> isValid(i.target, i.numbers, false)).mapToLong(cal -> cal.target).sum();
}

Day 8 {#day-8}

Day 8 has us back in history staring at antennas. The description was quite cryptic, but reading it carefully you learn that the necessary step is to find the difference between a pair of coordinates and then extrapolate the path inside the bounds of the grid.

To read the grid into a structure I used a simple nested loop, adding a new list to the map if it is absent, then adding the new coordinate for the antenna.

for (int r = 0; r < gridH; r++) {
    for (int c = 0; c < gridW; c++) {
        char ch = input.get(r).charAt(c);
        if (ch != '.') {
            antennas.computeIfAbsent(ch, k -> new ArrayList<>()).add(new Coord(c, r));
        }
    }
}

We then have to find the antinode for the point which, for the pair, is just a single difference step from the antenna. Interestingly we need to count the *unique* antinodes. Whenever you get such a requirement, always think about using a Set for storage.

Getting the pairs is straightforward, and we have done it earlier in the series already. The first loop starts at 0 and ends the element before the end, size - 1. The inner loop starts at current pos + 1 and ends at the size of the list.

for (int p = 0; p < coords.size() - 1; p++) {
    for (int n = p + 1; n < coords.size(); n++) {

Then just compute the difference and add the antinode when it is in bounds.

var antinode1 = cur.add(cur.diff(next));
var antinode2 = next.add(next.diff(cur));

In part 2 the path needs to be extrapolated until it goes out of bounds. This can easily be wrapped in its own method.

void addAntinodesInDirection(Set<Coord> antinodes, Coord start, Coord diff) {
    var current = start;
    while (true) {
        var next = current.add(diff);
        if (!next.inBound(0, gridW, 0, gridH)) {
            break;
        }
        antinodes.add(next);
        current = next;
    }
}

A pretty straightforward problem to solve, on to tomorrow!

Day 9 {#day-9}

For me this was a hard day. We are given a list of numbers that we use to fragment files on a disk. The first part of the puzzle was quite straightforward, create a list that holds the file ids and spaces and just follow the rules.

for (int i = 0; i < input.length(); i++) {
    for (int j = 0; j < input.charAt(i)-'0'; j++) {
        if (i%2 == 0) {
            disk.add(id);
        } else {
            disk.add(null);
        }
    }
    if (i%2 == 0) {
        id++;
    }
}

From there just create 2 pointers, one on the left and one on the right. The left tracks the empty space and the right tracks the file ids that we want to put in the empty space. The important thing in Java is to make the implementation of the list a LinkedList. This allows for little-overhead reshuffling of the list.

var l = 0;
var r = disk.size() - 1;
while (l < r) {
    if (disk.get(l) != null) {
        l++;
        continue;
    }

    if (disk.get(r) == null) {
        r--;
        continue;
    }

    disk.set(l, disk.get(r));
    disk.set(r, null);
    l++;
    r--;
}

Part 2 became much harder, we are not to find space for the blocks of files instead of fragments. I first tried the same approach, but it took forever. I then saw the error of my ways and decided to use a lookup table for the empty spaces. This table maps the empty spaces of size N, in the below example 1 and 2, to a list of start/end coordinates.

1 = [1,1] [2,2]
2 = [3,4] [6,7]

The list of coordinates needs to remain sorted, so I used a PriorityQueue for it. Then it is just a matter of determining the size of the file under the r pointer by looping over it until we hit another id, and then looking up the most left candidate of the empty spaces.

// Gets all candidates that will fit the file
for (int i = bs; i < 10; i++) {
    var earliest = free[i].peek();
    if (earliest != null && earliest < r) {
        candidates.add(new Candidate(i, earliest));
    }
}

if (candidates.isEmpty()) {
    return null;
}

// Sort based on the index (most left first)
candidates.sort((lhs, rhs) -> {
    return lhs.idx() - rhs.idx();
});

var can = candidates.getFirst();
free[can.size()].remove();

The final solution runs in a matter of milliseconds, so I am quite happy with that.

Day 10 {#day-10}

Finally a depth first / breath first path seeker! We need to identify a trail that leads to a summit, or rather all trails that lead to the summit. Part 1 wants to know the score (how many summits can a path reach) and part 2 its rating (how many trails are there that reach a summit). This is pretty straightforward in the sense that you create a Queue and put the start of the trail in, then for each direction you construct a more complete path.

As always it is important to check for bounds and if the path is incremental (business rule). If the new path is actually at the summit, add it to the finished paths. If it is not, try to complete it.

Trail solve(char[][] grid, Coord zero) {
var q = new ArrayDeque<List<Coord>>();
q.add(List.of(zero));

List<List<Coord>> paths = new ArrayList<>();
while (!q.isEmpty()) {
    var current = q.removeFirst();

    for (Coord d : zero.directNeighbors()) {
        var last = current.getLast();
        var nc = last.add(d);

        if (!nc.inBound(0,grid[0].length, 0, grid.length)) continue;
        if (grid[nc.y()][nc.x()] != grid[last.y()][last.x()] + 1) continue;

        var newPath = new ArrayList<>(current);
        newPath.add(nc);

        if (grid[nc.y()][nc.x()] == '9') {
            paths.add(newPath);
        } else {
            q.addLast(newPath);
        }

    }
}
var score = paths.stream().map(l->l.getLast()).collect(Collectors.toSet()).size();
var rating = paths.size();

return new Trail(score, rating);

Day 11 – one to remember {#day-11-one-to-remember}

For the last couple of days the discussion forums have been full with memes about brute forcing the answer. Up to now you could really do so. I have one colleague who wrote a nice brute force for Day 6 that took several minutes, but it did work. Personally I am not a fan of the brute forcing approach, I like to make it more elegant when possible.

Today is this years first Lanternfish type of problem, one where the problem space becomes so large that your computer is not able to brute force it due to memory constraints. It calls for a more elegant solution.

Part 1 and part 2 are basically the same, the difference is the amount of iterations for the problem. In this case we have some rules in which rocks change and split up. We are tasked to find the amount of rocks after 25 and 75 iterations. The first is do-able with a brute force approach, the second is not.

The rules are straightforward, but the solution to the problem space might not be. The trick is to use something called memoization.

In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls to pure functions and returning the cached result when the same inputs occur again.

So, basically we store results of method calls. Lets first look at my solution. It is a recursive function that takes a number and the number of iterations to apply to it.

if (iterations == 0) return 1;
var cache = new Cache(number,iterations);
if (memo.containsKey(cache)) {
    return memo.get(cache);
}
if (number == 0) {
    long l = ways(1, iterations - 1, memo);
    memo.put(cache, l);
    return l;
}

String s = "" + number;
int l = s.length();
if (l % 2 == 0) {
    long c = ways(Long.parseLong(s.substring(0,l/2)), iterations-1, memo) + ways(Long.parseLong(s.substring(l/2,l)), iterations-1, memo);
    memo.put(cache, c);
    return c;
}
long c =  ways(number * 2024, iterations-1, memo);
memo.put(cache, c);
return c;

For this call the result is stored in a memo, a simple HashMap that stores the method arguments in a CacheKey and then stores the count, the result of the recursive call. What happens here is that there might be many different calls to this method, such as (9,23). In this case the number 9 has 23 iterations to go. By computing the result once and then storing the computed value we save the time of doing the same calculations many other times.

This was a really fun and relatively quick challenge, greatly enjoyed it!

Day 12 – Garden Groups {#day-12-garden-groups}

This day had me stumped for quite a time. The puzzle starts off with a simple question; group the garden (grid) into areas that are the same and count the fences required. A simple flood fill type of solution works very well here.

Whenever a neighbor is not the same type (or we are the edge), a fence is required. If it is the same type, add it to the queue for further processing.

for (int r = 0; r < input.length; r++) {
    for (int c = 0; c < input[r].length; c++) {
        var start = new Coord(c, r);

        if (!seen.add(start)) {
            continue;
        }

        var queue = new ArrayDeque<Coord>();
        queue.add(start);

        var cells = new HashSet<Coord>();
        int perimeter = 0;
        char fenceType = input[r][c];

        while (!queue.isEmpty()) {
            Coord cell = queue.poll();
            cells.add(cell);

            for (Coord neighbor : Coord.directNeighbors()) {
                Coord next = cell.add(neighbor);

                if (!next.inBound(0, input[0].length, 0, input.length) ||
                    input[next.y()][next.x()] != fenceType) {
                    perimeter++;
                } else if (seen.add(next)) {
                    queue.add(next);
                }
            }
        }

        fences.add(new Fence(perimeter, cells));
    }
}

The next part had me going for a little bit. Instead of the area or fences the elves need the sides counted. This turns out to be quite a thing until it becomes clear that counting corners also works.

Lets say the below map is our grid. When looking at the A in cell 0,2 it is possible to check if it is a corner by checking that the cell above it and the cell to the right are not the same. The same goes for the cell below and the cell to the right.

{{< figure src=“/ox-hugo/corners.png” >}}

A neat trick to find the sides to an area. The code turned out to be relatively easy:

for (var cell : fence.cells()) {
    char curr = input[cell.y()][cell.x()];

    for (int ud : new int[]{-1, 1}) {
        for (int lr : new int[]{-1, 1}) {
            int ny = cell.y() + ud;
            int nx = cell.x() + lr;

            boolean outOfBoundsY = ny < 0 || ny >= input.length;
            boolean outOfBoundsX = nx < 0 || nx >= input[0].length;

            if ((outOfBoundsY || input[ny][cell.x()] != curr) &&
                (outOfBoundsX || input[cell.y()][nx] != curr)) {
                corners++;
            } else if (!outOfBoundsY && !outOfBoundsX &&
                        input[ny][cell.x()] == curr &&
                        input[cell.y()][nx] == curr &&
                        input[ny][nx] != curr) {
                corners++;

        }
    }
}

On to Friday the 13th!

Day 13 – Claw Contraption {#day-13-claw-contraption}

Today was quite something. We have claw machines that have 2 buttons. The buttons move the arm a set space on the x and y coordinates. Both buttons have a different value for the cost and we are tasked to find the cheapest path to a prize on some distant x and y location.

My initial solution was naive and used dynamic programming to solve it. It did not adhere to the rule every problem has a solution that completes in at most 15 seconds on ten-year-old hardware. So eventually I found a solution (thanks Hyperneutrino!) that uses math to solve this problem.

Basically we are trying to find the amount of x and y movements both the A button (indicated by S) and the B button (indicated by T) have to make in order to get to the prize x and y values.

axS + bxT = px
ayS + byT = py

We can make these equations the same by multiplying with by and bx. We do this so we can remove the B button from the equation and solve A.

axbyS + bxbyT = pxby
aybxS + bybxT = pybx

Now bxbyT == bxbyT

This can then be rewritten in a single equation, from which we can isolate A.

axbyS - aybxS = pxby - pybx

(axby-aybx)S =  pxby - pybx

Now we can divide by axby - aybx and get our solution for A. We have to ensure the input is never 0 to prevent division by zero. In the code we can check this by checking that ax * by == ay * bx never occurs.

S = pxby - pybx
    -----------
    axby - aybx

As we now have the A button value, we can also solve the B button.

axS + bxT = px
bxT = px - axS
T = px-axS
    ------
    bx

In code the solution looks very simple. Notice the ca%1==0 && cb%1==0 check to ensure we do not allow for fractional steps.

private long solve(double ax, double ay, double bx, double by, long px, long py) {
    long answer = 0L;
    double ca = (px * by - py * bx) / (ax * by - ay * bx);
    double cb = (px - ax * ca) / bx;
    if (ca % 1 == 0 && cb % 1 == 0) {
        answer += (long) (ca*3) + cb;
    }
    return answer;
}

Day 14 – Restroom redoubt {#day-14-restroom-redoubt}

Today we are back at Easter Bunny HQ, looking for a restroom. We are given a collection of robots, their current position on a grid and their velocity. The question becomes, where are they after 100 iterations (seconds)? An interesting part of this question is that the robots wrap around the grid.

This mechanic allows for very easy calculation of the final coordinates, as (x + vx * 100) % width will give us the final position, instead of having to go through all the calculations.

Interestingly, Java does not really like negative numbers in the modulo operator. For example, -102 % 11 yields -3 while it should yield 8 for it to be useful in our case. So, when the number is negative, just add the width to it.

for (var robot : robots) {
    int newX = (robot.start.x() + robot.vel.x() * 100) % width;
    if (newX < 0) newX += width;

    int newY = (robot.start.y() + robot.vel.y() * 100) % height;
    if (newY < 0) newY += height;

    positions.merge(new Coord(newX, newY), 1, Integer::sum);
}

Part 2 was a horrible puzzle. There was no clue what to do in order to get this:

{{< figure src=“/ox-hugo/day14.png” >}}

I finally solved it by looking at the field when all robots are on a unique position. I also have seen solution where the minimum safety value is found. The problem description was:

During the bathroom break, someone notices that these robots seem awfully similar to ones built and used at the North Pole. If they're the same type of robots, they should have a hard-coded Easter egg: very rarely, most of the robots should arrange themselves into a picture of a Christmas tree.

What is the fewest number of seconds that must elapse for the robots to display the Easter egg?

Maybe if it had said “very rarely, but when all the robots arrange themselves”, but then again, how are you supposed to know that it means non-overlapping.

Love the Christmas tree though.

Day 15 – Warehouse Woes {#day-15-warehouse-woes}

Yay, the Lanternfish have made an appearance! Sadly this puzzle had me quite stumped for a while. I had to rewrite my solution 2 times in order to get it right.

First, let me explain. We are still not finding the historian (whom I think is just Eric in a costume). Instead we are on a side-quest helping our fishy friends with robots in their warehouses. The first puzzle is straightforward; move the player around, moving objects that you run into.

When we have that sorted we are sent to a second warehouse. This time the boxes that we move are twice as large, but the robot is still the same size. This means we get into situations as the following example:

######
#    #
# [] #
# @  #
######

Here the player can move up, but we are only hitting one side of the box. We have to take into account that we need to move the other part along as well. Even more complicated, we can get into the following situation.

######
#[]  #
#[][]#
# [] #
# @  #
######

In this situation we can not move, even though the 2nd box on the middle layer might think we can, as it has a white-space above it.

I worked on arrays for a while, but eventually went for a more “Java” solution and create the factory as objects. Using the objects it is possible to attack the problem more in a “game engine” type of way, by making each object react to the interaction.

My code is horrible not-optimized, I apologize for that right away, but it gets the job done :D

Firstly, I split the process out into two segments, first to see if we can move, then to actually move. Side note: I should really use a lookup table for the coordinate to find the objects instead of looping over it.

The objects are simple POJOs, all extending the aptly named Thing.

class Player extends Thing {
    public Player(Coord start, Coord end) {
        super(start, end);
    }
}

class Wall extends Thing {
    public Wall(Coord start, Coord end) {
        super(start, end);
        this.canMove = false;
    }
}

class Box extends Thing {
    public Box(Coord start, Coord end) {
        super(start, end);
    }
}

Thing has all the logic, with canMove() and move basically doing the same thing, except for move actually moving the objects into a new coordinate. Only if we have a space as neighbor, or if *all* of the neighbors can move, only then do we move the current object.

boolean move(List<Thing> factory, Coord direction) {
    if (!canMove)
        return false;

    Coord nsp = start.add(direction);
    Coord nep = end.add(direction);

    Set<Thing> hits = new HashSet<>();
    for (var t : factory) {
        if (t == this)
            continue;

        if (t.collidesWith(nsp))
            hits.add(t);

        if (t.collidesWith(nep))
            hits.add(t);
    }

    if (hits.size() == 0) { // space, so we can move ourself
        this.start = this.start.add(direction);
        this.end = this.end.add(direction);
        return true;
    }

    if (hits.stream().allMatch(t -> t.move(factory, direction))) {
        this.start = this.start.add(direction);
        this.end = this.end.add(direction);
        return true;
    }

    return false;
}

Collision is checked against both the left and right hand side of the object. Meaning that we can easily handle boxes of size 2.

boolean collidesWith(Coord c) {
    if ((start.x() == c.x() && start.y() == c.y()) || (end.x() == c.x() && end.y() == c.y())) {
        return true;
    }
    return false;
}

Much more work then I thought it would be, but a nice solution anyways.

More to come {#more-to-come}

[This article will be update with more days]

#writing

My old laptop, now almost 6 years old, has seen it all. From conferences, to lectures, traveling to distant places and to the library. I did a lot of work on it during the writing of my thesis, and it is a victim to countless hours of compiler time.

Sadly the battery started to die. It got to the point that you can only use it shortly for heavier loads. Luckily, unlike certain hardware (looking at you Apple), it is easy to fix. All it needs is a new battery. So I found that ifixit had the right parts and a very useful kit with all the right tools to do the job.

It only took 10 minutes to do, and now my trusty old laptop is able to last 7 to 8 hours again! Gotta love the ability to repair things. I hope it lasts another 5 to 6 years!

Today the academic world is remembering Bastiaan Heeren, who passed away last week.

I spent the better time of a year working on my thesis, and before that I enjoyed lectures given by Bastiaan. He was a person with a great love for teaching, especially when you can get into the nitty gritty details of software quality and the benefits of functional programming.

I look back fondly on my time with Bastiaan. He was an open, warm, critical and encouraging human being. He had a great love for his family and work.

Right after I graduated his illness progressed and as a result he was unable to join the graduation ceremony last September. We exchanged gratitude through email and in a blink of an eye, it is over. He was only 46.

You will be missed.

I recently came across Traefik. It is a reverse proxy built specifically for services in the cloud. I was searching for a convenient (up-to-date) way to expose my project using a reverse proxy within docker-compose. I used to use nginx for this, but it then requires a generator and an lets encrypt listener (so 3 containers). Traefik only requires a single container and allows you to label your docker containers to apply rules to them.

The below configuration creates a traefik instance, sets it up to host port 80 and 443 for web, and 8080 for its dashboard (protect that port in your firewall). It also sets up letsencrypt certificates and automatic redirection from port 80 to 443.

version: '3'

services:
  reverse-proxy:
    image: traefik:v3.1
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro # Traefik config file
      - traefik-certs:/certs # Docker volume to store the acme file for the Certifactes

  app:
    image: your/image
    ports:
      - 8081:8080
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app-http.rule=Host(`example.com`) || Host(`www.example.com`)"
      - "traefik.http.routers.app-http.entrypoints=web"
      - "traefik.http.routers.app-http.middlewares=redirect-to-https"
      - "traefik.http.routers.app-https.rule=Host(`example.com`) || Host(`www.example.com`)"
      - "traefik.http.routers.app-https.entrypoints=websecure"
      - "traefik.http.routers.app-https.tls=true"
      - "traefik.http.routers.app-https.tls.certresolver=letencrypt"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true"
volumes:
  traefik-certs:
    name: traefik-certs

The mentioned config file is reproduced below:

api:
  dashboard: true # Optional can be disabled
  insecure: true # Optional can be disabled
  debug: false # Optional can be Enabled if needed for troubleshooting
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
serversTransport:
  insecureSkipVerify: true
providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: proxy # Optional; Only use the "proxy" Docker network, even if containers are on multiple networks.
certificatesResolvers:
  letencrypt:
    acme:
      email: contact@example.com
      storage: /certs/acme.json
      #caServer: https://acme-v02.api.letsencrypt.org/directory # prod (default)
      caServer: https://acme-staging-v02.api.letsencrypt.org/directory # staging
      httpChallenge:
        entryPoint: web

#development

This is my first article in a series called Rock Solid Software. In it I explore different dimensions of software that does not simply break. You can write good software in any programming language, although some are more suited to a disciplined practice then others, Clojure is definitely in the relaxed space of discipline here.

Today I am exploring the use of Selmer templates in Clojure. If you have explored Biff at all you will know that all the UI logic works by sending Hiccup through a handler, which will turn into HTML through rum (specifically the wrap-render-rum middleware). If you provide a vector as a result for an endpoint, it will be converted to HTML.

;; You provide this...
[:h1 "test"]
;; => [:h1 "test"]

;; It will then be converted to HTML
(rum/render-static-markup
  [:h1 "test"])
;; => "<h1>test</h1>"

This is absolutely great for rapid prototyping, however it becomes quite tedious if you want to test it. The idea of testing a function is to provide it some inputs and to validate if the outputs match the expectation. Verifying if HTML matches an expectation, or a vector of hiccup for that matter, is quite difficult.

To increase testability I added selmer to my project. This separates presentation from data by having selmer render templates with a map of data. Selmer is based on Django templates, which means that it has a rich set of features, such as extending base templates, defining blocks and providing control structures such as if and for loops. A very simple template looks like this:

{% extends "_layout.html" %}

{% block content %}
<article>
Hallo <b>{{name}}</b> from simple template
</article>
{% endblock %}

As the template extends _layout.html, lets take a look at that as well. I have stripped it down to the bare minimum here, but you might expect scripts, css, nav bars and many other things in the base template. The important thing here is that the block has the name of content, and our snippet above also has a block called content, the above article will be put inside the main below.

<!doctype html>
<html class="no-js" lang="">
  <head>
    <title>{{title}}</title>
  </head>
  <body>
    <main id="main">
      {% block content %}
      {% endblock %}
    </main>
  </body>
</html>

All that is left is to provide a middleware that will handle the selmer return type from an endpoint. In this case a map with a :template and :content key. If both keys are inside the response, the given template will be rendered using the content map.

(defn wrap-render-selmer [handler]
  (fn [ctx]
    (let [response (handler ctx)]
      (if (and (map? response) (every? response [:template :content]))
        (let [res (selmer/render-file (:template response) (:content response))]
          {:status 200
           :headers {"content-type" "text/html"}
           :body res})
        response))))

My new authentication function has become quite simple, provide a login page using the auth/login.html template. Of course it requires a whole bunch of different attributes in order to render the whole page, but another wrapper adds all the required metadata known to the application already, such as css and script files, application settings and even theme information. All the endpoint has to take into account is its own required information.

(defn login [{:keys [params] :as ctx}]
  (let [err (:error params)]
    (ui/page ctx {:template "auth/login.html"
                  :content (merge {} (when err {:errors [err]}))})))

This is all great and all, but it has nothing to do with testability, right? Well, a map is easier tested then an unstructured vector, right? In other languages, such as Rust, you can get compile time validation of templates, which is great! Sadly selmer does not have that, however we can just simply render a template file and see if there are any missing values.

The below snippet takes the “missing” value and replaces it with a placeholder. So, given a template and an endpoint function we can easily check if all required entries are provided in the map. The below function renders the template, provides a csrf token which is not available inside testing, and verifies that the template does not have any missing values.

(defn missing-value-fn [tag _context-map]
  (str "<Missing value: " (or (:tag-value tag) (:tag-name tag)) ">"))

(selmer.util/set-missing-value-formatter! missing-value-fn)

;; Ensure all the page's required fields are present.
(deftest selmer-validation
  (let [s (sut/page {} {:template "_layout.html" :content {}})
        ;; CSRF is not set during testing...
        res (selmer.parser/render-file (:template s) (assoc (:content s) :csrf "csrf"))]
    (is (not (str/blank? res)))
    (is (not (str/includes? res "<Missing value:")))))

Another step into building rock solid software.

#clojure #development

{{< figure src=“/ox-hugo/20240722203307screenshot.png” >}}

Je hebt tijd gereserveerd om te gaan studeren en je volgt een ritueel om goed in jouw “Deep Work” modus te komen, dat studeren gaat echt goed lukken! Tijdens de colleges en in de lesmaterialen vind je allerlei links naar papers, websites, YouTube videos en haal je veel informatie uit boeken. Al deze bronnen zijn zeer waardevol en bij de eindopdracht zul je veel van deze bronnen weer moeten gebruiken om argumenten te onderbouwen of juist iets te ontkrachten. Hoe ga je dan om met die bronnen zonder dat je gek wordt van allerlei documentjes?

Een natuurlijk reactie is om hiervan allemaal bladwijzers te maken in jouw browser als het op het web is, en voor andere bronnen juist weer in een bestandje bij te houden met wat je hebt gelezen. Dit gaat een tijdje goed, en dan begin je aan jouw opdracht.

Eerst kom je erachter dat je een of ander speciaal formaat moet gebruiken om naar bronnen te verwijzen, APA of IEEE, en dat je dus een lijst bijhoudt van de artikelen die je daadwerkelijk in de opdracht gebruikt. Er mogen geen bronnen in de lijst staan die je niet in de tekst gebruikt en in de tekst mag je niet verwijzen naar iets wat niet in de bronnenlijst staaat. Dan wordt het opeens lastig. Nu blijkt een programma zoals Word wel een bronnenbeheersysteem te hebben, maar dat is echt wel veel werk omdat het per document bronnen opslaat en op macOS werkt het zelfs niet naar behoren. Het systeem van bladwijzers en losse documentjes blijkt toch niet zo handig.

Wat werkt dan wel? Mijn advies is om Zotero te gebruiken (alternatieven). Het programma wordt omschreven als een research assistant. Het idee is dat je alle bronnen die je raadpleegt toevoegt aan Zotero. In Zotero administreer je wie de auteur is, sla je een korte omschrijving (abstract) van de bron op en geef je aan wanneer je de bron hebt geraadpleegd. In onderstaande screenshot zie je een (bijna) lege Zotero bibliotheek met enkel mijn vorige artikel als geraadpleegde bron. Aan de rechterkant zie je alle meta informatie over de bron.

{{< figure src=“/ox-hugo/Zotero-empty.png” caption=”Figure 1: Zotero met een artikel” >}}

Een hele gave feature van Zotero is dat het ook een snapshot van online bronnen bewaard. In dit geval heeft het een offline versie van mijn artikel gemaakt, precies zoals het eruit zag toen ik het opsloeg. Op deze wijze is altijd de bron beschikbaar, ook al haal ik mijn website morgen offline. Met PDF bronnen slaat het zelfs een kopietje van het bestand op. Super handig, en jij hebt altijd de informatie bij je die je nodig hebt.

{{< figure src=“/ox-hugo/Zotero-snapshot.png” caption=”Figure 2: Een snapshot in Zotero” >}}

Je kan een gratis Zotero account aanmaken als je de zekerheid van een goede backup wil hebben. Dat is optioneel, maar anders moet jij zorgen voor een goede backup van jouw database, dus het is zeker een aanrader. In mijn eigen Zotero staan duizenden papers en websites opgeslagen. Ik gebruik verschillende plugins waar ik later meer over ga schrijven. Met die plugins haal ik bronnen van het web, mijn telefoon en zorg ik voor goede notities over de bronnen.

Zotero heeft mij echt zoveel tijd gescheeld, en ik hoop dat jij hiermee ook een tool krijgt om net iets relaxter te gaan studeren.

#learnToStudy

{{< figure src=“/ox-hugo/20240720225806screenshot.png” >}}

In mijn vorige artikel heb ik uitgelegd hoe je tijd kunt vinden om te studeren, de vraag is echter, is alle tijd hetzelfde? Het simpele antwoord is “nee”. Maar waarom niet, zul je vragen, en daarmee komen we op het onderwerp van dit artikel.

Cal Newport heeft een fantastisch boek geschreven, “Deep Work: Rules for Focused Success in a Distracted World”. In dit boek onderzoekt hij hoe je gefocust werkt en wat er voor nodig is om gefocust te blijven [1]. In het boek identificeert hij 2 soorten werk; “Deep Work” en “Shallow Work”. Deze concepten hebben voor mij de aanpak van mijn dagelijkse werk zelfs veranderd, maar dat is een verhaal voor een andere keer.

Deep work vs Shallow work {#deep-work-vs-shallow-work}

Deep Work is feitelijk het werk waarbij je volledige aandacht (concentratie) nodig is, bijvoorbeeld het lezen van een ingewikkeld stuk theorie in een paper of een boek, of het programmeren van een algoritme voor een lesopdracht. Het gaat hier dus om het type werk waarbij een afleiding zorgt dat je helemaal kwijt raakt waar je was gebleven en afleiding dus zeer ongewenst is.

Shallow Work, daarentegen, is het werk waarvoor je maar weinig focus nodig hebt. Denk bijvoorbeeld aan je email bijwerken, of met medestudenten overleggen. In essentie, werk waarbij je even afgeleid kan worden en meteen weer verder kan gaan.

Een studie, net als een werkomgeving, heeft een combinatie van Deep Work en Shallow Work. Dit feit kun je gebruiken om jouw tijd nog beter in te delen.

Tijd blokkeren {#tijd-blokkeren}

In mijn vorige artikel hebben we blokken van ongeveer 4 uren kunnen reserveren in de kalender. Dit is niet per toeval. Cal Newport beschreef dat geoefende “Deep Workers” in staat zijn om maximaal 3 uren achterelkaar in volle focus te werken. Dat betekent 3 uurtjes werken aan een paper of opdracht, en dan is het wel klaar. Je hersenen hebben er dan ook even genoeg van. Om die 3 uren gefocust te werken heb je wel oefening nodig. Als je nog nooit zo lang aan iets hebt gezeten, dan moet je eerst je hersenen trainen. Dit doe je door met een uurtje te beginnen en dan langzaam op te bouwen. Niet te snel, natuurlijk.

Newport identificeerde ook dat om gefocust te gaan werken de meeste mensen een ritueel ontwikkelen. Een soort mentaal stappenplan om je hersenen zover te krijgen dat je er even echt voor kan gaan zitten. Hij beschrijft de rituelen van grote denkers uit het verleden, maar in dit artikel zal ik mijn strategie uitleggen. Het is zeer de moeite waard om de verschillende strategieën in zijn boek te onderzoeken.

Een ritueel om te focussen {#een-ritueel-om-te-focussen}

Een techniek die ik veel inzet is om eerst Shallow Work tijd te reserveren om zoveel mogelijk de taken voor een week (of leerlijn) uit te schrijven en na te lezen. Simpelweg een todolijst is al meer dan voldoende. Op deze wijze hebben mijn hersenen niet de neiging om te gaan denken “zouden we iets vergeten?”. Elke keer als ik ga zitten om te studeren neem ik eerst een periode van 10 tot 15 minuten om:

  1. De todolijst na te lopen of het nog up-to-date is.
  2. Alle middelen die ik nodig heb te verzamelen.
  3. Mijn omgeving leeg te maken, alleen dat wat ik nodig heb mag er zijn.

De todolijst, in mijn geval een notitie waarin ik alle onderdelen van een leerlijn heb beschreven (welke boeken, papers, websites moet ik lezen? Wat is de eindopdracht?) en waarbij ik al mijn vragen in een eigen sectie zet. Deze vragen worden (als het goed is) gedurende de leerlijn beantwoord, en zo niet, dan heb ik op het einde iets om te vragen aan de docent.

Laat ik een voorbeeld geven. Voor de cursus “Software Quality Management” kreeg ik een waslijst aan artikelen om te lezen, echt een serieuze stapel. De artikelen hadden relatie op onderdelen van de lesstof en ik heb vervolgens voor mijzelf een planning gemaakt welk paper ik in welke week gelezen moest hebben (waar ze relevant waren voor het lesonderwerp). Ook heb ik ze gekoppeld aan onderdelen van de eindopdracht, zodat ik altijd een startpunt heb. Zo kun je natuurlijk alles van een leerlijn uitzoeken en uitstippelen, vaak is een syllabus of lesindeling hier heel handig als leidraad. Hieronder zie je daadwerkelijke screenshots van mijn “planning” voor de leerlijn.

{{< figure src=“/ox-hugo/20240718232022screenshot.png” caption=”Figure 1: Papers in een weekplanning” >}}

{{< figure src=“/ox-hugo/20240718235148screenshot.png” caption=”Figure 2: Een simpele planning om bij te houden of ik op schema zit” >}}

Geen afleidingen {#geen-afleidingen}

Als je voor jezelf alles op een rijtje hebt, dan kun je met een snelle review al vrij eenvoudig in een goede flow terecht komen. Ik zorg er daarna voor dat ik alles wat ik nodig heb op mijn bureau leg, of digitaal beschikbaar heb. Er is niets zo vervelends als beginnen en er dan achter te komen dat je iets in de mail moet opzoeken om dan afgeleid te worden door een gekke meme die door een medestudent is opgestuurd, weg is je studietijd.

{{< figure src=“/ox-hugo/20240720232155screenshot.png” caption=”Figure 3: Een opmerking waar je wel op moet reageren, toch?” >}}

En dat is ook meteen het 3de punt. Alles leeg (of uit). Zet je telefoon op stil en “niet storen”, alles wat notificaties op de computer kan maken afsluiten en de niet storen modus aanzetten. Soms vraagt dit thuis ook afspraken zoals “als de deur dicht is, niet storen”. Dat is misschien gek, maar als je deeltijd studeert is een gemiste studiesessie al snel een beetje vertraging bij het afronden, en als het te vaak gebeurt dan loop je echt achter.

TIP: Voor je telefoon zijn apps zoals [Forest](https://play.google.com/store/apps/details?id=cc.forestapp&hl=en_US) echt ideaal. Je krijgt geen notificaties, en je kan niks met jouw telefoon. En voor elke sessie plant je weer boompjes in jouw digitale bos, hoe leuk is dat?

Dit doe je allemaal in dat ene kwartiertje (of een uur afhankelijk van hoe duidelijk je al hebt wat je moet gaan doen), en dan is het tijd om echt aan de gang te gaan, je begint feitelijk aan jouw Deep Work sessie. Ik zet dan vaak nog een zacht achtergrond muziekje aan om complexe papers te lezen of code te schrijven en gaan maar.

Niet alle tijd is hetzelfde. Om effectief te studeren moet je het voor jezelf mogelijk maken om te concentreren. Zorg dat je al je vragen en zorgen hebt opgeschreven, dat er geen afleidingen zijn en dat alles wat je nodig hebt er gewoon al is. Bouw het tijdens jouw studie langzaam op, eerst sessie van een uur en dan op het einde echt knallen!

Bronnen {#bronnen}

[1] Newport (2016) Deep Work: Rules for Focused Success in a Distracted World, Grand Central Publishing.

#learnToStudy

This is a Dutch artcile, there is also an English version.

Dus, jij hebt besloten om te gaan studeren? Misschien wil je jouw HBO- of Masterdiploma halen, of juist dat ene supertechnische certificaat bemachtigen. Het is geweldig dat je deze stap gaat zetten, maar zodra je begint, zul je vrij snel de vraag moeten beantwoorden waar je de tijd vandaan haalt.

Tijd is onze meest waardevolle, niet-hernieuwbare bron. Studeren vergt tijd – en niet zo'n klein beetje ook – dus wil je het natuurlijk goed doen. De meeste studies verwachten dat je wekelijks ergens tussen de 12 en 24 uur investeert om bij te blijven, en dat is flink wat! Als je nog niet studeert, probeer dan eens na te denken over welke dagen en momenten je die tijd kunt vrijmaken. Ga je minder uit eten of juist minder sporten? Vroeg opstaan in het weekend, of juist extra laat naar bed?

De meeste studenten lopen tegen dit probleem aan wanneer ze beginnen met studeren; ik zie het maar al te vaak. Je start vol vertrouwen aan een studie, maar dan realiseer je je dat je niet hebt nagedacht over hoe het in jouw leven past en wat je moet opofferen om te kunnen studeren. Veel studenten proberen dan ook alles tegelijk te doen, met alle gevolgen van dien. Gelukkig is er een strategie die je kunt toepassen. Ik heb deze strategie zelf gebruikt tijdens mijn deeltijd HBO- en Masterstudies, en voor mij werkte het perfect.

De kern van de strategie is het gebruik van je agenda. Om goed de baas te zijn over je tijd, moet je precies weten wanneer je wat gaat doen. Dit klinkt eenvoudig, maar de meeste mensen noteren alleen belangrijke zaken in hun agenda, zoals de tandartsafspraak of de verjaardag van tante Loes.

Op een dag doe je natuurlijk heel veel. Zelf heb ik een baan van 40 uur per week, ga ik naar de sportschool, wil ik tijd doorbrengen met mijn gezin, en nog veel meer. Als ik die activiteiten niet zou inplannen, raak ik al snel het overzicht kwijt. Zonder overzicht wordt het ook moeilijk om effectief te studeren, want dan staat opeens tante Loes op de stoep.

De strategie omvat de volgende stappen:

  1. Gebruik een kalender die je overal kunt raadplegen.
  2. Blokkeer je werktijd en houd je hieraan.
  3. Blokkeer ook tijd voor je gezin en vrienden.
  4. Plan studiemomenten in.

Het maakt niet echt uit welke kalender je gebruikt, zolang deze maar voor jou beschikbaar is. In mijn voorbeelden zal ik de Proton Calendar gebruiken, die is lekker veilig en gevestigd in de EU. Zo heb je geen last van grote techbedrijven die meekijken in jouw planning.

Een lege kalender ziet er zo uit.

{{< figure src=“/ox-hugo/20240715193929screenshot.png” caption=”Figure 1: Een lege agenda, dat is nog eens rustig!” >}}

Zoals je kunt zien, heb ik elk gebied van mijn leven een eigen kleurtje gegeven. Dat is superhandig, want daarmee zie je in één oogopslag waar een afspraak bij hoort.

{{< figure src=“/ox-hugo/20240715194152screenshot.png” caption=”Figure 2: Elke kalender een eigen kleur, helaas ben ik niet goed in kleuren kiezen.” >}}

Als eerste stap plannen we de werkdagen in. Ik heb zelf al mijn werkafspraken in mijn agenda staan, maar ook dan blokkeer ik mijn werktijd met een kalenderafspraak. Op die manier weet ik altijd wanneer ik wel of niet beschikbaar ben op een gegeven tijdstip. Als je ook moet reizen naar en van je werk, plan dan ook die tijd in. Ik doe dat bijvoorbeeld op woensdag en vrijdag.

{{< figure src=“/ox-hugo/20240715195048screenshot.png” caption=”Figure 3: Stap 1, plan jouw werkdagen in” >}}

Dit lijkt misschien een beetje suf, want je weet toch dat je werkt? Natuurlijk is dat zo, maar voor het mentale model van jouw beschikbare tijd is het belangrijk om deze periode te blokkeren. Zo weet je zeker dat je die tijd niet kunt gebruiken om te studeren. En kijk, nu lijkt het alsof er nog heel veel tijd over is, toch?

Tijdens mijn studie vond ik het ook belangrijk om tijd te reserveren voor mijn gezin en om leuke dingen te doen. Studeren moet leuk zijn en mag niet aanvoelen als een straf waardoor je geen plezierige activiteiten meer kunt ondernemen. Laten we nu alle tijd voor gezin en vrienden ook in de kalender zetten.

{{< figure src=“/ox-hugo/20240715195810screenshot.png” caption=”Figure 4: Stap 2, tijd voor het gezin en vrienden” >}}

Dan wordt het al een ander verhaal. Voor mij was het weekend belangrijk. Iedereen werkt hard gedurende de week, dus er is ook tijd nodig waarin we allemaal kunnen ontspannen. Wat meteen opvalt, is dat er dan relatief weinig tijd over lijkt te zijn om te studeren, maar schijn bedriegt. Ik vond het heel fijn om 's avonds te studeren, zo ongeveer van 8 uur tot 12 uur. Dat zijn toch 4 uren op een dag die voor mij goed werkten. Laten we die tijd inplannen.

{{< figure src=“/ox-hugo/20240715200322screenshot.png” caption=”Figure 5: Stap 3, tijd om te studeren” >}}

Dat zijn 4 blokken van 4 uur, plus de zaterdagochtend van 3 uurtjes — in totaal 19 uur die beschikbaar zijn om te studeren. Tijdens het inplannen viel meteen op dat de sportsessie op donderdag niet handig uitkwam, dus die heb ik verplaatst naar de ochtend. Met al deze ingeplande studietijd blijft er ook ruimte over voor familie en vrienden, precies op die momenten dat het voor iedereen goed uitkomt.

Wat nu als je geen avondmens bent? Ik begeleid ook studenten tijdens hun afstudeertraject, en sommigen zijn meer een ochtendmens. Zij staan om 5 uur 's ochtends op en studeren tot 7 uur. Dit doen ze vaak omdat ze kleine kinderen hebben en niet willen dat hun partner met alle zorg achterblijft. Zelf geef ik de voorkeur aan de avond, maar goed, ieder zijn ding.

Wanneer het nodig is, bijvoorbeeld als je dicht bij een deadline zit, kun je natuurlijk altijd tijd van een ander blok gebruiken. Doe dat echter altijd in overleg. Het voordeel van alles uitplannen is dat het duidelijkheid schept, maar daardoor ontstaan ook verwachtingen. Als je dan iets verandert, ook al is het tijdelijk, leg het uit zodat je geen problemen krijgt. Ik paste mijn schema vaak aan door naar de studielast van een module te kijken en een studieplan te maken; daarover zal ik later meer schrijven.

Zelf had ik ook nog bonustijd in mijn schema. De zondagochtend was vaak beschikbaar, afhankelijk van hoe mijn zoon naar een bouldersessie keek. Daarnaast had ik 's ochtends vaak nog een uurtje tijd, tussen het moment dat iedereen naar werk en school gaat en dat ik daadwerkelijk aan mijn werk begin. Die tijd gebruikte ik om papers te lezen en mijn eerste aantekeningen te maken. Maar ook daarover zal ik later meer schrijven.

Het “goed” gebruiken van de kalender om tijd te reserveren is natuurlijk niet revolutionair. Er zijn ook andere methoden zoals de “Trident Method” (uitgelegd door Ali Abdaal), en talloze andere technieken om tijd goed in te delen. Maar dit is de methode die voor mij goed werkte.

Hopelijk begin je jouw studietijd nu met een goed systeem om je tijd in te delen. Dit is de eerste stap naar een succesvolle studietijd!

#writing #learnToStudy

This is an English article, there is also a Dutch version.

So, you've decided to start studying? Maybe you want to earn your Bachelor or Master's degree, or perhaps you're aiming for that highly technical certificate. It's great that you're taking this step, but once you begin, you'll quickly need to answer the question of where you'll find the time.

Time is our most valuable, non-renewable resource. Studying requires time – and not just a little – so you naturally want to use it well. Most programs expect you to invest between 12 and 24 hours per week to keep up, and that's quite a bit! If you're not currently studying, try thinking about which days and times you could free up that time. Will you eat out less or exercise less? Get up early on weekends or stay up late?

Most students run into this problem when they start studying; I've seen it all too often. You start your studies with full confidence, only to realize you haven't considered how it fits into your life and what you need to sacrifice to make it work. Many students try to do everything at once, with predictable results. Thankfully, there is a strategy you can apply. I used this strategy during my part-time Bachelor and Master's studies, and it worked perfectly for me.

The core of the strategy is using your calendar. To manage your time effectively, you need to know exactly when you'll do what. This sounds simple, but most people only note important things in their calendar, like dentist appointments or Aunt Loes's birthday.

You do a lot in a day. Personally, I have a 40-hour workweek, go to the gym, spend time with my family, and much more. If I didn't plan these activities, I'd quickly lose track. Without an overview, it becomes difficult to study effectively because suddenly Aunt Loes will show up at the door.

The strategy includes the following steps:

  1. Use a calendar that you can access anywhere.
  2. Block out your work hours and stick to them.
  3. Also block time for your family and friends.
  4. Schedule study sessions.

It doesn't really matter which calendar you use, as long as it's accessible to you. In my examples, I'll use the Proton Calendar because it's secure and based in the EU. This way, you don't have to worry about big tech companies snooping into your schedule.

An empty calendar looks like this:

{{< figure src=“/ox-hugo/20240715193929screenshot.png” caption=”Figure 1: An empty calendar, that's quite calm!” >}}

As you can see, I've given each area of my life its own color. This is super handy because you can immediately see which appointment belongs to which area.

{{< figure src=“/ox-hugo/20240715194152screenshot.png” caption=”Figure 2: Each calendar its own color, though I'm not great at choosing colors.” >}}

As the first step, let's plan the workdays. I already have all my work appointments in my calendar, but I also block my work time with a calendar event. This way, I always know when I am or am not available. If you also have to travel to and from work, plan that time as well. I do this on Wednesdays and Fridays.

{{< figure src=“/ox-hugo/20240715195048screenshot.png” caption=”Figure 3: Step 1, plan your workdays” >}}

This may seem a bit silly because you know you're working, right? Of course, but for the mental model of your available time, it's essential to block this period. This way, you know you can't use that time for studying. See, now it seems like there's still a lot of time left, right?

During my study, I also found it important to reserve time for my family and to do fun things. Studying should be enjoyable and not feel like a punishment that prevents you from doing pleasant activities. Let’s now add all the family and friends' time to the calendar.

{{< figure src=“/ox-hugo/20240715195810screenshot.png” caption=”Figure 4: Step 2, time for family and friends” >}}

Now it's a different story. Weekends were important for me. Everyone works hard during the week, so time is needed to relax together. What's immediately noticeable is that there seems to be relatively little time left for studying, but appearances can be deceiving. I found it very convenient to study in the evenings, from around 8 PM to 12 AM. That's 4 hours in a day that worked well for me. Let's schedule that time.

{{< figure src=“/ox-hugo/20240715200322screenshot.png” caption=”Figure 5: Step 3, time to study” >}}

That amounts to 4 blocks of 4 hours, plus 3 hours on Saturday morning — totaling 19 hours available for studying. While planning, it immediately became apparent that the workout session on Thursday didn't fit well, so I moved it to the morning. With all this scheduled study time, there's also room for family and friends, at times that are often convenient for everyone.

But what if you're not a night owl? I also guide students during their graduation projects, and some are more morning people. They get up at 5 AM and study until 7 AM, often because they have small children and don't want to leave their partner with all the care. Personally, I prefer the evening, but to each their own.

When necessary, such as when a deadline is approaching, you can always borrow time from another block, but always do this in consultation. The advantage of planning everything is that it provides clarity, but it also creates expectations. If you change something, even temporarily, explain it, so you don't run into problems. I often adjusted my schedule by looking at the workload of a module and creating a study plan; I'll write more about that later.

I also had bonus time in my schedule. Sunday morning was often available, depending on my son's interest in a bouldering session. Additionally, I often had an hour in the morning, between when everyone left for work and school and when I started working. I used that time to read papers and make my initial notes. But more on that later.

Using the calendar effectively to reserve time is not revolutionary. There are other methods such as the “Trident Method” (explained by Ali Abdaal), and many other techniques for good time management. But this is the method that worked well for me.

Hopefully, you'll start your study time now with a good system to manage your time. This is the first step towards a successful study period!

#learnToStudy #writing

So, on Thursday I defended my thesis in front of the graduation committee, and passed! This means that the work I have been doing for the last year comes to an end. From now on there are not long nights and weekends working on my thesis anymore.

Back in 2021 I started my journey of achieving a Master's degree, first with a connecting program and then with the 2 year Master program. Even though I have been in computer science in some form for the last 30 years I still found it to be quite a learning experience.

I greatly enjoyed the track on formal verification of systems and found logic quite enjoyable to work with. My least favorite topic was the security course, mainly due to its reliance on a lot of relatively old literature, but that is only a small “unhappiness”. The courses were well structured and very do-able in the time given.

So happy to be free of the work now. Here is to picking up some fun things again!