Advent of Code 2021 in Julia – Day 16: Packet Decoder

Table of Contents

I can feel the difficulty ramping up in the last few days. For the second part of the puzzle, I had to utilized Julia’s meta-programming capacity. It’s the first time I touched the meta-programming section in the Julia documentation. Fortunately my code worked fine this time, and I can still keep up with solving the puzzles during the same day they’re released.

Part 1

Show challenge - day 16, part 1

As you leave the cave and reach open waters, you receive a transmission from the Elves back on the ship.

The transmission was sent using the Buoyancy Interchange Transmission System (bits), a method of packing numeric expressions into a binary sequence. Your submarine’s computer has saved the transmission in hexadecimal (your puzzle input).

The first step of decoding the message is to convert the hexadecimal representation into binary. Each character of hexadecimal corresponds to four bits of binary data:

0 = 0000
1 = 0001
2 = 0010
3 = 0011
4 = 0100
5 = 0101
6 = 0110
7 = 0111
8 = 1000
9 = 1001
A = 1010
B = 1011
C = 1100
D = 1101
E = 1110
F = 1111

The bits transmission contains a single packet at its outermost layer which itself contains many other packets. The hexadecimal representation of this packet might encode a few extra 0 bits at the end; these are not part of the transmission and should be ignored.

Every packet begins with a standard header: the first three bits encode the packet version, and the next three bits encode the packet type id. These two values are numbers; all numbers encoded in any packet are represented as binary with the most significant bit first. For example, a version encoded as the binary sequence 100 represents the number 4.

Packets with type id 4 represent a literal value. Literal value packets encode a single binary number. To do this, the binary number is padded with leading zeroes until its length is a multiple of four bits, and then it is broken into groups of four bits. Each group is prefixed by a 1 bit except the last group, which is prefixed by a 0 bit. These groups of five bits immediately follow the packet header. For example, the hexadecimal string D2FE28 becomes:

110100101111111000101000
VVVTTTAAAAABBBBBCCCCC

Below each bit is a label indicating its purpose:

  • The three bits labeled V (110) are the packet version, 6.
  • The three bits labeled T (100) are the packet type id, 4, which means the packet is a literal value.
  • The five bits labeled A (10111) start with a 1 (not the last group, keep reading) and contain the first four bits of the number, 0111.
  • The five bits labeled B (11110) start with a 1 (not the last group, keep reading) and contain four more bits of the number, 1110.
  • The five bits labeled C (00101) start with a 0 (last group, end of packet) and contain the last four bits of the number, 0101.
  • The three unlabeled 0 bits at the end are extra due to the hexadecimal representation and should be ignored.

So, this packet represents a literal value with binary representation 011111100101, which is 2021 in decimal.

Every other type of packet (any packet with a type id other than 4) represent an operator that performs some calculation on one or more sub-packets contained within. Right now, the specific operations aren’t important; focus on parsing the hierarchy of sub-packets.

An operator packet contains one or more packets. To indicate which subsequent binary data represents its sub-packets, an operator packet can use one of two modes indicated by the bit immediately after the packet header; this is called the length type id:

  • If the length type id is 0, then the next 15 bits are a number that represents the total length in bits of the sub-packets contained by this packet.
  • If the length type id is 1, then the next 11 bits are a number that represents the number of sub-packets immediately contained by this packet.

Finally, after the length type id bit and the 15-bit or 11-bit field, the sub-packets appear.

For example, here is an operator packet (hexadecimal string 38006F45291200) with length type id 0 that contains two sub-packets:

00111000000000000110111101000101001010010001001000000000
VVVTTTILLLLLLLLLLLLLLLAAAAAAAAAAABBBBBBBBBBBBBBBB
  • The three bits labeled V (001) are the packet version, 1.

  • The three bits labeled T (110) are the packet type id, 6, which means the packet is an operator.

  • The bit labeled I (0) is the length type id, which indicates that the length is a 15-bit number representing the number of bits in the sub-packets.

  • The 15 bits labeled L (000000000011011) contain the length of the sub-packets in bits, 27.

  • The 11 bits labeled A contain the first sub-packet, a literal value representing the number 10.

  • The 16 bits labeled B contain the second sub-packet, a literal value representing the number 20.

    After reading 11 and 16 bits of sub-packet data, the total length indicated in L (27) is reached, and so parsing of this packet stops.

As another example, here is an operator packet (hexadecimal string EE00D40C823060) with length type id 1 that contains three sub-packets:

11101110000000001101010000001100100000100011000001100000
VVVTTTILLLLLLLLLLLAAAAAAAAAAABBBBBBBBBBBCCCCCCCCCCC
  • The three bits labeled V (111) are the packet version, 7.
  • The three bits labeled T (011) are the packet type id, 3, which means the packet is an operator.
  • The bit labeled I (1) is the length type id, which indicates that the length is a 11-bit number representing the number of sub-packets.
  • The 11 bits labeled L (00000000011) contain the number of sub-packets, 3.
  • The 11 bits labeled A contain the first sub-packet, a literal value representing the number 1.
  • The 11 bits labeled B contain the second sub-packet, a literal value representing the number 2.
  • The 11 bits labeled C contain the third sub-packet, a literal value representing the number 3.

