I just realized that there are multiple parts to the same day. Time to revisit Day 1 :D


The task (part 1)

Today’s task is to determine whether the reports provided as input are safe or unsafe.

Here’s the example data:

7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9

Each line represents a single report, and each number in the report represents a “level.”

A report is considered safe if:

  1. The levels are either all increasing or all decreasing.
  2. The difference between two adjacent levels is at least 1 and at most 3.

The result should be the count of all safe reports.


Write-up

Each line in the input represents a single report. So, the first step is to read the file and split the string by newline characters. Since the levels need to be numbers, I’ll also split each report by whitespace and parse the values into integers:

[[7, 6, 4, 2, 1]
 [1, 2, 7, 8, 9]
 [9, 7, 6, 2, 1]
 [1, 3, 2, 4, 5]
 [8, 6, 4, 4, 1]
 [1, 3, 6, 7, 9]]

The result is an array of reports, where each report is an array of integers. The goal is to count how many of these reports are safe. Let’s initialize a safe_count variable at 0.


The plan

The initial idea was straightforward: for each report, check if the levels are increasing or decreasing and whether the differences between adjacent levels fall within the range of 1 to 3.

Initially, this required two loops per report—unnecessary and inefficient.

Instead, I realized I only needed to check the first two levels of each report to determine whether it was increasing or decreasing.

I introduced a check_direction method to determine the trend (increasing or decreasing):

def check_direction(a,b)
  return :decreasing if a > b

  :increasing
end

With this method, I could determine the direction of the report from its first two levels. Next, I wrote the check_decreasing method to validate whether a report was both decreasing and safe:

def check_decreasing(array)
  array.each_cons(2) do |a, b|
    return false if a < b || !(a - b).abs.between?(1,3)
  end

  true
end

If any pair of levels fails the conditions (not decreasing or difference out of range), the method returns false early. Otherwise, it completes the loop and returns true.

The check_increasing method was almost identical, with the only difference being the comparison (a > b).


A cleaner approach

It struck me that both check_decreasing and check_increasing were nearly identical. To simplify, I merged them into a single validate method.

Instead of storing the method references in a hash, I stored the string representations of the comparison operators:

actions = {
  decreasing: "<",
  increasing: ">"
}

Here’s the merged validate method:

def validate(array, action)
  array.each_cons(2) do |a, b|
    return false if a.send(action, b) || !(a - b).abs.between?(1, 3)
  end

  true
end

Now, I could dynamically determine the direction of the report and pass the correct operator to validate.

Final implementation

input = File.read("puzzle_input.txt")  
# input = File.read "./example.txt"

reports = input.split("\n").map { |i| i.split.map(&:to_i) }

safe_count = 0

def validate(array, action)
  array.each_cons(2) do |a, b|
    return false if a.send(action.to_sym, b) || !(a - b).abs.between?(1, 3)
  end

  true
end

reports.each do |report|
  action = report[0] > report[1] ? "<" : ">"
  safe_count += 1 if validate(report, action)
end

puts safe_count