exercism/elixir/rotational-cipher/lib/rotational_cipher.ex

61 lines
1.8 KiB
Elixir

defmodule RotationalCipher do
@moduledoc """
Generates a simple Caesar cipher by rotating the alphabet by a given amount.
Use for that shifted alphabet generation function and transliteration table based on two alphabets.
"""
@alphabet ?a..?z
|> Enum.to_list()
|> to_string()
|> String.split("", trim: true)
# Generates a shifted alphabet based on the given shift amount.
# If swaped is true, the function will return a map with the index as the key and the character as the value,
# otherwise the function will return a map with the character as the key and the index as the value.
defp alphabet(shift \\ 0, swaped \\ false) when shift < 26 and shift >= 0 do
@alphabet
|> Enum.with_index(fn char, index ->
position = if index + shift >= 26, do: index + shift - 26, else: index + shift
if swaped == true, do: {position, char}, else: {char, position}
end)
|> Enum.into(%{})
end
# Generates a transliteration table based on the given shift amount.
defp transliterate_table(shift) do
transliterated = alphabet(shift, true)
alphabet()
|> Enum.reduce(%{}, fn {char, index}, acc ->
Map.merge(acc, %{
transliterated[index] => char,
String.upcase(transliterated[index]) => String.upcase(char)
})
end)
end
defp rotate_char(char, table), do: table[char] || char
@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) when shift == 0 or shift == 26, do: text
def rotate(text, shift) do
table = transliterate_table(shift)
text
|> String.graphemes()
|> Enum.map(&rotate_char(&1, table))
|> Enum.join()
end
end