言語処理100本ノック with Elixir 第1章: 準備運動

Qiita初投稿です。
Elixirを勉強中で、自然言語処理も勉強中なので[言語処理100本ノック](http://www.cl.ecei.tohoku.ac.jp/nlp100)をElixirでやってみます。
ご指摘・ツッコミお待ちしております。

1章1モジュール1ファイルで書いていきます。

<details>
<summary>全体のコード</summary>
<div>

```elixir:chapter_one.ex
defmodule ChapterOne do
  # 00. 文字列の逆順
  def reverse(str), do: str |> String.reverse

  # 01. 「パタトクカシーー」
  def pick_odd(str), do: str |> String.codepoints |> Enum.take_every(2) |> Enum.join

  # 02. 「パトカー」＋「タクシー」＝「パタトクカシーー」
  def merge_alternately(str1, str2) do
    [str1, str2]
    |> Enum.map(&String.codepoints/1)
    |> Enum.zip
    |> Enum.map(&Tuple.to_list/1)
    |> Enum.concat
    |> Enum.join
  end

  # 03. 円周率
  def num_of_characters_by_word(sentence) do
    sentence
    |> String.replace(~r/[,.]/, "")
    |> String.split
    |> Enum.map(&String.length/1)
  end

  # 04. 元素記号
  def take_first_char_of_words(sentence) do
    sentence
    |> String.split
    |> Enum.map(&String.first/1)
    |> Enum.with_index(1)
    |> Enum.into(%{})
  end
  def take_first_char_of_words(sentence, nums_of_char) do
    sentence
    |> String.split
    |> Enum.zip(nums_of_char)
    |> Enum.map(fn {word, num} -> String.slice(word, 0..num-1) end)
    |> Enum.with_index(1)
    |> Enum.into(%{})
  end

  def repeat_replace(list, [], _value), do: list
  def repeat_replace(list, [head | tail], value) do
    list
    |> List.replace_at(head, value)
    |> repeat_replace(tail, value)
  end

  # 05 n-gram
  def n_gram(str, n, :word) do
    str
    |> String.split
    |> Enum.chunk_every(n, 1, :discard)
  end
  def n_gram(str, n, :char) do
    str
    |> String.codepoints
    |> Enum.chunk_every(n, 1, :discard)
    |> Enum.map(&List.to_string/1)
  end

  # 06. 集合
  def get_union(a, b), do: MapSet.union(a, b)
  def get_intersection(a, b), do: MapSet.intersection(a, b)
  def get_difference(a, b), do: MapSet.difference(a, b)

  # 07. テンプレートによる文生成
  def template(x, y, z), do: "#{x}時の#{y}は#{z}"

  # 08. 暗号文
  def cipher(str) do
    str
    |> String.to_charlist
    |> Enum.map(&_cipher/1)
    |> List.to_string
  end

  def _cipher(int) when (int >= 97 and int <= 122), do: 219 - int
  def _cipher(int), do: int

  # 09. Typoglycemia
  def make_typoglycemia(str) do
    str
    |> insert_spaces_before_marks
    |> String.split
    |> Enum.map(&String.codepoints/1)
    |> Enum.map(&shuffle_inside/1)
    |> Enum.map(&Enum.join/1)
    |> Enum.join(" ")
    |> remove_spaces_before_marks
  end

  def shuffle_inside(list) when length(list) > 4 do
    first = List.first(list)
    inside = Enum.slice(list, 1..-2) |> Enum.shuffle
    last = List.last(list)
    Enum.concat([[first], inside, [last]])
  end
  def shuffle_inside(list), do: list

  def insert_spaces_before_marks(str) do
    str
    |> String.replace(",", " ,")
    |> String.replace(".", " .")
    |> String.replace(":", " :")
    |> String.replace(";", " ;")
    |> String.replace("!", " !")
    |> String.replace("?", " ?")
  end
  def remove_spaces_before_marks(str) do
    str
    |> String.replace(" ,", ",")
    |> String.replace(" .", ".")
    |> String.replace(" :", ":")
    |> String.replace(" ;", ";")
    |> String.replace(" !", "!")
    |> String.replace(" ?", "?")
  end

  # 実行
  def run() do
    reverse("stressed")
    |> IO.inspect(label: "00. 文字列の逆順")

    pick_odd("パタトクカシーー")
    |> IO.inspect(label: "01. 「パタトクカシーー」")

    merge_alternately("パトカー", "タクシー")
    |> IO.inspect(label: "02. 「パトカー」＋「タクシー」＝「パタトクカシーー」")

    num_of_characters_by_word("Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.")
    |> IO.inspect(label: "03. 円周率")

    indexes_in_1 = [1, 5, 6, 7, 8, 9, 15, 16, 19] |> Enum.map(&(&1-1))
    nums_of_char = List.duplicate(2, 20) |> repeat_replace(indexes_in_1, 1)
    take_first_char_of_words("Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.", nums_of_char)
    |> IO.inspect(label: "04. 元素記号")

    n_gram("I am an NLPer", 2, :word)
    |> IO.inspect(label: "05. n-gram 単語")
    n_gram("I am an NLPer", 2, :char)
    |> IO.inspect(label: "05. n-gram 文字")

    x = n_gram("paraparaparadise", 2, :char) |> MapSet.new
    y = n_gram("paragraph", 2, :char) |> MapSet.new
    get_union(x, y)
    |> IO.inspect(label: "06. 集合 和")
    get_intersection(x, y)
    |> IO.inspect(label: "06. 集合 積")
    get_difference(x, y)
    |> IO.inspect(label: "06. 集合 差")

    x = 12
    y = "気温"
    z = 22.4
    template(x, y, z)
    |> IO.inspect(label: "07. テンプレートによる文生成")

    cipher("Elixir is a dynamic, functional language designed for building scalable and maintainable applications.")
    |> IO.inspect(label: "08. 暗号文 暗号化")
    |> cipher
    |> IO.inspect(label: "08. 暗号文 復号化")

    make_typoglycemia("I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")
    |> IO.inspect(label: "09, Typoglycemia")
  end
end
```
</div>
</details>

## 00. 文字列の逆順
> 文字列"stressed"の文字を逆に（末尾から先頭に向かって）並べた文字列を得よ．

[String.reverse/1](https://hexdocs.pm/elixir/String.html#reverse/1) を使うだけ

```elixir
  def reverse(str), do: str |> String.reverse

  def run() do
    reverse("stressed")
    |> IO.inspect(label: "00. 文字列の逆順")
  end
```

## 01. 「パタトクカシーー」
> 「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ．

Enumで扱うために[String.codepints/1](https://hexdocs.pm/elixir/String.html#codepoints/1)で文字配列に変換し、[Enum.take_every/2](https://hexdocs.pm/elixir/Enum.html#take_every/2)と[Enum.join/2](https://hexdocs.pm/elixir/Enum.html#join/2)で処理する。

```elixir
  def pick_odd(str), do: str |> String.codepoints |> Enum.take_every(2) |> Enum.join

  def run() do
    pick_odd("パタトクカシーー")
    |> IO.inspect(label: "01. 「パタトクカシーー」")
  end
```

## 02. 「パトカー」＋「タクシー」＝「パタトクカシーー」
> 「パトカー」＋「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ．

複数のEnumerableの要素をまとめるには[Enum.zip/1](https://hexdocs.pm/elixir/Enum.html#zip/1)を使う。
`Enum.zip/1`はTupleのListを返す。TupleはEnumerableではないので、[Tuple.to_list/1](https://hexdocs.pm/elixir/Tuple.html#to_list/1)でListに変換したのち[Enum.concat/1](https://hexdocs.pm/elixir/Enum.html#concat/1)と`Enum.join/1`で処理する。

```elixir
  def merge_alternately(str1, str2) do
    [str1, str2]
    |> Enum.map(&String.codepoints/1)
    |> Enum.zip
    |> Enum.map(&Tuple.to_list/1)
    |> Enum.concat
    |> Enum.join
  end

  def run() do
    merge_alternately("パトカー", "タクシー")
    |> IO.inspect(label: "02. 「パトカー」＋「タクシー」＝「パタトクカシーー」")
  end
```

## 03. 円周率
> "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し，各単語の（アルファベットの）文字数を先頭から出現順に並べたリストを作成せよ．

[String.split/1](https://hexdocs.pm/elixir/String.html#split/1)を使用し、文を空白で分割する。アルファベットのみをカウントするので、分割前に[String.replace/4](https://hexdocs.pm/elixir/String.html#replace/4)でコンマとピリオドを取り除く。
各単語の文字数は[String.length/1](https://hexdocs.pm/elixir/String.html#length/1)で取得する。

```elixir
  def num_of_characters_by_word(sentence) do
    sentence
    |> String.replace(~r/[,.]/, "")
    |> String.split
    |> Enum.map(&String.length/1)
  end

  def run() do
    num_of_characters_by_word("Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.")
    |> IO.inspect(label: "03. 円周率")
  end
```

## 04. 元素記号
> "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し，1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字，それ以外の単語は先頭に2文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．

単語の先頭何文字取るのかを指定するリストを入力するようにした。このリストを作成するために関数`repeat_replace/3`を作成した。
`String.split/1`で文を単語に分割したあと`Enum.zip/2`で文字数指定リストと組み合わせ、[String.slice/3](https://hexdocs.pm/elixir/String.html#slice/3)を使って先頭の文字を取り出す。  
[Enum.with_index/2](https://hexdocs.pm/elixir/Enum.html#with_index/2)でindexをつけたあと、[Enum.into/2](https://hexdocs.pm/elixir/Enum.html#into/2)でMapに変換する。

```elixir
  def take_first_char_of_words(sentence) do
    sentence
    |> String.split
    |> Enum.map(&String.first/1)
    |> Enum.with_index(1)
    |> Enum.into(%{})
  end
  def take_first_char_of_words(sentence, nums_of_char) do
    sentence
    |> String.split
    |> Enum.zip(nums_of_char)
    |> Enum.map(fn {word, num} -> String.slice(word, 0..num-1) end)
    |> Enum.with_index(1)
    |> Enum.into(%{})
  end

  def repeat_replace(list, [], _value), do: list
  def repeat_replace(list, [head | tail], value) do
    list
    |> List.replace_at(head, value)
    |> repeat_replace(tail, value)
  end

  def run() do
    indexes_in_1 = [1, 5, 6, 7, 8, 9, 15, 16, 19] |> Enum.map(&(&1-1))
    nums_of_char = List.duplicate(2, 20) |> repeat_replace(indexes_in_1, 1)
    take_first_char_of_words("Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.", nums_of_char)
    |> IO.inspect(label: "04. 元素記号")
  end
```

## 05. n-gram
> 与えられたシーケンス（文字列やリストなど）からn-gramを作る関数を作成せよ．この関数を用い，"I am an NLPer"という文から単語bi-gram，文字bi-gramを得よ．

単語n-gramか文字n-gramかをatomで指定するようにした。単語の場合は`String.split/1`、文字の場合は`String.codepoints/1`でListに変換し、[Enum.chunk_every/4](https://hexdocs.pm/elixir/Enum.html#chunk_every/4)を利用して指定した要素数ごとのListを得る。文字の場合は[List.to_string/1](https://hexdocs.pm/elixir/List.html#to_string/1)でStringに戻す。

```elixir
  def n_gram(str, n, :word) do
    str
    |> String.split
    |> Enum.chunk_every(n, 1, :discard)
  end
  def n_gram(str, n, :char) do
    str
    |> String.codepoints
    |> Enum.chunk_every(n, 1, :discard)
    |> Enum.map(&List.to_string/1)
  end

  def run() do
    n_gram("I am an NLPer", 2, :word)
    |> IO.inspect(label: "05. n-gram 単語")
    n_gram("I am an NLPer", 2, :char)
    |> IO.inspect(label: "05. n-gram 文字")
  end
```

## 06. 集合
> "paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を，それぞれ, XとYとして求め，XとYの和集合，積集合，差集合を求めよ．さらに，'se'というbi-gramがXおよびYに含まれるかどうかを調べよ．

集合を扱うための[MapSet](https://hexdocs.pm/elixir/MapSet.html)モジュールを利用する。ずばりそのままの関数[MapSet.union/2](https://hexdocs.pm/elixir/MapSet.html#union/2)、[MapSet.intersection/2](https://hexdocs.pm/elixir/MapSet.html#intersection/2)、[MapSet.difference/2](https://hexdocs.pm/elixir/MapSet.html#difference/2)があるのでそれを使うだけ。

```elixir
  def get_union(a, b), do: MapSet.union(a, b)
  def get_intersection(a, b), do: MapSet.intersection(a, b)
  def get_difference(a, b), do: MapSet.difference(a, b)

  def run() do
    x = n_gram("paraparaparadise", 2, :char) |> MapSet.new
    y = n_gram("paragraph", 2, :char) |> MapSet.new
    get_union(x, y)
    |> IO.inspect(label: "06. 集合 和")
    get_intersection(x, y)
    |> IO.inspect(label: "06. 集合 積")
    get_difference(x, y)
    |> IO.inspect(label: "06. 集合 差")
  end
```

## 07. テンプレートによる文生成
> 引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ．さらに，x=12, y="気温", z=22.4として，実行結果を確認せよ．

文字列`""`の中で`#{}`を使うとその中の式を評価し、文字列として結合してくれる。
https://elixir-lang.org/getting-started/basic-types.html#strings

```elixir
  def template(x, y, z), do: "#{x}時の#{y}は#{z}"

  def run()
    x = 12
    y = "気温"
    z = 22.4
    template(x, y, z)
    |> IO.inspect(label: "07. テンプレートによる文生成")
  end
```

## 08. 暗号文
> 与えられた文字列の各文字を，以下の仕様で変換する関数cipherを実装せよ．
> 
> - 英小文字ならば(219 - 文字コード)の文字に置換
> - その他の文字はそのまま出力
> 
> この関数を用い，英語のメッセージを暗号化・復号化せよ．

英小文字のASCIIコードは97~122。https://ja.wikipedia.org/wiki/ASCII

文字列をASCIIコードとして扱うために[String.to_charlist/1](https://hexdocs.pm/elixir/String.html#to_charlist/1)を使う。変換後は`List.to_string/1`で文字列に戻す。
`String.to_charlist`はStringをcharlist（UTF-8でエンコードした数値(Integer)のList）にして返すが、`String.codepoints`はStringのListを返す。

```elixir
"パトカー" |> String.codepoints # ["パ", "ト", "カ", "ー"]
"パトカー" |> String.to_charlist # [12497, 12488, 12459, 12540]
```

Elixirでは`""`（ダブルクォーテーション）で囲むとString、`''`（シングルクォーテーション）で囲むとcharlistになる。
https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html

```elixir
  def cipher(str) do
    str
    |> String.to_charlist
    |> Enum.map(&_cipher/1)
    |> List.to_string
  end

  def _cipher(int) when (int >= 97 and int <= 122), do: 219 - int
  def _cipher(int), do: int

  def run() do
    cipher("Elixir is a dynamic, functional language designed for building scalable and maintainable applications.")
    |> IO.inspect(label: "08. 暗号文 暗号化")
    |> cipher
    |> IO.inspect(label: "08. 暗号文 復号化")
  end
```

## 09. Typoglycemia
> スペースで区切られた単語列に対して，各単語の先頭と末尾の文字は残し，それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ．ただし，長さが４以下の単語は並び替えないこととする．適当な英語の文（例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."）を与え，その実行結果を確認せよ．

[Typoglycemia](https://ja.wikipedia.org/wiki/%E3%82%BF%E3%82%A4%E3%83%9D%E3%82%B0%E3%83%AA%E3%82%BB%E3%83%9F%E3%82%A2)

ランダム並び替えには[Enum.shuffle/1](https://hexdocs.pm/elixir/Enum.html#shuffle/1)を利用した。
コンマやピリオドを適切に扱うためにスペースを追加、削除する関数を用意した。（例文では予めスペースが入れてあるため処理しなくても問題ない。）

```elixir
  def make_typoglycemia(str) do
    str
    |> insert_spaces_before_marks
    |> String.split
    |> Enum.map(&String.codepoints/1)
    |> Enum.map(&shuffle_inside/1)
    |> Enum.map(&Enum.join/1)
    |> Enum.join(" ")
    |> remove_spaces_before_marks
  end

  def shuffle_inside(list) when length(list) > 4 do
    first = List.first(list)
    inside = Enum.slice(list, 1..-2) |> Enum.shuffle
    last = List.last(list)
    Enum.concat([[first], inside, [last]])
  end
  def shuffle_inside(list), do: list

  def insert_spaces_before_marks(str) do
    str
    |> String.replace(",", " ,")
    |> String.replace(".", " .")
    |> String.replace(":", " :")
    |> String.replace(";", " ;")
    |> String.replace("!", " !")
    |> String.replace("?", " ?")
  end
  def remove_spaces_before_marks(str) do
    str
    |> String.replace(" ,", ",")
    |> String.replace(" .", ".")
    |> String.replace(" :", ":")
    |> String.replace(" ;", ";")
    |> String.replace(" !", "!")
    |> String.replace(" ?", "?")
  end

  def run() do
    make_typoglycemia("I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")
    |> IO.inspect(label: "09, Typoglycemia")
  end
```