DEV Community

Cover image for Advent of Code 2019 - Day 4
bretthancox
bretthancox

Posted on • Updated on

Advent of Code 2019 - Day 4

Introduction

Day 3 was a big spike in complexity for me. Not sure why - maybe it was meant to be harder, maybe my brain doesn't deal well with the type of problem it was. Fortunately, Day 4 made far more sense to me. I was able to break it down into much more logical steps, thus making my functions far better structured and the flow far cleaner.

Day 4.1

The challenge: given a range of six digit "passwords", count the number of "passwords" that, checking each number from left to right, (1) only equal the preceding number or increment it, and (2) have at least one pair of neighboring numbers that are equal.
An example that meets both definitions is 122345

My logic for this part of Day 4 was to start with the checks I needed to perform:

  • Does each individual number within the sequence equal or exceed its predecessor?
  • Is there at least one pair of equal, neighboring numbers within the input?

So, that's two functions!

(defn check_equal_incrementing_order
  "I check if the numbers are equal or increase across the number being tested"
  [vector_of_ints]
  (loop [vector_of_ints vector_of_ints
         final_state true]
    (if (or (= 1 (count vector_of_ints)) (false? final_state))
      final_state
      (recur 
       (rest vector_of_ints) 
       (if 
        (> (first vector_of_ints) (second vector_of_ints))
         false
         true)))))


(defn check_at_least_one_equal_pair
  "I check if at least one neighboring pair of numbers are equal"
  [vector_of_ints]
  (loop [vector_of_ints vector_of_ints
         final_state false]
    (if (or (= 1 (count vector_of_ints)) (true? final_state))
      final_state
      (recur 
       (rest vector_of_ints) 
       (if 
        (= (first vector_of_ints) (second vector_of_ints))
         true
         false)))))
Enter fullscreen mode Exit fullscreen mode

Next was the handling of the input. Specifically, splitting the initial number into individual integers. From my experience, strings are easier to split than integers, so my steps are:

  • Convert the integer to a string
  • Split that string into a vector of individual characters
  • Parse the vector of split strings into a vector of integers

Add one function to push the data through those steps and we have the following:

(defn convert_int_to_string
  "I convert an integer to a string"
  [integer]
  (str integer))


