Advent of Code 2022: Day 4

This task is interesting as we introduce string-scanning, one of the unique elements of Icon/Unicon.

Specifically, we have to decompose strings such as: "1-2,3-4", identifying the four numbers "1", "2", "3" and "4".

String scanning works by moving through a scanned string, returning substrings which match given sets of characters. The tab procedure is often used to make this movement: it can move "until" a character in a given set is found, or "while" the current character is in the given set.

Consider the following, which attempts to find the four numbers for our two ranges in a string:

    line ? {                                # <1>
      tab(upto(&digits))                    # <2>
      start_1 := tab(many(&digits))         # <3>
      tab(upto(&digits))
      end_1 := tab(many(&digits))
      tab(upto(&digits))
      start_2 := tab(many(&digits))
      tab(upto(&digits))
      end_2 := tab(many(&digits))
    }

  1. Starts scanning the string in line.
  2. Moves "until" it reaches a digit.
  3. Moves "while" it finds a digit, returning the sequence of found digits.

Another option for tab is to "match" a given sequence: tab(match(",")) will only move forward if the next character is a comma. As this is a frequently used operation, there is a shorthand form: =(","). We can use this to make our line scanner recognise the grammar of the task input: the range limits are separated by hyphens, and the two ranges by a comma.

    line ? { 
      start_1 := tab(many(&digits))
      =("-")
      end_1 := tab(many(&digits))
      =(",")
      start_2 := tab(many(&digits))
      =("-")
      end_2 := tab(many(&digits))
    }

To make the components easy to handle, we use a record to store each set of pairs in four separate fields:

record assignment_pair(start_1, end_1, start_2, end_2)

The two tasks merely ask us to count whether the two ranges overlap or not, so we define two procedures, one to test if one range contains the other and another to test if they overlap. A simple loop counts those assignment_pair instances which match the given condition.

Final Program

procedure main(inputs)
  local filename

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

    # Part 1 solution
    write("Part 1 count is: ", count_if(filename, contains))      # <1>
    # Part 2 solution
    write("Part 2 score is: ", count_if(filename, overlap))
  } else {
    write("Provide a filename of data")
  }
end

record assignment_pair(start_1, end_1, start_2, end_2)

procedure line2assignment(line)
  local start_1, end_1, start_2, end_2

  line ? { 
    start_1 := tab(many(&digits))
    =("-")
    end_1 := tab(many(&digits))
    =(",")
    start_2 := tab(many(&digits))
    =("-")
    end_2 := tab(many(&digits))

    return assignment_pair(start_1, end_1, start_2, end_2)
  }
  stop("Invalid line representation")
end

procedure contains(assignment)
  return assignment.start_1 <= assignment.start_2 <= assignment.end_2 <= assignment.end_1 | assignment.start_2 <= assignment.start_1 <= assignment.end_1 <= assignment.end_2
end

procedure overlap(assignment)
  return not (assignment.end_1 < assignment.start_2 | assignment.end_2 < assignment.start_1)
end

procedure count_if(filename, condition)
  local count, file, assignment

  count := 0

  file := open(filename, "r") | stop ("Cannot open ", filename)
  every assignment := line2assignment(!file) do {                 # <2>
    if condition(assignment) then                                 # <3>
      count +:= 1
  }
  close(file)

  return count
end

  1. The two tasks are similar, differing just on the condition with which to count.
  2. Loop through the file, parsing each line into an assignment_pair instance.
  3. Count 1 if the given condition applies.

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