Edited at

[Ruby] 二次元配列を列方向に足し合わせる(長さが異なる配列も可)

More than 5 years have passed since last update.


やりたいこと

# [[1,1,1],[2,3],[4]] の場合、

[1, 1 ,1]
[2, 3 ]
[4 ]
↓↓↓ 列(タテ)方向に足し合わせる
[7, 4, 1]

もともと以下記事を参考に便利に使わせてもらっていたのですが、

Ruby - 配列の同じ項数同士を足す - Qiita

http://qiita.com/hash/items/7eb4d0eb865de719302d

この手法の場合、異なる長さの配列だとエラーになってしまいます。


OK(inputする各配列の長さが等しい)

# [1,1,1],[2,3,4] のinputで、[3,4,5] のoutputが欲しい

[13] pry(main)> [[1,1,1],[2,3,4]].transpose.map{|ary| ary.inject(&:+)}
=> [3, 4, 5]


NG(inputする各配列の長さが異なる)

# [1,1,1],[2,3],[4] のinputで、[7,4,1] のoutputが欲しい

[14] pry(main)> [[1,1,1],[2,3],[4]].transpose.map{|ary| ary.inject(&:+)}
IndexError: element size differs (2 should be 3)

ということで、対策を考えていきます。


追記

(2014/8/1 15:53 追記)

コメント欄にてよりベターな方法を教えて頂きました!

[42] pry(main)> array = [[1,1,1],[2,3],[4]]

=> [[1, 1, 1], [2, 3], [4]]
[43] pry(main)> array.each_with_object([]) { |e, a| e.each_index { |i| a[i] = a[i].to_i + e[i] } }
=> [7, 4, 1]

これにて、以下内容は不要となるので、参考程度に読み流して頂けますと・・。


TL;DR

以下の2ステップで完了です。


Arrayクラスをオープンしてsafe_transposeメソッドを追加

class Array

def safe_transpose
result = []
max_size = self.max { |a,b| a.size <=> b.size }.size
max_size.times do |i|
result[i] = Array.new(self.first.size)
self.each_with_index { |r,j| result[i][j] = r[i] }
end
result
end
end


使う

[15] pry(main)> [[1,1,1],[2,3],[4]].safe_transpose.map{|ary|ary.inject(){|s,i|s=s+i.to_i}}

=> [7, 4, 1]

これで異なる長さの配列でも利用できるようになりました。


詳しく


transpose最適化

そもそもやろうとしているプロセスとしては、


  1. transposeで2次元配列を行列に見立てて、配列を列方向に入れ替え

  2. 各配列ごとの要素をinjectで足し合わせ

です。

そして今回のエラーの原因としては、

transposeが異なる長さの配列を許可していないから。

[16] pry(main)> [[1,1,1],[2,3],[4]].transpose

IndexError: element size differs (2 should be 3)

ということで、オープンクラスを用いてArrayクラスにsafe_transposeメソッドを定義します。

class Array

def safe_transpose
result = []
max_size = self.max { |a,b| a.size <=> b.size }.size
max_size.times do |i|
result[i] = Array.new(self.first.size)
self.each_with_index { |r,j| result[i][j] = r[i] }
end
result
end
end

これで、足りない項の部分はnilで補ってくれるようになりました。

[14] pry(main)> [[1,1,1],[2,3],[4]].safe_transpose

=> [[1, 2, 4], [1, 3, nil], [1, nil, nil]]


inject最適化

本当はinject(&:+)で書きたいところですが、nilを数値にしてやる必要があるため(※)、こうなります。

[17] pry(main)> [[1,1,1],[2,3],[4]].safe_transpose.map{|ary|ary.inject(){|s,i|s=s+i.to_i}}

=> [7, 4, 1]

safe_transposeでnilでなく0を返せばinject(&:+)でいけるようになりますが、

その場合transposeの意図が変わってしまうため避けました。

もっとスマートなやり方もある気がするので、コメントお願い致します!


参考

How to safely transpose Ruby arrays

http://www.matthewbass.com/2009/05/02/how-to-safely-transpose-ruby-arrays/

Ruby - 配列の同じ項数同士を足す - Qiita

http://qiita.com/hash/items/7eb4d0eb865de719302d