Rubyのパターンマッチを使って簡単なプログラミング問題を解いてみた

業務で必要になった簡単な文字列処理をRuby 2.7で導入されたパターンマッチを使って解いてみました。

## 今回のお題はこちら

`"1, 5, 10-12, 15, 18-20"`

という文字列から

`[1, 5, 10, 11, 12, 15, 18, 19, 20]`

という配列を作りたい。どう書く？

## 僕の解答例

こんな感じで解いてみました。
Minitestによるテストも一緒に書いてます。

```ruby
require 'minitest/autorun'

def parse_as_array(str)
  str
    .scan(/\d+(?:-\d+)?/)
    .flat_map{|s|
      case s.split('-').map(&:to_i)
      in [n]    then n
      in [n, m] then [*n..m]
      end
    }
end

class ParseAsArrayTest < Minitest::Test
  def test_parse_as_array
    str = "1, 5, 10-12, 15, 18-20"
    expected = [1, 5, 10, 11, 12, 15, 18, 19, 20]
    assert_equal expected, parse_as_array(str)
  end
end
```

### ざっくり解説

上の`parse_as_array`メソッドのロジックを上から順に分解しながら説明してみます。

まず、正規表現を使って文字列から数字とハイフンだけを配列として抜き出します。

```ruby
str = "1, 5, 10-12, 15, 18-20"
str.scan(/\d+(?:-\d+)?/)
#=> ["1", "5", "10-12", "15", "18-20"]
```

正規表現の読み方がわからない！という人はこちらの記事を読めばわかるはずです。

- [初心者歓迎！手と目で覚える正規表現入門・その１「さまざまな形式の電話番号を検索しよう」 \- Qiita](https://qiita.com/jnchito/items/893c887fbf19e17d3ff9)
- [初心者歓迎！手と目で覚える正規表現入門・その２「微妙な違いを許容しつつ置換しよう」 \- Qiita](https://qiita.com/jnchito/items/64c3fdc53766ac6f2008)

さらに、その配列をそれぞれハイフンでsplitします。

```ruby
arr = ["1", "5", "10-12", "15", "18-20"]
arr.map{|s| s.split('-')}
#=> [["1"], ["5"], ["10", "12"], ["15"], ["18", "20"]]
```

ただし、そのままだと文字列になってしまうので、`to_i`メソッドで整数に変換します。

```ruby
arr = ["1", "5", "10-12", "15", "18-20"]
arr.map{|s| s.split('-').map(&:to_i)}
#=> [[1], [5], [10, 12], [15], [18, 20]]
```

そうすると、"-"が含まれる文字列は要素が2個に、それ以外は要素が1個になります。
この「1個か、2個か」をパターンマッチで条件分岐して、なおかつ変数に代入します。

```ruby
arr = [[1], [5], [10, 12], [15], [18, 20]]
arr.each {|a|
  case a
  in [n]
    # 要素が1個の場合の処理
    pp n
  in [n, m]
    # 要素が2個の場合の処理
    pp [n, m]
  end
}
#=> 1
#   5
#   [10, 12]
#   15
#   [18, 20]
```

Ruby 2.7のパターンマッチ構文の使い方についてはこちらの記事をご覧ください。

- [サンプルコードでわかる！Ruby 2\.7の新機能・パターンマッチ（前編） \- Qiita](https://qiita.com/jnchito/items/9bb4aa1dcefa00257815)

さらに、"10-12"のような文字列は`[10, 11, 12]`のような配列に変換する必要があります。
これは範囲（Range）を使って実現します。

```ruby
n = 10
m = 12
[*n..m]
#=> [10, 11, 12]
```

ハイフンを含まない数字（1や5）はそのまま数字として使えばOKです。

ここまでのアイデアを組み合わせてメソッドチェーンにすると、次のようなコードが書けます。（パターンマッチ構文は`then`を使って`in`を1行にまとめています）

```ruby
str = "1, 5, 10-12, 15, 18-20"
str
  .scan(/\d+(?:-\d+)?/)
  .map{|s|
    case s.split('-').map(&:to_i)
    in [n]    then n
    in [n, m] then [*n..m]
    end
  }
#=> [1, 5, [10, 11, 12], 15, [18, 19, 20]]
```

ただし、上の結果を見るとわかるように、"10-12"や"18-20"の変換結果はネストした配列になってしまっています。
そこで、`map`の代わりに`flat_map`を使ってフラットな配列が返るようにします。

```ruby
str = "1, 5, 10-12, 15, 18-20"
str
  .scan(/\d+(?:-\d+)?/)
  .flat_map{|s|
    case s.split('-').map(&:to_i)
    in [n]    then n
    in [n, m] then [*n..m]
    end
  }
#=> [1, 5, 10, 11, 12, 15, 18, 19, 20]
```

これをメソッド化すれば完成です！

```ruby
def parse_as_array(str)
  str
    .scan(/\d+(?:-\d+)?/)
    .flat_map{|s|
      case s.split('-').map(&:to_i)
      in [n]    then n
      in [n, m] then [*n..m]
      end
    }
end

str = "1, 5, 10-12, 15, 18-20"
parse_as_array(str)
#=> [1, 5, 10, 11, 12, 15, 18, 19, 20]
```

### パターンマッチを使わない場合

ほぼ同じ考え方でパターンマッチを使わない書き方もできます。

```ruby
def parse_as_array(str)
  str
    .scan(/\d+(?:-\d+)?/)
    .flat_map{|s|
      n, m = s.split('-').map(&:to_i)
      m ? [*n..m] : n
    }
end
```

あれ、こっちの方がシンプルかも・・・？？😅

## その他の解答例

弊社内で出てきたその他の解答例です。

```ruby
# evalを使うパターン
def parse_as_array(str)
  str
    .gsub('-', '..')
    .split(',')
    .flat_map {|x| Array(eval(x)) }
end
```

```ruby
# evalを使うパターンその2
def parse_as_array(str)
  str
    .gsub(/(\d+)-(\d+)/, '*\1..\2')
    .then{|s| eval("[#{s}]")}
end
```

いろいろな解き方があって面白いですね！