LoginSignup
9
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-21

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

今回のお題はこちら

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

という文字列から

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

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

僕の解答例

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

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メソッドのロジックを上から順に分解しながら説明してみます。

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

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

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

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

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

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

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個か」をパターンマッチで条件分岐して、なおかつ変数に代入します。

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のパターンマッチ構文の使い方についてはこちらの記事をご覧ください。

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

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

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

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

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を使ってフラットな配列が返るようにします。

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]

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

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]

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

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

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

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

その他の解答例

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

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

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

9
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6