業務で必要になった簡単な文字列処理を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"]
正規表現の読み方がわからない!という人はこちらの記事を読めばわかるはずです。
- 初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」 - Qiita
- 初心者歓迎!手と目で覚える正規表現入門・その2「微妙な違いを許容しつつ置換しよう」 - Qiita
さらに、その配列をそれぞれハイフンで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
いろいろな解き方があって面白いですね!