Potato Blog


Advent of Code: Day 1

The software engineer’s calendar has 25 days in it (Dec01 - Dec25). Every other day of the year is preparing us for Advent of Code , the advent calendar for nerds filled with programming puzzles that can be solved in any programming language. It’s a great way to learn a new programming language or interesting nuances about a language you understand well. For instance, during Advent of Code 2023 I learned range in golang does not guarantee it will iterate in order. If order is important when iterating, using for is necessary.

This year I’ve decided to solve the puzzles using both golang and zig. I won’t post every day of AoC here but this first day is a good exercise in my journey to learn more zig.

Spoiler warning! The remainder of the post contains the solution to Day 1: Historian Hysteria.

On the first day you’re tasked with comparing two lists in search of answers to a location. Elvish highjinks always cause trouble, but nothing that can’t be solved with a little bit of code. You can read the story but this is about the code, so let’s dive in. The two lists are in an input that appears like below, two columns separated by three spaces:

3   4
4   3
2   5
1   3
3   9
3   3

The instructions say we must pair up the smallest number in the left list with the smallest number in the right list. Within each of the pairs you must find the distance between the two locations by subtracting the left from the right. Finally, you must sum all the differences to find the total distance. First thing that comes to mind is we need to sort these lists, but first we need a data structure that will help us. So std.ArrayList to the rescue.

const std = @import("std");
const print = std.debug.print;
const file = @embedFile("input.txt");

pub fn main() !void {
  // Need to allocate some bytes in memory.
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  defer _ = gpa.deinit();
  const allocator = gpa.allocator();

  // We'll allocate this in memory to hold the left column of values
  var left = std.ArrayList(i32).init(allocator);
  defer left.deinit();

  // This will hold the right column of values
  var right = std.ArrayList(i32).init(allocator);
  defer right.deinit();
}

Okay we have a place to hold our lists. Let’s process our input and get those values into their respective lists.

const std = @import("std");
const print = std.debug.print;
const file = @embedFile("input.txt");

pub fn main() !void {
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  defer _ = gpa.deinit();
  const allocator = gpa.allocator();

  var left = std.ArrayList(i32).init(allocator);
  defer left.deinit();

  var right = std.ArrayList(i32).init(allocator);
  defer right.deinit();

  // Tokenize will split our input by any number of delimiters, here we use a space and newline
  // `std.mem.tokenize` drops empty values so we should only get our numbers
  var tok = std.mem.tokenize(u8, file, " \n");

  // Now we iterate `tok` calling `tok.next()` each time we want to see the next token
  // `tok.next()` returns an optional, we'll use a while loop to check for null and unwrap
  // the value as `t` (left number), then within the loop we call `tok.next().?`
  // again to unwrap the right value, finally **append** these values to their respective list
  // moving along our inputs two at a time
  while (tok.next()) |t| {
    const int_left = try std.fmt.parseInt(i32, t, 10);
    const int_right = try std.fmt.parseInt(i32, tok.next().?, 10);

    try left.append(int_left);
    try right.append(int_right);
  }
}

There. Now we need to ensure that we pair up the smallest numbers on the left list to the smallest numbers on the right list. Since we’re storing integers in our lists, this becomes an easy call to sort in the standard library.

const std = @import("std");
const print = std.debug.print;
const file = @embedFile("input.txt");

pub fn main() !void {
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  defer _ = gpa.deinit();
  const allocator = gpa.allocator();

  var left = std.ArrayList(i32).init(allocator);
  defer left.deinit();

  var right = std.ArrayList(i32).init(allocator);
  defer right.deinit();

  var tok = std.mem.tokenize(u8, file, " \n");

  while (tok.next()) |t| {
    const int_left = try std.fmt.parseInt(i32, t, 10);
    const int_right = try std.fmt.parseInt(i32, tok.next().?, 10);

    try left.append(int_left);
    try right.append(int_right);

  }

  // Sort left and right lists in ascending order
  std.mem.sort(i32, left.items, {}, comptime std.sort.asc(i32));
  std.mem.sort(i32, right.items, {}, comptime std.sort.asc(i32));
}

Last thing left to do is calculate the distances by pairing up the values from both lists, subtracting them, and sum all the differences as the total.

const std = @import("std");
const print = std.debug.print;
const file = @embedFile("input.txt");

pub fn main() !void {
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  defer _ = gpa.deinit();
  const allocator = gpa.allocator();

  var left = std.ArrayList(i32).init(allocator);
  defer left.deinit();

  var right = std.ArrayList(i32).init(allocator);
  defer right.deinit();

  var tok = std.mem.tokenize(u8, file, " \n");

  while (tok.next()) |t| {
    const int_left = try std.fmt.parseInt(i32, t, 10);
    const int_right = try std.fmt.parseInt(i32, tok.next().?, 10);

    try left.append(int_left);
    try right.append(int_right);

  }

  std.mem.sort(i32, left.items, {}, comptime std.sort.asc(i32));
  std.mem.sort(i32, right.items, {}, comptime std.sort.asc(i32));

  // Part 1: calculate distance
  var total_distance: u32 = 0;
  for (left.items, 0..) |_, i| {
    // use @abs built-in to ignore signs when subtracting
    total_distance += @abs(left.items[i] - right.items[i]);
  }

  print("Part 1: {d}\n", .{total_distance});
}

Every Advent of Code puzzle has a second part. This half will test how reusable/flexible or well-thought your first half was. The test is whether you need to rewrite it all, or not. Lucky for me, this one was easy. In the second half it turns out the elves couldn’t agree how to properly read the locations from the lists. Now we need to calculate a similarity score by adding up each number in the left list after multiplying it by the number of occurrences of that number in the right list.

const std = @import("std");
const print = std.debug.print;
const file = @embedFile("input.txt");

pub fn main() !void {
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  defer _ = gpa.deinit();
  const allocator = gpa.allocator();

  var left = std.ArrayList(i32).init(allocator);
  defer left.deinit();

  var right = std.ArrayList(i32).init(allocator);
  defer right.deinit();

  var tok = std.mem.tokenize(u8, file, " \n");

  while (tok.next()) |t| {
    const int_left = try std.fmt.parseInt(i32, t, 10);
    const int_right = try std.fmt.parseInt(i32, tok.next().?, 10);

    try left.append(int_left);
    try right.append(int_right);

  }

  std.mem.sort(i32, left.items, {}, comptime std.sort.asc(i32));
  std.mem.sort(i32, right.items, {}, comptime std.sort.asc(i32));

  var total_distance: u32 = 0;
  for (left.items, 0..) |_, i| {
    total_distance += @abs(left.items[i] - right.items[i]);
  }

  print("Part 1: {d}\n", .{total_distance});

  // Part 2: calculate similarity
  var total_similarity: i32 = 0;
  var count_similarity: i32 = 0;

  // Iterate each number in the left list
  for (left.items) |num_left| {
    // Find number of occurrences in the right list
    for (right.items) |num_right| {
      if (num_left == num_right) {
        count_similarity += 1;
      }
    }
    // Keep a running total and reset our similarity count before
    // moving to the next number in the left list
    total_similarity += num_left * count_similarity;
    count_similarity = 0;
  }

  print("Part 2: {d}\n", .{total_similarity});
}

This is an easy one. They get tougher as you progress, but it’s a great example of why AoC is a good way to learn a language and, genuinely, how to code in the real world. The problems you will face are not all extreme edge cases. Some puzzles are common occurrences in every day software engineering and they’re presented in a way that is entertaining. Give it a try, and fail a lot.