Advent of Ruby 3.0 - Day 06 - Custom Customs

baweaver

Brandon Weaver

Posted on December 30, 2020

Advent of Ruby 3.0 - Day 06 - Custom Customs

Ruby 3.0 was just released, so it's that merry time of year where we take time to experiment and see what all fun new features are out there.

This series of posts will take a look into several Ruby 2.7 and 3.0 features and how one might use them to solve Advent of Code problems. The solutions themselves are not meant to be the most efficient as much as to demonstrate novel uses of features.

I'll be breaking these up by individual days as each of these posts will get progressively longer and more involved, and I'm not keen on giving 10+ minute posts very often.

With that said, let's get into it!

<< Previous | Next >>

Day 06 - Part 01 - Custom Customs

Day six was another fairly easy one. For part one we're not using anything particularly new. Our task this time is to find "yes" answers and count them, where "yes" is signified by a letter. Records are separated by a blank line, but there may be one or more response in each record separated by newlines:

abcx
abcy
abcz
Enter fullscreen mode Exit fullscreen mode

In this case we have 6 unique yes answers from three people, abcxyz. The first part of the problem focuses on this, so let's take a look at the solution:

puts File
  .read(ARGV[0])
  .split(/\n\n/)
  .map { _1.gsub(/\W/, '').chars.uniq.size }
  .sum
Enter fullscreen mode Exit fullscreen mode

gsub on Whitespace

The first new line we see is in the map function:

.map { _1.gsub(/\W/, '').chars.uniq.size }
Enter fullscreen mode Exit fullscreen mode

\W stands for any whitespace, newline or space, meaning that input above goes from this:

abcx
abcy
abcz
Enter fullscreen mode Exit fullscreen mode

...to this:

abcxabcyabcz
Enter fullscreen mode Exit fullscreen mode

uniq Characters

The next part of this solution uses chars, which I believe was introduced in 2.6, to divide the String into its individual characters:

'abcxabcyabcz'.chars
# => ["a", "b", "c", "x", "a", "b", "c", "y", "a", "b", "c", "z"]
Enter fullscreen mode Exit fullscreen mode

After this, we want to know how many unique yes answers we have, so uniq is quite useful here:

'abcxabcyabcz'.chars.uniq
# => ["a", "b", "c", "x", "y", "z"]
Enter fullscreen mode Exit fullscreen mode

...and then all that is left is to find out how many of them there were with size:

'abcxabcyabcz'.chars.uniq.size
# => 6
Enter fullscreen mode Exit fullscreen mode

sumthing to Finish

Then the last part is to get the sum of all yes answers from all of the reports, which we can do with sum:

.map { _1.gsub(/\W/, '').chars.uniq.size }
.sum
Enter fullscreen mode Exit fullscreen mode

...and the first part of our problem is done.

Day 06 - Part 02 - This is Exhaustive

The second part makes things a bit harder: It's no longer that anyone answered yes to a question, but that everyone has. That means for every person who answered we need to find answers they all answered yes to, and that means an intersection.

Let's take a look at the solution:

def answer_intersection(answers) =
  answers
    .lines
    .map { _1.chomp.chars }
    .reduce(&:intersection)
    .size

puts File
  .read(ARGV[0])
  .split(/\n\n/)
  .map { answer_intersection(_1) }
  .sum
Enter fullscreen mode Exit fullscreen mode

chomp the chars

The first thing we want to do in our intersecting answers is to clean up the lines into a format we can use. Normally one would think just chars would work great, but that now means there's a newline in every single answer that counts as a definitive yes, so we want to chomp that off the end so it doesn't throw our count.

reduce Quick Lesson

Now this, this is an incredibly succinct line doing a lot of stuff. For those who don't understand reduce this is a trip, so let's start with a quick lesson on how it works.

Addition is the easiest reference, and before sum we used to do this:

[1, 2, 3].reduce(0, :+)
# => 6
Enter fullscreen mode Exit fullscreen mode

...which expands to this if we use the more explicit form:

[1, 2, 3].reduce(0) { |sum, v| sum + v }
# => 6
Enter fullscreen mode Exit fullscreen mode

reduce is often also called foldLeft in other languages, in that we're folding values to the left starting with an initial value. In this case, 0.

Why 0? Because it's a nice default for sums, if you add it to any number you get back that same number, and if there are no numbers you get back 0 which seems perfectly reasonable to me.

Now on to the block function, which takes two arguments sum and v. sum is often called an accumulator (a) or memo (m) depending on which tutorial.

sum, in this case, starts with an initial value of 0 given to reduce(0). v is an iterator, or every value in the collection we're reducing.

This particular code goes from this:

[1, 2, 3].reduce(0) { |sum, v| sum + v }
# => 6
Enter fullscreen mode Exit fullscreen mode

...to this if we got the effective representation:

0 + 1 + 2 + 3
# => 6
Enter fullscreen mode Exit fullscreen mode

Let's take a quick look at how values go through reduce. You see, sum is the result of the last iteration, and the values flowing through it would look like this:

[1, 2, 3].reduce(0) { |sum, v|
  p(sum: sum, v: v)
  sum + v
}

# {:sum=>0, :v=>1}
# {:sum=>1, :v=>2}
# {:sum=>3, :v=>3}
# => 6
Enter fullscreen mode Exit fullscreen mode

In the first iteration the new sum becomes 1 (0 + 1), the second 3 (1 + 2), and the last it becomes 6 (3 + 3) which is our final result. If we used parens it'd look something like this too:

(((0 + 1) + 2) + 3)
Enter fullscreen mode Exit fullscreen mode

If you want a more detailed explanation of reduce you should check out Reducing Enumerable to learn more.

So, intersection then?

Let's take a look back at that code for reduce then:

.reduce(&:intersection)
Enter fullscreen mode Exit fullscreen mode

There's no initial value, which means that it'll use the first item of the collection it's reducing:

record = [
  ['a', 'b', 'c', 'x'],
  ['a', 'b', 'c', 'y'],
  ['a', 'b', 'c', 'z']
]
Enter fullscreen mode Exit fullscreen mode

If reduce intersperses + when done with + it'd do the same with intersect. Consider:

a, b, c = record
a.intersection(b).intersection(c)
# => ["a", "b", "c"]
Enter fullscreen mode Exit fullscreen mode

Granted we can use & which does the same thing, but I prefer more readable methods when possible.

That's about the size of things

Now that we have all the intersecting answers where everyone said yes we can get how many of them there are with size, and in our main function we can get the sum of all of them to get our final answer.

Wrapping Up Day 06

That about wraps up day six, we'll be continuing to work through each of these problems and exploring their solutions and methodology over the next few days and weeks.

If you want to find all of the original solutions, check out the Github repo with fully commented solutions.

<< Previous | Next >>

💖 💪 🙅 🚩
baweaver
Brandon Weaver

Posted on December 30, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related