Rubyで言語処理100本ノックやってみた(00~04)

# はじめに
エンジニアになりたいRuby初心者です。以前『プロを目指す人のためのRuby入門』の著者である伊藤淳一(@jnchito)さんに「何か人に見せられるものを作りたい」と相談した際、「言語処理100本ノック」を勧めていただきました。
Railsチュートリアルの合間に進めていきたいと思います。

(言語処理本100ノック2015 https://www.cl.ecei.tohoku.ac.jp/nlp100/)

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

str = "stressed"
reversed_str = ""
str_size = str.size

while str_size > 0
  reversed_str << str[str_size - 1]
  str_size -= 1
end

p reversed_str

#=> "desserts"
```
while文を使い、空の文字列オブジェクトに`"stressed"`を逆から流し込みます。
また、`reverse`メソッドを使えば簡潔に書くことができます。
```rb
str = "stressed"
p str.reverse

#=> "desserts"
```

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

str = "パタトクカシーー"
sliced_str =  str[0] + str[2] + str[4] + str[6]
p sliced_str

#=> "パトカー"
```
`+`ではなく`<<`を使っても同じ出力が得られます。ただし、文字列の結合に`<<`を使用するとレシーバそのものの値を変更してしまいます。いわゆる「破壊的メソッド」です。

また、他に解き方がないか調べて考えた結果が以下のコードになります。
```rb
str = "パタトクカシーー"
sliced_str = str.chars.each_slice(2).map(&:first).join

p sliced_str
#=> "パトカー"
```
どのような処理をしているのか確認していきます。
1. `chars`は文字列`"パタトクカシーー"`を一文字ずつ分割し、配列に変換します。
```#=> ["パ", "タ", "ト", "ク", "カ", "シ", "ー", "ー"]```
1. `each_slice`は配列を引数で指定した数字の長さで分解します。
`#=> [["パ", "タ"], ["ト", "ク"], ["カ", "シ"], ["ー", "ー"]]`
3. `map(&:first)`は`map {|変数| 変数.first}`の省略記法となります。2で得られた配列の中の配列から、最初の要素を取り出します。
`#=> ["パ", "ト", "カ", "ー"]`
3. `join`は配列の中身を連結します。なおかつ`to_s`メソッドを呼び出し、配列を文字列に変換します。
`#=> "パトカー"`

今回は奇数番目の要素を取り出しましたが、`each_slice(2).map(&:last)`と記述することで偶数番目を取り出すことができます。非常に便利です。

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

patrol_car = "パトカー"
taxi = "タクシー"
i1 = 1
i2 = 0

while i1 <= 7
  patrol_car.insert(i1, taxi[i2])
  i1 += 2
  i2 += 1
end

p patrol_car

#=> "パタトクカシーー"
```
なんだか不細工なコードで、もっと上手いやり方がある気がします……。
というわけで調べてみると、`Array`クラスに`zip`メソッドという便利なものがありました。
これはレシーバの配列内の要素と引数として与えられた配列の要素を順に取り出していきます。そして取り出された要素をセットにした配列を作成します。配列の中の配列という形になります。
`
[["パ", "タ"], ["ト", "ク"], ["カ", "シ"], ["ー", "ー"]]
`

```rb
patrol_car = "パトカー"
taxi = "タクシー"

p patorl_car.chars.zip(taxi.chars).join

#=> "パタトクカシーー"
```

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

str = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."

p str.scan(/\w+/).map(&:size)

#=> [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
```
`scan`メソッドは引数に文字列か正規表現を渡すと、マッチした部分文字列の配列を返すというものです。便利！面白い問題ですね。

# 04. 元素記号
```rb
# "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文字を取り出し，取り出した文字列から単語の位置（先頭から何番目の単語か）への連想配列（辞書型もしくはマップ型）を作成せよ．

str = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."

scanned = str.scan(/\w+/)

ary = [1, 5, 6, 7, 8, 9, 15, 16, 19]
hash = {}

scanned.each.with_index(1) do |item, idx|
  if ary.include?(idx)
    hash[item.slice(0, 1)] = idx
  else
    hash[item.slice(0, 2)] = idx
  end
end

p hash

#=> {"H"=>1, "He"=>2, "Li"=>3, "Be"=>4, "B"=>5, "C"=>6, "N"=>7, "O"=>8, "F"=>9, "Ne"=>10, "Na"=>11, "Mi"=>12, "Al"=>13, "Si"=>14, "P"=>15, "S"=>16, "Cl"=>17, "Ar"=>18, "K"=>19, "Ca"=>20}
```
かなり苦戦しましたが、解けたときはものすごく気持ちがよかったです。`include?`メソッドを見つけることができてコンパクトになりました。
今回は`each.with_index`を使いましたが、`each_with_index`というよく似た記法も存在します。前者は引数に数字を与えることで、配列の添え字が指定した数字から開始するよう指定することができます（コード内の`i`にあたる部分）。後者は引数が0から始まります。
(参考 https://qiita.com/tsuchinoko_run/items/5cef7dd9d8baf48ffde7)

マグネシウムがMgではなくMiになっているのは仕様なのだろうか……。

# 最後に
よくできた問題で解いていて楽しかったです。リファレンスとにらめっこして「使えそうなメソッドはないか」と考える時間はためになりました。とりあえず5問だけやりましたが、また挑戦したいです。
「こんな解き方もあるよ」等ありましたら、マサカリ投げてくれると喜びます。
最後まで読んでいただきありがとうございました。