14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ソニックガーデン プログラマAdvent Calendar 2024

Day 17

【Ruby】1行パターンマッチを使って複雑なハッシュの中身を検証する

Last updated at Posted at 2024-12-16

TL;DR(長いので先に結論)

こんなハッシュがあった場合、

# optionsの中身
{:title=>{:text=>nil},
  # 略...
  :yAxis=>
   [{:min=>300,
     :max=>800,
     :tickInterval=>100,
     :title=>
      {:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
     :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}},
    {:title=>
      {:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
     :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
     :opposite=>true}],
  :tooltip=>{:enabled=>true},
  # 略...
  :setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}

普通だったらこういうふうにテストするけど、

it '"My Data 1"と"My Data 2"の2種類を出力する' do 
  options = do_something
  y_axis = options[:yAxis]
  expect(y_axis.size).to eq 2
  expect(y_axis[0][:title][:text]).to eq 'My Data 1'
  expect(y_axis[1][:title][:text]).to eq 'My Data 2'
  expect(y_axis[1][:opposite]).to be true
end

パターンマッチを使えば、1行で検証できちゃいます!!

it '"My Data 1"と"My Data 2"の2種類を出力する' do 
  options = do_something
  options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
end

詳細は本文にて👇👇👇

はじめに

この記事は「ソニックガーデン プログラマ - Qiita Advent Calendar 2024」17日目の記事です。

ところで、とある開発案件で、lazy_high_charts というgemを使ってチャート表示しているシステムがあります。

チャートを表示させるためにはさまざまなパラメータを作成してライブラリに渡す必要があります。

# copied from https://github.com/michelson/lazy_high_charts
@chart = LazyHighCharts::HighChart.new('graph') do |f|
  f.title(text: "Population vs GDP For 5 Big Countries [2009]")
  f.xAxis(categories: ["United States", "Japan", "China", "Germany", "France"])
  f.series(name: "GDP in Billions", yAxis: 0, data: [14119, 5068, 4985, 3339, 2656])
  f.series(name: "Population in Millions", yAxis: 1, data: [310, 127, 1340, 81, 65])

  f.yAxis [
    {title: {text: "GDP in Billions", margin: 70} },
    {title: {text: "Population in Millions"}, opposite: true},
  ]

  f.legend(align: 'right', verticalAlign: 'top', y: 75, x: -50, layout: 'vertical')
  f.chart({defaultSeriesType: "column"})
end

渡した値はoptionsseriesといったメソッドで取得できます。

# @chart.options の戻り値
{:title=>{:text=>"Population vs GDP For 5 Big Countries [2009]"},
 :legend=>
  {:align=>"right", :verticalAlign=>"top", :y=>75, :x=>-50, :layout=>"vertical"},
 :xAxis=>{:categories=>["United States", "Japan", "China", "Germany", "France"]},
 :yAxis=>
  [{:title=>{:text=>"GDP in Billions", :margin=>70}},
   {:title=>{:text=>"Population in Millions"}, :opposite=>true}],
 :tooltip=>{:enabled=>true},
 :credits=>{:enabled=>false},
 :plotOptions=>{:areaspline=>{}},
 :chart=>{:defaultSeriesType=>"column"},
 :subtitle=>{}}
# @chart.series の戻り値
[{:name=>"GDP in Billions", :yAxis=>0, :data=>[14119, 5068, 4985, 3339, 2656]},
 {:name=>"Population in Millions", :yAxis=>1, :data=>[310, 127, 1340, 81, 65]}]

上のサンプルコードだけでもそこそこ複雑なハッシュですが、実務ではさらにあれこれパラメータを渡さないといけません。
このデータをテストコード上でどう検証するか、というのがこの記事の主題です。

確認した動作環境

  • Ruby 3.3.3
  • rspec-core 3.13.0

実務で実際に使ったデータ(に近いもの)

上の例はlazy_high_chartsのREADMEから抜粋したものでしたが、実務で使ったデータはもうちょっとややこしいです。

たとえばoptionsはこんな感じ。

{:title=>{:text=>nil},
 :legend=>
  {:enabled=>true,
   :vertical_align=>"middle",
   :align=>"right",
   :layout=>"vertical",
   :item_style=>{:font_size=>12},
   :item_margin_top=>10,
   :item_margin_bottom=>10},
 :xAxis=>
  {:type=>"datetime",
   :dateTimeLabelFormats=>
    {:day=>"%Y/%m/%d",
     :week=>"%Y/%m/%d",
     :month=>"%Y/%m",
     :year=>"%Y"},
   :title=>{:text=>nil},
   :labels=>{:style=>{:fontSize=>16}, :rotation=>-45},
   :tickmark_placement=>"on"},
 # :yAxisの中身をテストしたい
 :yAxis=>
  [{:min=>300,
    :max=>800,
    :tickInterval=>100,
    :title=>
     {:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}},
   {:title=>
     {:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :opposite=>true}],
 :tooltip=>{:enabled=>true},
 :credits=>{:enabled=>false},
 :plotOptions=>{:series=>{:connectNulls=>true}},
 :chart=>{:margin_top=>20},
 :subtitle=>{},
 :setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}

:yAxisの中のハッシュは、条件によっては"My Data 1"がなく、"My Data 2"だけを表示するケースがあります。
このとき:oppositeの値がtrueからfalseに変わります。

{:title=>{:text=>nil},
 :legend=>
  {:enabled=>true,
   :vertical_align=>"middle",
   :align=>"right",
   :layout=>"vertical",
   :item_style=>{:font_size=>12},
   :item_margin_top=>10,
   :item_margin_bottom=>10},
 :xAxis=>
  {:type=>"datetime",
   :dateTimeLabelFormats=>
    {:day=>"%Y/%m/%d",
     :week=>"%Y/%m/%d",
     :month=>"%Y/%m",
     :year=>"%Y"},
   :title=>{:text=>nil},
   :labels=>{:style=>{:fontSize=>16}, :rotation=>-45},
   :tickmark_placement=>"on"},
 # My Data 1がなく、My Data 2のケースもある。このとき :opposite は false になる
 :yAxis=>
  [{:title=>
     {:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :opposite=>false}],
 :tooltip=>{:enabled=>true},
 :credits=>{:enabled=>false},
 :plotOptions=>{:series=>{:connectNulls=>true}},
 :chart=>{:margin_top=>20},
 :subtitle=>{},
 :setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}

一方、seriesはこんな感じです。

[{:type=>"line",
  :name=>"My Data 1",
  :data=>
  [[1697932800000, 392],
   [1700352000000, 530],
   [1702771200000, 585],
   [1705190400000, 388],
   [1707609600000, 558],
   [1710028800000, 432],
   [1712448000000, 756],
   [1714867200000, 479],
   [1717286400000, 527],
   [1719705600000, 523],
   [1722124800000, 785],
   [1724544000000, 376],
   [1726963200000, 676],
   [1729382400000, 781]]
  :yAxis=>0,
  :color=>"#013676",
  :zIndex=>2},
 {:type=>"line",
  :name=>"My Data 2",
  :data=>
   [[1697932800000, 76],
    [1700352000000, 76],
    [1702771200000, 77],
    [1705190400000, 74],
    [1707609600000, 76],
    [1710028800000, 76],
    [1712448000000, 75],
    [1714867200000, 76],
    [1717286400000, 77],
    [1719705600000, 76],
    [1722124800000, 75],
    [1724544000000, 74],
    [1726963200000, 77],
    [1729382400000, 77]],
  :yAxis=>1,
  :color=>"#ACC0D6",
  :zIndex=>1}]

こちらもやはり"My Data 1"がなく、"My Data 2"だけになるケースがあります。
この場合、:yAxisは1ではなく0になります。

# My Data 1がなく、My Data 2だけのケース。このとき :yAxis は 0 になる
[{:type=>"line",
  :name=>"My Data 2",
  :data=>
   [[1697932800000, 76],
    [1700352000000, 76],
    [1702771200000, 77],
    [1705190400000, 74],
    [1707609600000, 76],
    [1710028800000, 76],
    [1712448000000, 75],
    [1714867200000, 76],
    [1717286400000, 77],
    [1719705600000, 76],
    [1722124800000, 75],
    [1724544000000, 74],
    [1726963200000, 77],
    [1729382400000, 77]],
  :yAxis=>0,
  :color=>"#ACC0D6",
  :zIndex=>1}]

愚直に検証する場合

optionsにはたくさんデータが含まれていますが、プログラムで動的に変更しているのは:yAxisの部分だけなのでここが検証の対象になります。
:yAxisについて具体的に検証したいのは以下のような内容です。

  • 通常の表示なら"My Data 1"と"My Data 2"の2種類を出力する。このとき、"My Data 2"には:opposite=>trueのオプションが付く
  • "My Data 1"を表示しないケースでは、"My Data 2"だけを出力する。このとき、"My Data 2"には:opposite=>falseのオプションが付く
# 通常の表示
 :yAxis=>
  [{:min=>300,
    :max=>800,
    :tickInterval=>100,
    :title=>
     {:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}},
   {:title=>
     {:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :opposite=>true}],

# My Data 1を表示しないケース
 :yAxis=>
  [{:title=>
     {:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
    :opposite=>false}],

これを愚直にRSpecで検証するとしたらこんな感じでしょうか。

context '通常の表示' do
  it '"My Data 1"と"My Data 2"の2種類を出力する' do 
    options = do_something
    y_axis = options[:yAxis]
    expect(y_axis.size).to eq 2
    expect(y_axis[0][:title][:text]).to eq 'My Data 1'
    expect(y_axis[1][:title][:text]).to eq 'My Data 2'
    expect(y_axis[1][:opposite]).to be true
  end
end

context '"My Data 1"を表示しないケース' do
  it '"My Data 2”だけを出力する' do 
    options = do_something
    y_axis = options[:yAxis]
    expect(y_axis.size).to eq 1
    expect(y_axis[0][:title][:text]).to eq 'My Data 2'
    expect(y_axis[0][:opposite]).to be false
  end
end

まあ、別にこれでもいいと言えばいいんですが、ちょっと泥臭い感じが否めません。

1行パターンマッチを使うとこうなる

そこでRuby 3.0から導入された1行パターンマッチを使って書き換えてみましょう。
先ほどのテストはこんなふうになります。

context '通常の表示' do
  it '"My Data 1"と"My Data 2"の2種類を出力する' do 
    options = do_something
    options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
  end
end

context '"My Data 1"を表示しないケース' do
  it '"My Data 2”だけを出力する' do 
    options = do_something
    options => {yAxis: [{title: {text: 'My Data 2'}, opposite: false}]}
  end
end

なんと、それぞれ検証が1行で済んでしまいました!

パターンマッチの文法解説

とはいえ、パターンマッチには不慣れな人も多いと思うので簡単に文法を説明していきましょう。
まず大枠は以下のようになっています。

options => {...}

これはcase/inを使わない、1行パターンマッチの構文です。
optionsがハッシュであることを検証しています。(optionsがハッシュ以外ならエラー)

さらに、{}の中身はこうなっています。

options => {yAxis: [...]}

これはoptionsに:yAxisというキーが含まれ、その値が配列になっていることを検証しています。
このとき、optionsには:yAxis以外のキーが含まれていても構いません。

配列の中身はこうなっています。

options => {yAxis: [{...}, {...}]}

これは要素が2個あり、どちらもハッシュであることを検証しています。
配列の要素は2個ぴったりじゃないとエラーになります。

1個目の要素の中身はこうなっています。

options => {yAxis: [{title: {text: 'My Data 1'}, {...}]}

これはハッシュの中に:titleというキーがあり、その値がさらにハッシュであることを検証しています。
そして値のハッシュには:textというキーがあり、その値が"My Data 1"という文字列であることを検証しています。
ハッシュは部分一致で検証されるため、:title:text以外のキーが検証対象のハッシュに含まれていてもエラーになりません。

一方、2個目の要素の中身はこうなっています。

options => {yAxis: [{...}, {title: {text: 'My Data 2'}, opposite: true}]}

title: {text: 'My Data 2'}の部分の考え方は先ほどと同じです。
これに加えて、ここではさらにopposite: trueというキーと値が含まれていることも検証しています。

上の説明をまとめるとこうなります。

# optionsは、
# - :yAxisというキーがあり、その値は配列である
# - 配列には2つのハッシュが要素として含まれる
# - 1つめの要素のハッシュは、:titleがキー、その値はtext: 'My Data 1'が含まれるハッシュ
# - 2つめの要素のハッシュは、:titleがキー、その値はtext: 'My Data 2'が含まれるハッシュ
# - さらに2つめの要素のハッシュには、opposite: trueというキーと値も含まれる
options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}

ここまで理解できれば、"My Data 1"を表示しないケースのパターンマッチもすぐに理解できるはずです。
以下の説明を読んで、自分の理解が合っているかどうか確認してみてください。

# optionsは、
# - :yAxisというキーがあり、その値は配列である
# - 配列には1つのハッシュが要素として含まれる
# - 1つめの要素のハッシュは、:titleがキー、その値はtext: 'My Data 2'が含まれるハッシュ
# - さらに1つめの要素のハッシュには、opposite: falseというキーと値も含まれる
options => {yAxis: [{title: {text: 'My Data 2'}, opposite: false}]}

もし検証に失敗したらどうなるのか?

optionsの中身が指定したパターンにマッチしない場合は NoMatchingPatternError が発生してテストが落ちます。

ためしに「通常の表示」で使っていた"My Data 1"を"My Data 100"に変えてテストを実行してみましょう。

options = do_something
# わざとMy Data 1をMy Data 100に変えてみる
options => {yAxis: [{title: {text: 'My Data 100'}}, {title: {text: 'My Data 2'}, opposite: true}]}

するとテスト実行時に次のようなエラーが発生します。

NoMatchingPatternError: {:title=>{:text=>nil}, :legend=>{:enabled=>true, :vertical_align=>"middle", :align=>"right", :layout=>"vertical", :item_style=>{:font_size=>12}, :item_margin_top=>10, :item_margin_bottom=>10}, :xAxis=>{:type=>"datetime", :dateTimeLabelFormats=>{:day=>"%Y/%m/%d", :week=>"%Y/%m/%d", :month=>"%Y/%m", :year=>"%Y"}, :title=>{:text=>nil}, :labels=>{:style=>{:fontSize=>16}, :rotation=>-45}, :tickmark_placement=>"on"}, :yAxis=>[{:min=>300, :max=>800, :tickInterval=>100, :title=>{:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}}, :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}}, {:title=>{:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}}, :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}, :opposite=>true}], :tooltip=>{:enabled=>true}, :credits=>{:enabled=>false}, :plotOptions=>{:series=>{:connectNulls=>true}}, :chart=>{:margin_top=>20, :renderTo=>"chart-multiple-timeline"}, :subtitle=>{}, :setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}: "My Data 100" === "My Data 1" does not return true

一番最後に "My Data 100" === "My Data 1" does not return true というメッセージが見えるので、どのポイントでマッチしなかったのかがわかります。

ですが、残念ながら検証に失敗したときのエラーメッセージは冗長でちょっとわかりづらいですね😅

応用: expectで囲んで検証していることを明示的に示す

先ほどはパターンマッチを使って以下のようなテストコードを書きました。

it '"My Data 1"と"My Data 2"の2種類を出力する' do 
  options = do_something
  options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
end

が、これだとexpectが1つもなく、どこで何を検証しているのかわかりづらいです。
この問題を避けるために、あえてexpect{...}.not_to raise_errorで囲んであげると良いかもしれません。

it '"My Data 1"と"My Data 2"の2種類を出力する' do 
  options = do_something
  expect {
    options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
  }.not_to raise_error
end

この場合、もしパターンマッチに失敗して NoMatchingPatternError が発生した場合は以下のような実行結果になります。

expected no Exception, got #<NoMatchingPatternError: {:title=>{:text=>nil}, :legend=>{:enabled=>true, :vertical_align=>"middle",...etStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}: "My Data 100" === "My Data 1" does not return true> with backtrace:

改行させるのもアリ

1行パターンマッチという名前ですが、=>の右辺は改行して読みやすくするのもOKです。

options => {
  yAxis: [
    {title: {text: 'My Data 1'}},
    {title: {text: 'My Data 2'}, opposite: true}
  ]
}

パターンマッチを使いつつ、値を変数にも格納する

続いて、seriesの値も検証してみましょう。
seriesはこんなデータになっていました。

# 通常の表示
[{:type=>"line",
  :name=>"My Data 1",
  :data=>
  [[1697932800000, 392],
   [1700352000000, 530],
   [1702771200000, 585],
   [1705190400000, 388],
   [1707609600000, 558],
   [1710028800000, 432],
   [1712448000000, 756],
   [1714867200000, 479],
   [1717286400000, 527],
   [1719705600000, 523],
   [1722124800000, 785],
   [1724544000000, 376],
   [1726963200000, 676],
   [1729382400000, 781]]
  :yAxis=>0,
  :color=>"#013676",
  :zIndex=>2},
 {:type=>"line",
  :name=>"My Data 2",
  :data=>
   [[1697932800000, 76],
    [1700352000000, 76],
    [1702771200000, 77],
    [1705190400000, 74],
    [1707609600000, 76],
    [1710028800000, 76],
    [1712448000000, 75],
    [1714867200000, 76],
    [1717286400000, 77],
    [1719705600000, 76],
    [1722124800000, 75],
    [1724544000000, 74],
    [1726963200000, 77],
    [1729382400000, 77]],
  :yAxis=>1,
  :color=>"#ACC0D6",
  :zIndex=>1}]

# "My Data 1"を表示しないケース
[{:type=>"line",
  :name=>"My Data 2",
  :data=>
   [[1697932800000, 76],
    [1700352000000, 76],
    [1702771200000, 77],
    [1705190400000, 74],
    [1707609600000, 76],
    [1710028800000, 76],
    [1712448000000, 75],
    [1714867200000, 76],
    [1717286400000, 77],
    [1719705600000, 76],
    [1722124800000, 75],
    [1724544000000, 74],
    [1726963200000, 77],
    [1729382400000, 77]],
  :yAxis=>0,
  :color=>"#ACC0D6",
  :zIndex=>1}]

検証したいポイントは以下の通りです。

  • 通常の表示では"My Data 1"と"My Data 2"のデータが出力される。このとき、"My Data 2"の:yAxisは1になる
  • "My Data 1"を表示しないケースでは"My Data 2"のデータだけが出力される。このとき、"My Data 2"の:yAxisは0になる
  • "My Data 1"も"My Data 2"も、:dataには14件のデータ(配列)が含まれる

この内容を検証するために、1行パターンマッチを利用して以下のようなテストコードを書きました。

context '通常の表示' do
  it '"My Data 1"と"My Data 2"の2種類を出力する' do 
    series = do_something
    series => [{name: 'My Data 1', data: Array => data_1, yAxis: 0}, {name: 'My Data 2', data: Array => data_2, yAxis: 1}]
    expect(data_1.size).to eq 14
    expect(data_2.size).to eq 14
  end
end

context '"My Data 1"を表示しないケース' do
  it '"My Data 2”だけを出力する' do 
    series = do_something
    series => [{name: 'My Data 2', data: Array => data_2, yAxis: 0}]
    expect(data_2.size).to eq 14
  end
end

基本的にoptionsを検証したときと考え方は同じなのですが、一箇所だけ新しいテクニックを使っています。
それは以下の部分です。

data: Array => data_1

これはdata: Array=> data_1の部分に分かれます。

  • data: Arrayでは、:dataに対応する値が配列(Array)であることを検証しています
  • => data_1では、上でマッチした配列をdata_1というローカル変数に格納しています(as パターンと呼ばれるパターンマッチの記法です)

つまり、data_1には以下の値が格納されます。

# data_1の中身
[[1697932800000, 392],
 [1700352000000, 530],
 [1702771200000, 585],
 [1705190400000, 388],
 [1707609600000, 558],
 [1710028800000, 432],
 [1712448000000, 756],
 [1714867200000, 479],
 [1717286400000, 527],
 [1719705600000, 523],
 [1722124800000, 785],
 [1724544000000, 376],
 [1726963200000, 676],
 [1729382400000, 781]]

同様に、data_2には以下の値が格納されます。

# data_2の中身
[[1697932800000, 76],
 [1700352000000, 76],
 [1702771200000, 77],
 [1705190400000, 74],
 [1707609600000, 76],
 [1710028800000, 76],
 [1712448000000, 75],
 [1714867200000, 76],
 [1717286400000, 77],
 [1719705600000, 76],
 [1722124800000, 75],
 [1724544000000, 74],
 [1726963200000, 77],
 [1729382400000, 77]]

data_1data_2はただの変数(ただの配列)なので、普通にRSpecで検証できます。

expect(data_1.size).to eq 14
expect(data_2.size).to eq 14

上の例では単純に要素数を検証しただけですが、必要に応じて各要素の中身を詳しく検証することもできます。

また、ローカル変数に格納する必要がなければ=> data_1=> data_2は省略して構いません。

it '"My Data 1"と"My Data 2"の2種類を出力する' do 
  series = do_something
  # dataの値が配列であることを検証する(配列の中身は任意。変数にも格納しない)
  series => [{name: 'My Data 1', data: Array, yAxis: 0}, {name: 'My Data 2', data: Array, yAxis: 1}]
end

応用:暗黙的にキーと同名のローカル変数に代入する(ただし条件あり)

先ほどの例で使った以下のコードは、

context '"My Data 1"を表示しないケース' do
  it '"My Data 2”だけを出力する' do 
    series = do_something
    series => [{name: 'My Data 2', data: Array => data_2, yAxis: 0}]
    expect(data_2.size).to eq 14
  end
end

こんなふうに書き替えることもできます。

context '"My Data 1"を表示しないケース' do
  it '"My Data 2”だけを出力する' do 
    series = do_something
    series => [{name: 'My Data 2', data:, yAxis: 0}]
    expect(data.size).to eq 14
  end
end

変わったのは以下の部分です。

data: Array => data_2
# ↓
data:

これにあわせてexpectの部分も変わっています。

expect(data_2.size).to eq 14
# ↓
expect(data.size).to eq 14

ハッシュのパターンマッチで値を省略してキーだけにすると、キーに対応する値がキーと同名のローカル変数に格納されます。
つまり、ここではdataという変数に14個の配列が格納されます。

ただし、このテクニックは:dataが1回しか登場しない場合に限り有効です。
:dataが2回登場するとローカル変数名が重複してしまうのでエラーが発生します。

# 以下の場合は:dataが2回登場するので、明示的に変数名を指定しないと構文エラー
series => [{name: 'My Data 1', data:, yAxis: 0}, {name: 'My Data 2', data:, yAxis: 1}]
#=> duplicated variable name (SyntaxError)

注意:ハッシュのキーはシンボルであることが必須

パターンマッチでハッシュを扱う場合、ハッシュのキーはシンボルでなければならない点に注意してください。

# これはOK
{ab: 1, cd: 2} => {ab: 1}

# ハッシュのキーが文字列だとマッチしない
{'ab' => 1, 'cd' => 2} => {ab: 1}
#=> {"ab"=>1, "cd"=>2}: key not found: :ab (NoMatchingPatternKeyError)

# パターンマッチのパターンは構文的にシンボルのキーしか書けない
{'ab' => 1, 'cd' => 2} => {'ab' => 1}
#=> syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END (SyntaxError)

Rails環境であれば symbolize_keys メソッドでキーをシンボルに変換することができます。

# キーをシンボルに変換しておけばOK
{'ab' => 1, 'cd' => 2}.symbolize_keys => {ab: 1}

おまけ:=>の代わりにinを使う?

1行パターンマッチには2種類の書き方があります。
ひとつはこの記事で紹介した=>を使う方法で、もうひとつはinを使う方法です。

foo => {...}
foo in {...}

それぞれ以下のような違いがあります。

  • => はマッチに成功するとnilが返り、失敗するとNoMatchingPatternKeyErrorが発生する
  • in はマッチに成功するとtrueが返り、失敗するとfalseが返る

なので、この記事で紹介したテストはinを使って以下のように書くこともできます。

it '"My Data 1"と"My Data 2"の2種類を出力する' do 
  options = do_something
  # 1行パターンマッチがtrueを返すことを検証する
  expect((
    options in {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
  )).to be true
end

inを使ったパターンマッチはtrue/falseを返すので一見テストコード向きに思えますが、パターンマッチに失敗したときに原因を教えてくれないため(なぜならfalseを返すだけだから)、=>を使った方が実用的だと思います。

ちなみに、上のコードでは expect(( ... )).to のように丸かっこが二重になっていますが、これは記述ミスではありません。
inを使った1行パターンマッチをメソッドの引数に渡すときは、パターンマッチ自体を()で囲まないと構文エラーになります。

puts([1] in [n])
#=> syntax error, unexpected `in', expecting ')' (SyntaxError)

puts(([1] in [n]))
#=> true

inを使った1行パターンマッチは、Rubyを長年書いている筆者でも戸惑うようなちょっと変わった挙動になるので要注意です。

# メソッド呼び出しの丸かっこは省略できるが、パターンマッチの丸かっこは省略できない
# (省略すると [1] in [n] より puts [1] が優先的に評価される)
puts ([1] in [n])
#=> true

# 変数に代入するときも丸かっこが必要
# (省略すると [1] in [n] より ret = [1] が優先的に評価される)
ret = ([1] in [n])
puts ret
#=> true

# if文では丸かっこ不要
puts "matched." if [1] in [n]
#=> matched.

以下のissueでも同じような質問が挙がっていました。

まとめ

というわけで、この記事では1行パターンマッチを使って複雑なハッシュの中身を検証するコード例を紹介してみました。

複雑に入り組んだハッシュの中身を検証する場合はここで紹介したパターンマッチのテクニックが使えないか検討してみてください!

それでは明日の「ソニックガーデン プログラマ - Qiita Advent Calendar 2024」もお楽しみに〜:christmas_tree:

【PR】Rubyのパターンマッチを学ぶならこれ!

Rubyのパターンマッチはうまく使いこなせると非常に強力な武器になるのですが、かなり高機能なぶん、仕様(構文)が複雑です。
拙著「プロを目指す人のためのRuby入門 改訂2版」では、1章ぶんまるごと使ってパターンマッチを詳しく、丁寧に説明しています。

Screenshot 2024-12-16 at 11.58.44.png

パターンマッチ、名前は知ってるけど全然わからない😣という人は、ぜひ「プロを目指す人のためのRuby入門 改訂2版」を読んでみてください!

ちなみに、パターンマッチが解説されているのは、さくらんぼが2つの「改訂2版」の方なので、間違って第1版を買わないように注意してくださいね!

20211201085815.png

14
3
0

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
14
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?