exercism/elixir/simple-cipher/lib/simple_cipher.ex

85 lines
2.7 KiB
Elixir

defmodule SimpleCipher do
@chars ?a..?z
defguardp is_char(char) when char in @chars
defguardp is_sign(sign) when sign in [-1, 1]
defp cipher(text, key) do
length = String.length(text)
times = div(length, String.length(key)) + 1
key
|> Kernel.to_charlist()
|> Enum.map(&(&1 - ?a))
|> List.duplicate(times)
|> List.flatten()
|> Enum.take(length)
end
defp rotate(text, key, sign) when is_binary(text) and is_sign(sign) do
cipher = cipher(text, key)
text
|> Kernel.to_charlist()
|> Enum.with_index(fn element, index -> rotate(element, Enum.at(cipher, index), sign) end)
|> Kernel.to_string()
end
defp rotate(char, shift, sign) when is_sign(sign) and is_char(char) and is_char(char + sign * shift), do: char + sign * shift
defp rotate(char, shift, sign) when is_sign(sign) and is_char(char), do: char + sign * shift - sign * 26
@doc """
Given a `plaintext` and `key`, encode each character of the `plaintext` by
shifting it by the corresponding letter in the alphabet shifted by the number
of letters represented by the `key` character, repeating the `key` if it is
shorter than the `plaintext`.
For example, for the letter 'd', the alphabet is rotated to become:
defghijklmnopqrstuvwxyzabc
You would encode the `plaintext` by taking the current letter and mapping it
to the letter in the same position in this rotated alphabet.
abcdefghijklmnopqrstuvwxyz
defghijklmnopqrstuvwxyzabc
"a" becomes "d", "t" becomes "w", etc...
Each letter in the `plaintext` will be encoded with the alphabet of the `key`
character in the same position. If the `key` is shorter than the `plaintext`,
repeat the `key`.
Example:
plaintext = "testing"
key = "abc"
The key should repeat to become the same length as the text, becoming
"abcabca". If the key is longer than the text, only use as many letters of it
as are necessary.
"""
def encode(plaintext, key), do: rotate(plaintext, key, 1)
@doc """
Given a `ciphertext` and `key`, decode each character of the `ciphertext` by
finding the corresponding letter in the alphabet shifted by the number of
letters represented by the `key` character, repeating the `key` if it is
shorter than the `ciphertext`.
The same rules for key length and shifted alphabets apply as in `encode/2`,
but you will go the opposite way, so "d" becomes "a", "w" becomes "t",
etc..., depending on how much you shift the alphabet.
"""
def decode(ciphertext, key), do: rotate(ciphertext, key, -1)
@doc """
Generate a random key of a given length. It should contain lowercase letters only.
"""
def generate_key(length) do
1..length
|> Enum.map(fn _i -> Enum.random(@chars) end)
|> to_string()
end
end