Advent of Code 2020 solutions in Zig
const std = @import("std");

const PATH = "input/day21.txt";
const Str = []const u8;

pub fn first(allocator: ?std.mem.Allocator) anyerror!usize {
    var arena = std.heap.ArenaAllocator.init(allocator.?);
    defer arena.deinit();

    var food = try parseInput(arena.allocator(), @embedFile(PATH));
    try food.reduce();

    var count: usize = 0;
    for (food.ingrediens) |item| {
        if (food.allergens.get(item) == null) count += 1;
    }

    return count;
}

fn stringCompare(context: void, a: Str, b: Str) bool {
    _ = context;
    return std.ascii.lessThanIgnoreCase(a, b);
}

pub fn second(allocator: ?std.mem.Allocator) anyerror!Str {
    var arena = std.heap.ArenaAllocator.init(allocator.?);
    defer arena.deinit();

    var ret = std.ArrayList(u8).init(allocator.?);

    var food = try parseInput(arena.allocator(), @embedFile(PATH));
    try food.reduce();

    var order = std.ArrayList(Str).init(arena.allocator());

    var ait = food.allergens.iterator();
    while (ait.next()) |ntry| {
        try order.append(ntry.value_ptr.*);
    }

    std.sort.sort(Str, order.items, {}, comptime stringCompare);

    for (order.items) |o| {
        var it = food.allergens.iterator();
        while (it.next()) |entry| {
            if (std.mem.eql(u8, o, entry.value_ptr.*)) {
                try ret.appendSlice(entry.key_ptr.*);
                try ret.append(',');
            }
        }
    }

    ret.items = ret.items[0 .. ret.items.len - 1];

    return ret.toOwnedSlice();
}

const Foods = struct {
    ingrediens: []Str,
    reduced: std.StringHashMap(std.BufSet),
    allergens: std.BufMap,

    fn reduce(self: *@This()) !void {
        while (self.reduced.count() != self.allergens.count()) {
            var it = self.reduced.iterator();
            while (it.next()) |entry| {
                // if only one item left, that is the allergen
                if (entry.value_ptr.*.count() == 1) {
                    var eit = entry.value_ptr.*.iterator();
                    const value = eit.next().?;
                    try self.allergens.put(value.*, entry.key_ptr.*);
                } else {
                    // remove all known allergens from the ingrediens list
                    var ait = self.allergens.iterator();
                    while (ait.next()) |elem| {
                        entry.value_ptr.*.remove(elem.key_ptr.*);
                    }
                }
            }
        }
    }
};

fn parseInput(allocator: std.mem.Allocator, in: Str) !Foods {
    var f = Foods{
        .ingrediens = undefined,
        .reduced = std.StringHashMap(std.BufSet).init(allocator),
        .allergens = std.BufMap.init(allocator),
    };

    var ings = std.ArrayList(Str).init(allocator);
    var lines = std.mem.split(u8, in, "\n");

    while (lines.next()) |line| {
        // std.debug.print("{s}\n", .{line});
        var ing_allerg = std.mem.tokenize(u8, line, "()");

        const ingredients = ing_allerg.next().?;
        const allergens = ing_allerg.next().?;

        // collect ingredients
        var ing_list = std.ArrayList(Str).init(allocator);
        defer ing_list.deinit();

        var ing_it = std.mem.tokenize(u8, ingredients, " ");
        while (ing_it.next()) |ing| {
            try ing_list.append(ing);
        }

        try ings.appendSlice(ing_list.items);

        // collect allergens
        var all_it = std.mem.tokenize(u8, allergens, ", ");
        _ = all_it.next(); // drop "contains"
        while (all_it.next()) |a| {
            const res = try f.reduced.getOrPut(a);

            if (res.found_existing == true) {
                var new = std.BufSet.init(allocator);
                for (ing_list.items) |item| {
                    if (res.value_ptr.contains(item)) {
                        try new.insert(item);
                    }
                }
                res.value_ptr.* = new;
            } else {
                res.value_ptr.* = std.BufSet.init(allocator);
                for (ing_list.items) |item| {
                    try res.value_ptr.*.insert(item);
                }
            }
        }
    }

    f.ingrediens = try ings.toOwnedSlice();

    return f;
}

test "day20a" {
    try std.testing.expectEqual(@as(usize, 2162), try first(std.testing.allocator));
}

test "day20b" {
    const actual = try second(std.testing.allocator);
    defer std.testing.allocator.free(actual);
    try std.testing.expectEqualStrings("lmzg,cxk,bsqh,bdvmx,cpbzbx,drbm,cfnt,kqprv", actual);
}

const test_input =
    \\mxmxvkd kfcds sqjhc nhms (contains dairy, fish)
    \\trh fvjkl sbzzf mxmxvkd (contains dairy)
    \\sqjhc fvjkl (contains soy)
    \\sqjhc mxmxvkd sbzzf (contains fish)
;