After reading 3 complete sub-packets, the number of sub-packets indicated in L (3) is reached, and so parsing of this packet stops.

Here are a few more examples of hexadecimal-encoded transmissions:

  • 8A004A801A8002F478 represents an operator packet (version 4) which contains an operator packet (version 1) which contains an operator packet (version 5) which contains a literal value (version 6); this packet has a version sum of 16.
  • 620080001611562C8802118E34 represents an operator packet (version 3) which contains two sub-packets; each sub-packet is an operator packet that contains two literal values. This packet has a version sum of 12.
  • C0015000016115A2E0802F182340 has the same structure as the previous example, but the outermost packet uses a different length type id. This packet has a version sum of 23.
  • A0016C880162017C3686B18A3D4780 is an operator packet that contains an operator packet that contains an operator packet that contains five literal values; it has a version sum of 31.

Decode the structure of your hexadecimal-encoded bits transmission; what do you get if you add up the version numbers in all packets?

Here’s the provided input. It’s quite long so don’t try to scroll through:

Show input - day 16
420D50000B318100415919B24E72D6509AE67F87195A3CCC518CC01197D538C3E00BC9A349A09802D258CC16FC016100660DC4283200087C6485F1C8C015A00A5A5FB19C363F2FD8CE1B1B99DE81D00C9D3002100B58002AB5400D50038008DA2020A9C00F300248065A4016B4C00810028003D9600CA4C0084007B8400A0002AA6F68440274080331D20C4300004323CC32830200D42A85D1BE4F1C1440072E4630F2CCD624206008CC5B3E3AB00580010E8710862F0803D06E10C65000946442A631EC2EC30926A600D2A583653BE2D98BFE3820975787C600A680252AC9354FFE8CD23BE1E180253548D057002429794BD4759794BD4709AEDAFF0530043003511006E24C4685A00087C428811EE7FD8BBC1805D28C73C93262526CB36AC600DCB9649334A23900AA9257963FEF17D8028200DC608A71B80010A8D50C23E9802B37AA40EA801CD96EDA25B39593BB002A33F72D9AD959802525BCD6D36CC00D580010A86D1761F080311AE32C73500224E3BCD6D0AE5600024F92F654E5F6132B49979802129DC6593401591389CA62A4840101C9064A34499E4A1B180276008CDEFA0D37BE834F6F11B13900923E008CF6611BC65BCB2CB46B3A779D4C998A848DED30F0014288010A8451062B980311C21BC7C20042A2846782A400834916CFA5B8013374F6A33973C532F071000B565F47F15A526273BB129B6D9985680680111C728FD339BDBD8F03980230A6C0119774999A09001093E34600A60052B2B1D7EF60C958EBF7B074D7AF4928CD6BA5A40208E002F935E855AE68EE56F3ED271E6B44460084AB55002572F3289B78600A6647D1E5F6871BE5E598099006512207600BCDCBCFD23CE463678100467680D27BAE920804119DBFA96E05F00431269D255DDA528D83A577285B91BCCB4802AB95A5C9B001299793FCD24C5D600BC652523D82D3FCB56EF737F045008E0FCDC7DAE40B64F7F799F3981F2490

Here’s the solution to part 1:

using Pipe: @pipe
bits2int(bits) = parse(Int, join(string.(bits)), base = 2)

hex2bits(hex) = @pipe split(hex, "") .|> parse(Int, _, base=16) .|>
    digits(_, base=2, pad=4) .|> reverse |> vcat(_...) |> string.(_)

function decode_literal!(bits)
    literal_values = []
    while true
        chunk = splice!(bits, 1:5)
        literal_values = [literal_values; chunk[2:5]]
        first(chunk) == "0" ? break : continue
    end
end

function decode_bits!(bits, versions=0)
    if all(bits .== "0")
        return versions
    end
    version = splice!(bits, 1:3) |> bits2int
    ID = splice!(bits, 1:3) |> bits2int
    versions = versions + version
    if ID == 4
        decode_literal!(bits)
    else
        l_ID = popfirst!(bits)
        if l_ID == "0"
            subpacket_length = splice!(bits, 1:15) |> bits2int
            subpacket = splice!(bits, 1:subpacket_length)
            versions = decode_bits!(subpacket, versions)
        else
            n_packet = splice!(bits, 1:11) |> bits2int
            for i in 1:n_packet
                versions = decode_bits!(bits, versions)
            end
        end
    end
    versions = decode_bits!(bits, versions)
end

