Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

業務で必要になった簡単な文字列処理を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

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

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away