Elixircism 02

·

8 min read

Elixircism 02 - Secret Handshake

Welcome back. In this episode we need to crack the secret handshake given to us by this definition

You and your fellow cohort of those in the "know" when it comes to binary decide to come up with a secret "handshake".

1 = wink 10 = double blink 100 = close your eyes 1000 = jump

10000 = Reverse the order of the operations in the secret handshake.

Given a decimal number, convert it to the appropriate sequence of events for a secret handshake.

Here's a couple of examples:

Given the input 3, the function would return the array ["wink", "double blink"] because 3 is 11 in binary.

We're given a hint to use Bitwise so let's not look a gift horse in the mouth, and make use of that. Basically it allows us to do bitwise operations like you would find in other languages like C. For example:

iex(1)> use Bitwise
Bitwise
iex(2)> 5 &&& 1
1
iex(3)> 5 &&& 2
0

Here we are performing an and operation on 5 which is 101 in binary, it's that simple.

Now we know our brief, and we need to get started. Our input is a base 10 number, which can have up to 5 bits set: 11111 == 31 and each set bit is a part of the secret handshake.

Out first thought is that we need to iterate over each set bit in the number provided. Luckily for us, Elixir has a handy function for just that task:

digits(integer, base \ 10)

Returns the ordered digits for the given integer.

An optional base value may be provided representing the radix for the returned digits. This one must be an integer >= 2.

This is ideal as it returns the digits for whatever base we desire, which in our case is 2. So that's the first step done, but as we need to process our bits from lowest to highest, we also need to reverse it. So our first steps are

Integer.digits(code, 2)
|> Enum.reverse

Next we need to figure out how we map the set bit to our actions. Well the clue is in the action: map - let's create a map that will allow us to lookup an action string.

@message %{0 => "wink", 1 => "double blink", 2 => "close your eyes", 3 => "jump"}

This maps the index of the set bit to an action. If the bit at position 0 is set, we get a wink for example. In order for us to be able to do that we need to know the index of the bits as we iterate over them. This is easily done via

> with_index(enumerable, offset \\ 0)

> Returns the enumerable with each element wrapped in a tuple alongside its index.

Excellent. So let's see where we are now:

Integer.digits(code, 2)
|> Enum.reverse()
|> Enum.with_index()

iex(3)> Integer.digits(5, 2) |> Enum.reverse |> Enum.with_index
[{1, 0}, {0, 1}, {1, 2}]

Our result is now a list of tuples with the first element of the tuple being the bit, and the second the index. All we need to do now is lookup the action in our map if the bit is 1. Once again we can make use of the Enum.map function to accomplish this

Integer.digits(code, 2)
|> Enum.reverse()
|> Enum.with_index()
|> Enum.map(fn {v, i} -> if v == 1 do @message[i] end end )

What happens here is that for each tuple in the list, we match the value (v) and index (i) and if the value is 1, we lookup the action in our @messages map and return it. It's a lot more clear when we run it

iex(4)> message = %{0 => "wink", 1 => "double blink", 2 => "close your eyes", 3 => "jump"}
%{0 => "wink", 1 => "double blink", 2 => "close your eyes", 3 => "jump"}
iex(5)> Integer.digits(5, 2) |> Enum.reverse |> Enum.with_index |> Enum.map(fn {v, i} -> if v == 1 do message[i] end end )
["wink", nil, "close your eyes"]

Almost perfect, except for that rogue nil which isn't any use to us. That appears when the bit isn't set and there's nothing returned inside the map function. So what we need to do is filter out any nil values. And as luck would have it, Enum provides us with a filter function which works as you'd expect:

filter(enumerable, fun)

Filters the enumerable, i.e. returns only those elements for which fun returns a truthy value.

Thus making our code look like this

result = Integer.digits(code, 2)
|> Enum.reverse
|> Enum.with_index
|> Enum.map(fn {v, i} -> if v == 1 do @message[i] end end )
|> Enum.filter(fn x -> x != nil end)

iex(6)> Integer.digits(5, 2) |> Enum.reverse |> Enum.with_index |> Enum.map(fn {v, i} -> if v == 1 do message[i] end end ) |>  Enum.filter(fn x -> x != nil end)
["wink", "close your eyes"]

Perfect! We're almost done now. the only thing that's left is to handle the reverse action, if the fifth bit is set. Not a problem for us, let's make use of that Bitwise we were told about earlier

if (code &&& 16) != 0 do
  Enum.reverse(result)
else
  result
end

So after our sequence of operations above, the action is list is stored in result. Then we check the fifth bit of the original code to see if it's set. If it is we simply reverse the list with Enum.reverse\1, otherwise we return the result as is.

Now if we run the tests, we should see them all pass, and we've completed the handshake exercise.

If you have a better approach or any questions, leave a comment below.

See you next time.