# Checking with the examples
# bits = hex2bits("D2FE28")
# bits = hex2bits("38006F45291200")
# bits = hex2bits("EE00D40C823060")
# bits = hex2bits("8A004A801A8002F478")
# bits = hex2bits("620080001611562C8802118E34")
# bits = hex2bits("C0015000016115A2E0802F182340")
# bits = hex2bits("A0016C880162017C3686B18A3D4780")
bits = hex2bits(input |> strip)

decode_bits!(bits) |> print

Part 2

Show challenge - day 16, part 2

Now that you have the structure of your transmission decoded, you can calculate the value of the expression it represents.

Literal values (type id 4) represent a single number as described above. The remaining type ids are more interesting:

  • Packets with type id 0 are sum packets - their value is the sum of the values of their sub-packets. If they only have a single sub-packet, their value is the value of the sub-packet.
  • Packets with type id 1 are product packets - their value is the result of multiplying together the values of their sub-packets. If they only have a single sub-packet, their value is the value of the sub-packet.
  • Packets with type id 2 are minimum packets - their value is the minimum of the values of their sub-packets.
  • Packets with type id 3 are maximum packets - their value is the maximum of the values of their sub-packets.
  • Packets with type id 5 are greater than packets - their value is 1 if the value of the first sub-packet is greater than the value of the second sub-packet; otherwise, their value is 0. These packets always have exactly two sub-packets.
  • Packets with type id 6 are less than packets - their value is 1 if the value of the first sub-packet is less than the value of the second sub-packet; otherwise, their value is 0. These packets always have exactly two sub-packets.
  • Packets with type id 7 are equal to packets - their value is 1 if the value of the first sub-packet is equal to the value of the second sub-packet; otherwise, their value is 0. These packets always have exactly two sub-packets.

Using these rules, you can now work out the value of the outermost packet in your bits transmission.

For example:

  • C200B40A82 finds the sum of 1 and 2, resulting in the value 3.
  • 04005AC33890 finds the product of 6 and 9, resulting in the value 54.
  • 880086C3E88112 finds the minimum of 7, 8, and 9, resulting in the value 7.
  • CE00C43D881120 finds the maximum of 7, 8, and 9, resulting in the value 9.
  • D8005AC2A8F0 produces 1, because 5 is less than 15.
  • F600BC2D8F produces 0, because 5 is not greater than 15.
  • 9C005AC2F8F0 produces 0, because 5 is not equal to 15.
  • 9C0141080250320F1802104A08 produces 1, because 1 + 3 = 2 * 2.

What do you get if you evaluate the expression represented by your hexadecimal-encoded bits transmission?

Here’s the solution to part 2:

using Pipe: @pipe
bits2int(bits) = parse(Int, join(string.(bits)), base = 2)

hex2bits(hex) = @pipe split(hex, "") .|> parse(Int, _, base=16) .|>
    digits(_, base=2, pad=4) .|> reverse |> vcat(_...) |> string.(_)

function decode_literal!(bits)
    literal_values = []
    while true
        chunk = splice!(bits, 1:5)
        literal_values = [literal_values; chunk[2:5]]
        first(chunk) == "0" ? break : continue
    end
    return string(bits2int(literal_values))
end

function decode_bits!(bits, operations="", stop=false)
    if all(bits .== "0")
        return replace(operations, r",[ ]+$"=>"")
    end
    version = splice!(bits, 1:3) |> bits2int
    ID = splice!(bits, 1:3) |> bits2int
    if ID == 4
        operations = operations * decode_literal!(bits) * ", "
    else
        if ID == 0
            operations = operations * "+("
        elseif ID == 1
            operations = operations * "*("
        elseif ID == 2
            operations = operations * "min("
        elseif ID == 3
            operations = operations * "max("
        elseif ID == 5
            operations = operations * ">("
        elseif ID == 6
            operations = operations * "<("
        elseif ID == 7
            operations = operations * "==("
        end
        l_ID = popfirst!(bits)
        if l_ID == "0"
            subpacket_length = splice!(bits, 1:15) |> bits2int
            subpacket = splice!(bits, 1:subpacket_length)
            operations = decode_bits!(subpacket, operations)
        else
            n_packet = splice!(bits, 1:11) |> bits2int
            for i in 1:n_packet
                operations = decode_bits!(bits, operations, true)
            end
        end
        operations = operations * "), "
    end
    if stop
        return operations
    else
        operations = decode_bits!(bits, operations)
    end
end

# Checking with the examples
# bits = hex2bits("C200B40A82")
# bits = hex2bits("04005AC33890")
# bits = hex2bits("880086C3E88112")
# bits = hex2bits("CE00C43D881120")
# bits = hex2bits("D8005AC2A8F0")
# bits = hex2bits("9C005AC2F8F0")
# bits = hex2bits("9C0141080250320F1802104A08")
bits = hex2bits(input |> strip)
out = decode_bits!(bits)
Meta.parse(out) |> eval |> print