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)))))
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)))
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))
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))))))
)
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)))))))
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))
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))))))
)
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))))
Top comments (2)
I like your "first person style" of documenting functions.
Also I like how functions in clojure are often quite short
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 🙃