Advent of Code 2024: Day 1

> zig version
0.13.0

The input data is provided in two columns, as in this test file:

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

I would like to run the program passing in the name of the input file as a parameter:

> ./day-01 day-01-input.dat

So our first task is to get the command-line arguments, check that a filename is provided, and pass this into our program.

I do all this within main, calling solveDay to perform the data loading and calculations.

pub fn main() !void {
    // get allocator
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);  // <1>
    const allocator = arena.allocator();
    defer _ = arena.deinit();

    // get command-line args
    const args = try std.process.argsAlloc(allocator);                  // <2>
    defer std.process.argsFree(allocator, args);

    // see if filename is on command-line
    if (args.len == 2) {                                                // <3>
        const filename = args[1];
        solveDay(allocator, filename) catch |err| {                     // <4>
            switch (err) {                                              // <5>
                error.FileNotFound => {
                    std.debug.print("Error: File {s} was not found.\n", .{filename});
                    std.process.exit(1);
                },
                else => {
                    std.debug.print("Error: in processing file.\n", .{});
                    std.process.exit(1);
                },
            }
        };
    } else {
        std.debug.print("Provide a filename of input data.\n", .{});    // <6>
    }
}
  1. the allocator is required throughout the program - I use the arena allocator because everything gets freed at the end of the program.
  2. the command-line args are retrieved using argsAlloc and the allocator. They are put into a const variable called args which is an array. Any errors exit the program.
  3. check the length of the array: the args include the name of the calling process as the first argument.
  4. either call the solveDay function with the filename
  5. errors are caught, so we can print a meaningful error message for if a file is not found, or there are errors in processing the file.
  6. or print a helpful message.

The top-level function reads in the data and collects the solutions to each part:

fn solveDay (allocator: std.mem.Allocator, filename: []const u8) !void {
    // put the numbers in two lists, one per column
    var column1 = std.ArrayList(usize).init(allocator);                         // <1>
    var column2 = std.ArrayList(usize).init(allocator);

    try readData(allocator, filename, &column1, &column2);                      // <2>

    // sort the two columns
    std.mem.sort(usize, column1.items, {}, comptime std.sort.asc(usize));       // <3>
    std.mem.sort(usize, column2.items, {}, comptime std.sort.asc(usize));

    std.debug.print("Part 1: {d}\n", .{totalDifference(&column1, &column2)});   // <4>
    std.debug.print("Part 2: {d}\n", .{totalSimilarity(&column1, &column2)});
}
  1. Create the two columns here - this function then owns the memory.
  2. Reads the data from the given filename, using references to the columns to store the loaded data.
  3. Sorts each column using a std library function.
  4. Calls a calculation for each part, passing in references to the two columns.

This function is responsible for reading in the two columns of data. It does not return anything, instead placing the data into the respective ArrayList instances.

fn readData (allocator: std.mem.Allocator, filename: []const u8, 
    column1: *std.ArrayList(usize), column2: *std.ArrayList(usize)) !void {

    const file = try std.fs.cwd().openFile(filename, .{});                              // <1>
    defer file.close();
    const stat = try file.stat();                                     
    const data = try file.readToEndAlloc(allocator, stat.size);                         // <2>

    var lines = std.mem.tokenize(u8, data, "\n");                                       // <3>
    while (lines.next()) |line| {                                                       // <4>
        var columns = std.mem.tokenize(u8, line, " ");                                  // <5>
        const number1 = try std.fmt.parseInt(usize, columns.next() orelse return, 10);  // <6>
        const number2 = try std.fmt.parseInt(usize, columns.next() orelse return, 10);
        try column1.append(number1);                                                    // <7>
        try column2.append(number2);
    }
}
  1. Opens the file for reading - this assumes the file exists, etc.
  2. Reads the file into a buffer - uses file.stat().size so the buffer size is the file size.
  3. Creates an iterator for working over lines.
  4. Loops over each line.
  5. Creates an iterator over the line - we know the line must have two numbers.
  6. Parses the number from each column: columns.next() orelse return reflects that next may theoretically (not for our data) return null, and returns if so.
  7. The numbers are then appended to the column lists.

