Background
I have come up with a solution to the Caeser Cipher challenge in Exercism.io. This solution works and passes all tests, but I am still not happy.
Code
defmodule RotationalCipher do
@doc """
Given a plaintext and amount to shift by, return a rotated string.
Example:
iex> RotationalCipher.rotate("Attack at dawn", 13)
"Nggnpx ng qnja"
"""
@spec rotate(text :: String.t(), shift :: integer) :: String.t()
def rotate(text, shift) do
text
|> String.to_charlist()
|> Enum.map( fn char ->
cond do
is_lower_case?( char ) -> rotate_lower_case( char, shift )
is_upper_case?( char ) -> rotate_upper_case( char, shift )
true -> char
end
end )
|> to_string()
end
defp is_lower_case?( char ), do: char >= 97 && char <= 122
defp rotate_lower_case( char, shift ), do: 97 + rem( char - 71 + shift, 26 )
defp is_upper_case?( char ), do: char >= 65 && char <= 90
defp rotate_upper_case( char, shift ), do: 65 + rem( char - 39 + shift, 26 )
end
So, given a string and a shift, I have to shift all the letters. Punctuation, numbers and spaces remain the same.
I achieve this by converting the string to a charlist
, mapping over it, and then converting the result into a string again. But in my eyes, there is room for better.
Possible improvements
- I had to define
is_upper_case?
andis_lower_case?
for characters. Isn’t there a built in function that already does this? - In my
Enum.map
I have a function that I would like to extract into adefp
, for the sake of clarity. But I still don’t know how to do it and I only found answer that tell me how to use anonymous functions with the terse syntax. How would I call an already defined function that takes more than 1 parameter inside anEnum.map
? - I am using a conditional, even though I have the feeling I shouldn’t be using one yet. The exercise is supposed to be about string only but I am already using control flow. I have the feeling I am over complicating. Perhaps there is a simpler solution?
Let me know what you guys think!