Advent of Code 2022: Day 1

In this task, we must read a text file consisting of groups of numbers: each number is on its own line, and groups separated with a blank line.

In Icon, an open file can be read using a standard every ... do idiom:

  file := open(filename) | stop("Cannot open ", filename)      # <1>
  every line := !file do {                                     # <2>
    # process the line
  }
  close(file)                                                  # <3>
  1. Opens the given filename for reading. If the call to open fails, then the program is stopped with the given error message.
  2. This loop iterates through every line in the file.
  3. Finally, the file must be closed.

The numbers in each group must be totalled. An array is used to store the totals for each group, creating a new group for each blank line and adding the number into the last group if the line contains a number.

  groups := [0]                                               # <1>
  file := open(filename) | stop("Cannot open ", filename)
  every line := !file do {
    if line == "" then
      put(groups, 0)                                          # <2>
    else
      groups[*groups] +:= line                                # <3>
  }
  close(file)
  1. Each group is a number, stored in a list, starting with one entry for the first group.
  2. When the line is empty, extend the list to start a new group.
  3. When the line is not empty, assume it contains a number to add to the last element in the list.

Notice that:

Task 1: Maximum

Find the maximum element in the list. This can be done using iteration and comparison:

max_element := 0
every item := !groups do {
  if item > max_element then
    max_element := item
}

Unicon has an operator max, and generators can be embedded in expressions, so an equivalent version is:

max_element := 0
every max_element := max(max_element, !groups)

Even more concisely:

every (max_element := 0) := max(max_element, !groups)

Task 2: Sum of Top Three

Find the top-three items in the list, and return their sum. A simple but costly solution uses sort:

top_3 := sort(groups)[-3:0]            # <1>
sum_top_3 := 0
every sum_top_3 +:= !top_3             # <2>
  1. After sorting, take the last three items in the list: 0 is the index of the end of the list, and -3 means three items back from the end.
  2. Adds up all the numbers, in a similar style to above.

A more efficient way to find the top three scans the list once, retaining the top three items. The following solution assumes the numbers in the list are positive values, as in the problem definition.

top_3 := [0, 0, 0]                           # <1>
every item := !groups do {                   # <2>
  if item > top_3[1] then                    # <3>
    top_3 := [item, top_3[1], top_3[2]]      # <4>
  else if item > top_3[2] then
    top_3 := [top_3[1], item, top_3[2]]
  else if item > top_3[3] then
    top_3 := [top_3[1], top_3[2], item]
}
  1. Starts with the top three assumed to be 0.
  2. Works through each item in turn.
  3. The if conditions check if the item falls in the first, second or third position of the top three ...
  4. ... and is put into place, accordingly.

Final Program

procedure main(inputs)
  local filename

  if *inputs = 1 then {
    filename := inputs[1]

    # Part 1 solution
    write("Part 1: ", part_1(filename))
    # Part 2 solution
    write("Part 2: ", part_2(filename))

  } else {
    write("Provide a filename of data")
  }
end

procedure part_1(filename)
  local groups, max_element

  groups := read_data(filename)
  every item := !groups do {
    if item > max_element then
      max_element := item
  }

  return max_element
end

procedure part_2(filename)
  local groups, item, top_3

  groups := read_data(filename)

  top_3 := [0, 0, 0]
  every item := !groups do {
    if item > top_3[1] then
      top_3 := [item, top_3[1], top_3[2]]
    else if item > top_3[2] then
      top_3 := [top_3[1], item, top_3[2]]
    else if item > top_3[3] then
      top_3 := [top_3[1], top_3[2], item]
  }

  return top_3[1] + top_3[2] + top_3[3]
end

procedure read_data(filename)
  local file, groups, line

  file := open(filename) | stop("Cannot open ", filename)
  groups := [0]
  every line := !file do {
    if line == "" then
      put(groups, 0)
    else
      groups[*groups] +:= line
  }

  close(file)

  return groups
end

Functional Solution

Icon supports one aspect of higher-order functions, in that procedures can be passed as arguments to other procedures. This allows us to write more functional-style code. For instance, the solution above uses three every expressions to collect information generated from one source. We can capture this pattern in a functional way, by writing a fold (or reduce) procedure:

procedure fold(generator, base, fn)
  local result

  result := base
  every result := fn(result, !generator)
  return result
end

In this way, a list can be summed using:

fold(lst, 0, "+") # infix operators are given as strings

or the maximum item found using (define max if not using Unicon):

fold(lst, 0, max)

Here is the complete program rewritten to use fold:

procedure main(inputs)
  local file, filename, groups

  if *inputs = 1 then {
    filename := inputs[1]

    file := open(filename) | stop("Cannot open ", filename)
    groups := fold(file, [0], process_line)
    close(file)

    # Part 1 solution
    write("Part 1: ", fold(groups, 0, max))
    # Part 2 solution
    write("Part 2: ", fold(fold(groups, [0, 0, 0], top_three), 0, "+"))

  } else {
    write("Provide a filename of data")
  }
end

procedure top_three(top_3, item)
  if item > top_3[1] then
    return [item, top_3[1], top_3[2]]
  else if item > top_3[2] then
    return [top_3[1], item, top_3[2]]
  else if item > top_3[3] then
    return [top_3[1], top_3[2], item]
  else
    return top_3
end

procedure process_line(groups, line)
  if line == "" then
    put(groups, 0)
  else
    groups[*groups] +:= line
end

procedure fold(generator, base, fn)
  local result

  result := base
  every result := fn(result, !generator)
  return result
end

# if not using Unicon, define `max`
procedure max(a, b)
  if a > b then
    return a
  else
    return b
end

Page from Peter's Scrapbook, output from a VimWiki on 2024-01-29.