ham-capのブログ

プログラミング学習の記録

【Ruby】IndexErrorを起こさずに要素数の異なる二次元配列(配列の配列)をtransposeする

したいこと

RubyのArrayクラスが持つインスタンスメソッドtransposeはレシーバとなる二次元配列内の要素数が一致していないとIndexErrorが発生して上手く動かないため、それを避けるために各配列の要素数を揃える処理を書きたい。

二次元配列って何さ?

二次元配列というのは「配列を要素として持っている配列」のことです。

こんな感じ。

array = [["a", "b", "c"], ["d", "e", "f"], ["g", "h", "i"]]

中に入っている配列は外側の配列arrayの要素なので、添字がついています。 もし["a", "b", "c"]を呼び出したければarray[0]とやればいいし、更にその中の"b"を呼び出したければarray[0][1]とすればOK。

普通の配列を任意の要素数で区切って二次元配列を作ることができるメソッドもある。これはまた別の機会に書きます。

それと、今更だけどこの「配列の配列」を「二次元配列」と呼ぶのが一般的かどうかはよく分からない。けどたぶん通じる。

transposeメソッドって何さ?

Arrayクラスのインスタンスメソッド。

何ができるメソッドなのかを理解するためにとりあえずリファレンスマニュアルを読みます。

自身を行列と見立てて、行列の転置(行と列の入れ換え)を行います。転置した配列を生成して返します。空の配列に対しては空の配列を生成して返します。

それ以外の一次元の配列に対しては、例外 TypeError が発生します。各要素のサイズが不揃いな配列に対しては、例外 IndexError が発生します。

出典:docs.ruby-lang.orgclass Array (Ruby 3.0.0 リファレンスマニュアル)

とのこと。

僕と同じぐらいのプログラミングレベルのそこのあなた。

分かります。その顔は「日本語でおk。」の顔です。僕には分かります。

特に「行列と見立てて」の意味がわからなくないですか?僕は分かりませんでした。 初見で分かった人は結構すごいと思うので、オプーナを買う権利を与えます。

コードで見てみます。

#この配列を行列と見立てると、
array = [["a", "b", "c"], ["d", "e", "f"], ["g", "h", "i"]]

#こんな感じ
array = [["a", "b", "c"],
         ["d", "e", "f"],
         ["g", "h", "i"]]

#これをtransposeするとこうなる
array.transpose
=>[["a", "d", "g"]["b", "e", "h", ]["c", "f", "i"]]

リファレンスマニュアルの説明にある「行列の転置(行と列の入れ換え)」というのはこういうこと。 言い方を変えると、各配列のインデックス番号が同じものをまとめて配列にする。(例えばインデックス番号が0である"a", "d", "g"がまとめられて配列になる。)

で、各要素のサイズが不揃いな場合はIndexErrorが発生しますよと。 つまり仮に要素数が3つの配列と5つの配列と7つの配列があったとして、それらの行と列を入れ替えようと思っても、そもそも持っている要素の数があっていないから行列に見立てることができずにtransposeさん的にはNGになっちゃうってことのようです。

本題

さて、今回やりたいことは冒頭に書いたとおりtransposeメソッドを使用する際にIndexErrorが発生するのを回避すること。そのために配列の要素数を揃えたい。

例えば以下のように要素数の異なる配列を要素として持つ二次元配列があるとします。

array = [["a", "b", "c"], ["d", "e", "f", "g"], ["h", "i"]]

これをtransposeしようとすると以下のようにエラーが発生します。 f:id:ham-cap:20210317140205p:plain こうならないためには各配列の要素数を一致させる必要があります。 どうするかというと、素数が足りていない配列にnilを追加するだけです。 他にもっといいやり方があるのかもしれませんが、僕にはこれぐらいしか思いつきませんでした。

これを実装するために必要な処理を考えてみた結果、以下の流れでいけるのではと思いました。

  1. 最も多くの要素を持つ配列を見つけて、その要素数を上限値として設定する
  2. 各配列の要素数を調べて、1.で設定した上限に対して足りていない配列を探す
  3. 2.で見つかった配列に対して、要素数が上限値に達するまでnilを代入する

で、僕が書いたコードがこちら。

#要素数の異なる二次元配列
array = [["a", "b", "c"], ["d", "e", "f", "g"], ["h", "i"]]
#変数maxを用意し、配列array内で最も多い要素数を代入する
max = array.max_by { |a| a.size }.size
#各配列の要素数が最大値に達するまでnilを代入し、結果を変数adjusted_arrayに代入する
adjusted_array = array.each do |a|
                    while a.size < max
                      a << nil
                    end
                  end
=> [["a", "b", "c", nil], ["d", "e", "f", "g"], ["h", "i", nil, nil]]

で、これをtransposeすると、

adjusted_array.transpose
=> [["a", "d", "h"], ["b", "e", "i"], ["c", "f", nil], [nil, "g", nil]]

できました。

できる人からすると「ふーん。」って感じだと思いますが、whileを使うことを思いついたときはちょっと嬉しかったです。 ちなみに、僕はmax_byメソッドも知らなかったですし、whileを実戦投入したのも初めてでした。

まとめ

というわけで、見事IndexErrorを回避して要素数の異なる二次元配列をtransposeすることに成功しました。

僕は今回、フィヨルドブートキャンプのとある課題を作成する過程でこの処理を考えたのですが、結局そちらは二次元配列にする前に全要素数を調整するという方向性で実装することにしたため、今回の方法は採用しない予定です。

ですが、せっかく考えたものが日の目を見ないのはもったいないので今回ブログの記事にさせてもらいました。 ビギナーにとっては自分が考えたコードひとつひとつが脳みそにかいた汗の結晶なので、そのまま忘れ去るのはもったいないのです。 こんな記事でも誰かの役に立てればいいなぁと思いつつ、今回は終わりです。 もし、間違っている箇所や誤解を招く表現を見つけた方はそっと優しく教えていただければ幸いです。くれぐれも優しくお願いします。

どうでもいいですが、実はこの実装を考えていた時間よりもこの記事を書くまでにかかった時間のほうがはるかに長いです。文章で説明するの難しい。