호두나무 공방/Exercism in Elixir

Affine Cipher - Exercism in Elixir

2022. 10. 10. 22:36

문제 보기

고대 중동의 암호화 알고리즘을 간단하게 구현하는 문제였다. 이런 치환암호(substitution cipher) 방식의 알고리즘은 엘릭서에서는 문자 리스트로 바꿔서 처리하는 것이 간단하다. 배경 지식인 건지 내가 텍스트에서 못 찾은 건지, 설명에 몇 가지 내용이 빠져 있어서 테스트 케이스를 더듬어가며 답을 찾아야 하는 과정이 조금 번거로웠다. MMI(나머지 연산의 역원)를 구하는 과정에 스트림을 쓴 것이 내심 만족스럽다.

defmodule AffineCipher do
  @typedoc """
  A type for the encryption key
  """
  @type key() :: %{a: integer, b: integer}

  @doc """
  Encode an encrypted message using a key
  """
  @spec encode(key :: key(), message :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def encode(%{a: a}, _message) when rem(a, 2) == 0 or rem(a, 13) == 0 do
    {:error, "a and m must be coprime."}
  end

  def encode(%{a: a, b: b}, message) do
    message
    |> String.replace(~r/[\s\.\,]/, "")
    |> to_charlist()
    |> Enum.map(fn 
      c when c in ?a..?z ->
        ?a + rem(a * (c - ?a) + b, 26)
      c when c in ?A..?Z ->
        ?a + rem(a * (c - ?A) + b, 26)
      c ->
        c
    end)
    |> Enum.chunk_every(5)
    |> Enum.map(&to_string/1)
    |> Enum.join(" ")
    |> then(fn r -> {:ok, r} end)
  end

  @doc """
  Decode an encrypted message using a key
  """
  @spec decode(key :: key(), message :: String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def decode(%{a: a}, _encrypted) when rem(a, 2) == 0 or rem(a, 13) == 0 do
    {:error, "a and m must be coprime."}
  end

  def decode(%{a: a, b: b}, encrypted) do
    encrypted
    |> String.replace(~r/\s/, "")
    |> to_charlist()
    |> Enum.map(fn 
      y when y in ?a..?z ->
        ?a + Integer.mod(mmi(a) * (y - ?a - b), 26)
      y ->
        y
    end)
    |> to_string()
    |> then(fn r -> {:ok, r} end)
  end

  defp mmi(a) do
    Stream.iterate(1, &(&1 + 1))
    |> Stream.filter(fn i ->
      rem(a * i, 26) == 1
    end)
    |> Enum.take(1)
    |> hd()
  end
end