## Dissecting an Approach to Solving a Coding Challenge I Encountered in the Wild

What follows is a coding challenge that I recently encountered. During the challenge, I leveraged some Ruby “magic” to make what I consider an elegant solution. What follows is a step-through of the coding challenge iterative tasks.

For those impatient, skip ahead to Task 3.

## Task 1

Write a function when given an integer returns the following:

- For number divisible by 5 or that include the numeral 5, render “cats”.
- For number divisible by 7 or that include the numeral 7, render “boots”.
- For number that are both of the above, render “boots and cats”.

**Note:** If you are doing coding challenges, get comfortable with modulo arithmetic; that stuff shows up a lot.

Oddly, I rarely use it in my day to day coding. So I wonder why it keeps showing up in interview coding challenges?

## Task 2

Starting from 1, generate a list of the first number that matches each of the permutations of the above. (e.g. A: contains 5, but not divisible by 5, nor 7, nor contains 7, B: contains 5, and is divisible by 5, but not 7 nor contains 7).

For this to be feasible, you need to know how many combinations are possible.

Note, we are not worried about printing “cats” and “boots” at this point. Instead we’re generating a list of numbers.

## Task 3

Generalize the above so that users can provide arbitrary rules for task 2.

### Solution

```
class FirstMatchGenerator
def initialize(**rules)
@rules = rules
@number_of_permutations = 2 ** rules.length
@tester = Struct.new(*rules.keys, keyword_init: true)
end
def call
results = {}
integer = 0
continue = true
while continue
integer += 1
candidate = candidate_for(integer: integer)
next if results.include?(candidate)
results[candidate] = integer
continue = results.size < @number_of_permutations
end
results.values
end
private
def candidate_for(integer:)
conditions = @rules.each_with_object({}) do |(name, func), cond|
cond[name] = func.call(integer)
end
candidate = @tester.new(**conditions)
end
end
puts FirstMatchGenerator.new(
by_5: ->(i) { i % 5 == 0 },
by_7: ->(i) { i % 7 == 0 }
).call.inspect
# => [1, 5, 7, 35]
```

#### Dissecting the Solution

The above solution makes use of Ruby’s keyword args, splat and double splat operator, and the `Struct`

for equality tests.

Let’s look at the instantiation of the class:

```
FirstMatchGenerator.new(
by_5: ->(i) { i % 5 == 0 },
by_7: ->(i) { i % 7 == 0 }
)
```

In the above, we’re passing the keyword keys of `:by_5`

and `:by_7`

. The values of those keys are `lambdas`

(e.g. `->(i) { i % 5 == 0 },`

and `->(i) { i % 7 == 0 }`

). If it helps, think about us passing a Ruby `Hash`

as the parameter.

Now let’s example the `FirstMatchGenerator#initialize`

method:

```
def initialize(**rules)
@rules = rules
@number_of_permutations = 2 ** rules.length
@tester = Struct.new(*rules.keys, keyword_init: true)
end
```

In the above, the `**rules`

means we accept arbitrarily named keyword args (e.g. `:by_5`

and `:by_7`

). In fact, this is treated as `Hash`

object in the `initialize`

method.

The number of permutations is two raised to the power of the number of rules. In the example that would be 2^{2} or 4.

And last the `@tester`

is a dynamically created `Struct`

object with attributes that are the given named args via the `splat`

operator (e.g. `*rules.keys`

). And with the `keyword_init: true`

we can instantiate this `Struct`

with keyword args.

The reason for using the `Struct`

is that it implements an equality operator. For two `Struct`

’s, if all of their attributes are identical then the two `Struct`

objects are considered to be “equal” (e.g. `a == b`

).

Now to the `FirstMatchGenerator#call`

method:

```
def call
results = {}
integer = 0
continue = true
while continue
integer += 1
candidate = candidate_for(integer: integer)
next if results.include?(candidate)
results[candidate] = integer
continue = results.size < @number_of_permutations
end
results.values
end
```

There’s quite a bit going on. Before the `while`

loop is a setup of variables. Within the while loop we:

- create a candidate (more on that in a bit)
- check if we already have encountered that candidate (the
`Hash#include?`

method checks the equality of the candidate) - remember a new candidate
- break if we’ve encountered all candidates

After the `while`

loop we return the list of integers.

In my original implementation I did not have a private method but instead in-lined the results.

And last, the `FirstMatchGenerator#candidate_for`

method:

```
def candidate_for(integer:)
conditions = @rules.each_with_object({}) do |(name, func), cond|
cond[name] = func.call(integer)
end
candidate = @tester.new(**conditions)
end
```

The `@rules`

are a `Hash`

with keys that are symbols and values that are `lambdas`

(e.g. `{ by_5: ->(i) { i % 5 == 0 }, by_7: ->(i) { i % 7 == 0 } }`

).

Using those rules, the above code calls each of the `lambdas`

to determine the result.

- Given an
`integer`

of 5, we’ll have a`conditions`

`Hash`

of`{ by_5: true, by_7: false }`

. - Given an
`integer`

of 7, we’ll have a`conditions`

`Hash`

of`{ by_5: false, by_7: true }`

. - Given an
`integer`

of 35, we’ll have a`conditions`

`Hash`

of`{ by_5: true, by_7: true }`

.

We then use that `conditions`

`Hash`

to instantiate our `@tester`

`Struct`

; which provides us with the nifty equality test.

### Conclusion

Did I need the dynamic `Struct`

assignment? No. I could’ve used `Hash`

equality tests.

But in my experience an over-reliance on the `Hash`

object creates later problems. Why? Because a `Hash`

is a schema-less data store. Whereas a `Struct`

has a schema. And can provide more robust debugging information.

My hope in this walk through is to highlight some of the interplay of Ruby idioms.

For myself, I’m long a fan of keyword args and ever growing fan of the humble `Struct`

object.

## Top comments (2)

Great approach, i rly like how you use splat arguments to count the rules.

For permutation, i think ruby have method for that.

I always wonder the same thing.

This is a neat coding problem though, I will try and solve it later on and see if we match approaches.