Part 1 requires a sum of the absolute value of the differences between the two sorted lists:

fn totalDifference(column1: *std.ArrayList(usize), column2: *std.ArrayList(usize)) usize { // <1>
    var result : usize = 0;
    
    for (column1.items, column2.items) |left, right| {                                     // <2>
        if (left > right) {                                                                // <3>
            result += left - right;
        } else {
            result += right - left;
        }
    }

    return result;
}
  1. References to the two column lists are provided to the function, and a usize will be returned.
  2. The for loop steps through the two columns in parallel - a very tidy syntax.
  3. Make sure we add up the absolute value of the differences.

Part 2 requires a different kind of sum. My solution here makes use of a std library function to do the count:

fn totalSimilarity(column1: *std.ArrayList(usize), column2: *std.ArrayList(usize)) usize {
    var result: usize = 0;

    for (column1.items) |left| {
        result += left * std.mem.count(usize, column2.items, &[_]usize{left});    // <1>
    }

    return result;
}
  1. Count how often the first column item appears in the second column.

Final Program

const std = @import("std");

pub fn main() !void {
    // get allocator
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    const allocator = arena.allocator();
    defer _ = arena.deinit();

    // get command-line args
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    // see if filename is on command-line
    if (args.len == 2) {
        const filename = args[1];
        solveDay(allocator, filename) catch |err| {
            switch (err) {
                error.FileNotFound => {
                    std.debug.print("Error: File {s} was not found.\n", .{filename});
                    std.process.exit(1);
                },
                else => {
                    std.debug.print("Error: in processing file.\n", .{});
                    std.process.exit(1);
                },
            }
        };
    } else {
        std.debug.print("Provide a filename of input data.\n", .{});
    }
}

fn solveDay (allocator: std.mem.Allocator, filename: []const u8) !void {
    // put the numbers in two lists, one per column
    var column1 = std.ArrayList(usize).init(allocator);
    var column2 = std.ArrayList(usize).init(allocator);

    try readData(allocator, filename, &column1, &column2);

    // sort the two columns
    std.mem.sort(usize, column1.items, {}, comptime std.sort.asc(usize));
    std.mem.sort(usize, column2.items, {}, comptime std.sort.asc(usize));

    std.debug.print("Part 1: {d}\n", .{totalDifference(&column1, &column2)});
    std.debug.print("Part 2: {d}\n", .{totalSimilarity(&column1, &column2)});
}

fn totalDifference(column1: *std.ArrayList(usize), column2: *std.ArrayList(usize)) usize {
    var result : usize = 0;
    
    for (column1.items, column2.items) |left, right| {
        if (left > right) {
            result += left - right;
        } else {
            result += right - left;
        }
    }

    return result;
}

fn totalSimilarity(column1: *std.ArrayList(usize), column2: *std.ArrayList(usize)) usize {
    var result: usize = 0;

    for (column1.items) |left| {
        result += left * std.mem.count(usize, column2.items, &[_]usize{left});
    }

    return result;
}

// Read in the two columns of data from given filename
fn readData (allocator: std.mem.Allocator, filename: []const u8, 
    column1: *std.ArrayList(usize), column2: *std.ArrayList(usize)) !void {

    const file = try std.fs.cwd().openFile(filename, .{});
    defer file.close();
    const stat = try file.stat();
    const data = try file.readToEndAlloc(allocator, stat.size);

    var lines = std.mem.tokenize(u8, data, "\n");
    while (lines.next()) |line| {
        var columns = std.mem.tokenize(u8, line, " ");
        const number1 = try std.fmt.parseInt(usize, columns.next() orelse return, 10);
        const number2 = try std.fmt.parseInt(usize, columns.next() orelse return, 10);
        try column1.append(number1);
        try column2.append(number2);
    }
}

Page from Peter's Scrapbook, output from a VimWiki on 2024-12-03.