(defn make_vec_of_strings
  "I convert a string into a vector of individual characters"
  [string]
  (str/split string #""))


(defn make_substrings_into_ints
  "I convert the substrings into integers"
  [vec_of_strings]
  (into [] (map #(Integer/parseInt %) vec_of_strings)))


(defn produce_vector_of_ints
  "I push the initial number through the conversion steps"
  [number]
  (->> number
       (convert_int_to_string)
       (make_vec_of_strings)
       (make_substrings_into_ints)))
Enter fullscreen mode Exit fullscreen mode

The final step for determining if an input number is a valid password is to check that both of the earlier checks are true. So here's a function that determines that:

(defn check_both_checks_are_true
  "I perform the two checks and confirm that both return true"
  [vector_of_ints]
  (if (and (check_at_least_one_equal_pair vector_of_ints) (check_equal_incrementing_order vector_of_ints))
    true
    false))
Enter fullscreen mode Exit fullscreen mode

Last but not least, the command and control that takes in the start and stop values and runs each through the sequence. If check_both_checks_are_true returns true, increment the counter. At the end, the counter is the value to submit to pass Day 4.1:

(defn day4_1_command_and_control
  [start end]
  (loop [start start
         end end
         counter 0]
    (if (> start end)
      counter
      (if (true? (check_both_checks_are_true (produce_vector_of_ints start)))
        (recur 
         (inc start)
         end
         (inc counter))
        (recur
         (inc start)
         end
         counter)))))

(defn -main
  "I call the functions for the Advent of Code on the basis of which day(s) are added as arguments to the command line call"
  [& days]
  (loop [days days
         day (first days)]
    (if (empty? days)
      nil
      (do (cond

        ***snip***

              (= day "4") (do
                            (println "Day 4.1 - Number of combinations:" (day4_1_command_and_control day4_start day4_end)))
              :else (println "Day not completed yet"))
        (recur (rest days) (first (rest days))))))
        )
Enter fullscreen mode Exit fullscreen mode

Day 4.2

This part made me scratch my head for a moment. How to check if there are three or more neighboring numbers that are equal and then only discount the "password" if it doesn't also contain true matching pair of numbers?
A "password" that fails while meeting the criteria of part 1: 122234
A "password" that passes the new criteria: 122233
The latter password also has a 33 pair, so the 222 trio doesn't invalidate it.

My logic was to realize that the trio check was all I needed. I could find a matching pair, then check to see if the pair was isolated or part of a larger sequence.
Basically, in the working example above (122233) you would first find [2 2]. You then check the preceding numbers and the subsequent numbers and see if you find a trio that match. For [2 2], the preceding number gives you a trio of [1 2 2]. That is fine. For the subsequent number you get [2 2 2]. That is a fail.
Ignore the fact that you would find another [2 2] pair and move to the next pair in 122233. You get [3 3]. You can only look back as that is the end of the vector, giving you [2 3 3]. That is fine, meaning this "password" can be counted.

I wrote two functions to do this check:

(defn check_if_trio_equal
  "I take a trio of ints and check if they are equal or not, returning true if they are."
  [trio_of_ints]
  (let [[a b c] trio_of_ints]
    (if (= a b c)
      true
      false)))

(defn check_if_more_than_two_identical_values_neighboring
  "I check if at least one neighboring pair of numbers are equal without being part of larger group, returning true if there is one isolated pair"
  [vector_of_ints]
  (loop [posa 0]
    (if (= (count vector_of_ints) (+ posa 1))
      false
      (let [pair_to_check [(nth vector_of_ints posa) 
                           (nth vector_of_ints (+ 1 posa))]
            first_trio (if 
                        (<= (count vector_of_ints) (+ posa 2)) ;; Checks if index of posa+2 would be out of range...
                         [1 2 3] ;;...and if so, uses a generic vector that won't fail the check
                         [(nth vector_of_ints posa) (nth vector_of_ints (+ posa 1)) (nth vector_of_ints (+ posa 2))])
            second_trio (if 
                         (= posa 0) 
                          first_trio ;; If the backward looking vector would start at index -1, just run with the first vector
                          [(nth vector_of_ints (- posa 1)) (nth vector_of_ints posa) (nth vector_of_ints (+ posa 1))])]
        (if (and 
             (= (first pair_to_check) (second pair_to_check)) ;; The identified pair match...
             (false? (check_if_trio_equal first_trio)) ;; ..and the first trio does not match...
             (false? (check_if_trio_equal second_trio))) ;; ...and the second trio does not match. This means it is a true pair.
          true
          (recur (inc posa)))))))
Enter fullscreen mode Exit fullscreen mode

The second function gets complicated because I need to prevent my trio selection being outside the index range for the vector_of_ints. I catch situations that would result in index errors by using a static [1 2 3] true trio vector for first_trio and reusing first_trio for second_trio when that trio would be out of range.
If an isolated pair is found, the rest of the "password" is irrelevant, so it returns true. Otherwise it keeps going until it finds true, or it reaches the index limit and returns false.

Then I have the aggregator function, same as in part 1, that compiles the three checks, as it is now, and returns true or false:

(defn check_three_checks_are_true
  "I perform the three checks and confirm that both return true"
  [vector_of_ints]
  (if (and
       (check_at_least_one_equal_pair vector_of_ints)
       (check_equal_incrementing_order vector_of_ints)
       (check_if_more_than_two_identical_values_neighboring vector_of_ints))
    true
    false))
Enter fullscreen mode Exit fullscreen mode

Last, but not least, the command control for part 2 and calling it. Command and control is no different to part 1, except it calls the three check function instead of the two check:

(defn day4_2_command_and_control
  [start end]
  (loop [start start
         end end
         counter 0]
    (if (> start end)
      counter
      (if (true? (check_three_checks_are_true (produce_vector_of_ints start)))
        (recur
         (inc start)
         end
         (inc counter))
        (recur
         (inc start)
         end
         counter)))))


(defn -main
  "I call the functions for the Advent of Code on the basis of which day(s) are added as arguments to the command line call"
  [& days]
  (loop [days days
         day (first days)]
    (if (empty? days)
      nil
      (do
        (cond 

        ***snip***

              (= day "4") (do
                            (println "Day 4.1 - Number of combinations:" (day4_1_command_and_control day4_start day4_end))
                            (println "Day 4.2 - No more than two identical neighboring values:" (day4_2_command_and_control day4_start day4_end)))
              :else (println "Day not completed yet"))
        (recur (rest days) (first (rest days))))))
        )
Enter fullscreen mode Exit fullscreen mode

Testing

Since I determined my function logic first, I was able to write much better tests for Day 4. Part 1 is especially well covered.
I realized afterward that I had used primarily five digit numbers, but the logic of the challenge doesn't actually depend on six digits, so any number long enough to display the characteristics being tested would suffice.

(ns advent.core-test
  (:require [clojure.test :refer :all]
            [advent.core :refer :all]))


(deftest day4_order_equal_inc
  (testing "Confirming that the check for equal or incrementing order returns the correct result"
    (is (true? (check_equal_incrementing_order [1 2 2 4 5])))
    (is (false? (check_equal_incrementing_order [1 2 2 1 5])))))

(deftest day4_check_one_pair_equal
  (testing "Confirming that checking for one equal pair produces the correct result"
    (is (true? (check_at_least_one_equal_pair [1 2 2 4 5])))
    (is (false? (check_at_least_one_equal_pair [1 2 3 4 5])))))

(deftest day4_check_int_to_string
  (testing "Does the int get appropriately converted"
    (is (= (convert_int_to_string 12245) "12245"))
    (is (= (convert_int_to_string 12345) "12345"))))

(deftest day4_split_string
  (testing "Does the string get split correctly"
    (is (= (make_vec_of_strings "12245") ["1" "2" "2" "4" "5"]))))

(deftest day4_produce_ints
  (testing "Do the substrings get converted to integers"
    (is (= (make_substrings_into_ints ["1" "2" "2" "4" "5"]) [1 2 2 4 5]))))

(deftest day4_ensure_both_checks_are_true
  (testing "Confirm that when both checks are true, the result is true"
    (is (true? (check_both_checks_are_true [1 2 2 4 5])))
    (is (false? (check_both_checks_are_true [1 2 3 4 5])))
    (is (false? (check_both_checks_are_true [1 2 2 3 2])))))

(deftest day4_end_to_end_int_to_vecint
  (testing "Confirm that the end to end process of building the vec of ints works"
    (is (= (produce_vector_of_ints 12245) [1 2 2 4 5]))))

(deftest day4_1_test
  (testing "Confirm the results are correct"
    (is (= (day4_1_command_and_control 122342 122348) 5))
    (is (= (day4_1_command_and_control 112224 112233) 7))))


(deftest day4_2_logic_check
  (testing "Checking the logic of picking a password with only two neighboring values"
    (is (true? (check_if_more_than_two_identical_values_neighboring [1 2 2 4 5])))
    (is (false? (check_if_more_than_two_identical_values_neighboring [1 2 2 2 5])))))

(deftest day4_2_test
  (testing "Confirm the results are correct"
    (is (= (day4_2_command_and_control 122342 122348) 5))
    (is (= (day4_2_command_and_control 112224 112233) 7))))
Enter fullscreen mode Exit fullscreen mode

Oldest comments (2)

Collapse
 
erikpischel profile image
Erik Pischel

I like your "first person style" of documenting functions.

Also I like how functions in clojure are often quite short

Collapse
 
bretthancox profile image
bretthancox

Thank you. I'm an infrequent coder, so I try to write comments that help future me get back up to speed quickly.

I definitely don't write idiomatic Clojure, but I do try to write functions that have limited output variations, which is part of it 🙃