Rangeから配列

Rangeから配列を作るのはよくあることで、プログラミング言語の標準ライブラリでサポートされていることがほとんど。

Rubyなら次のように書ける。

$ ruby -e 'p (1..9).to_a'
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Bashならこう。

$ echo {1..9}
1 2 3 4 5 6 7 8 9

配列からRange

仕様

今回やりたいことはこの逆で、数値の並びからRangeの表記に書き換えること。

$ a=$(seq 3 && seq 7 9 && seq 15 15 && seq 30 36)
$ echo "$a"
1
2
3
7
8
9
15
30
31
32
33
34
35
36

上記をこのように表示したい。

1..3
7..9
15
30..36

Rubyで素直に書く

Rubyで素直に書くと以下のようになる。

#!/usr/local/bin/ruby

arr = readlines.map(&:to_i)
rng_arr = [arr.first]
prev = arr.first

arr[1..-1].each do |i|
  rng_arr << prev << i unless prev + 1 == i
  prev = i
end

rng_arr << arr.last

rng_arr.each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join("..")}

配列の一番目の要素は必ず必要なのでRange生成用の配列に格納し、そのあとは各要素で前の値に1足した数値でなければ配列に追加している。また最後の要素も必ず必要なのでRange生成用の配列に格納する。あとは配列からeach_sliceで二つずつ値を取り出し、..で繋げて標準出力する。

$ echo "$a" | ruby arr_to_rng.rb
1..3
7..9
15
30..36

Rubyでワンライナーで書く

ワンライナーに挑戦してみる。
方法としては、配列をズラして組み合わせることで、前後の値の比較をしすくする。配列をズラして組み合わせる方法を二つ書く。

前後に要素を追加してzip

正数を対象にしていると仮定して、-1を配列の前に足したものと配列の後に足したものをzipする。

$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); p ([-1] + arr).zip(arr + [-1])'
[[-1, 1], [1, 2], [2, 3], [3, 7], [7, 8], [8, 9], [9, 15], [15, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, -1]]

素直にスクリプトを書いたときはprevに値を代入していたが、これで代入なしで各要素の配列で前後が比べやすくなった。

生成した配列にselectを使って前後の値が異なるものを取り出す。

$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); p ([-1] + arr).zip(arr + [-1]).select{|x| x[0] + 1 != x[1]}'
[[-1, 1], [3, 7], [9, 15], [15, 30], [36, -1]]

あとはflattenしてから-1を消してあげるとRange生成用の配列ができる。

$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); ([-1] + arr).zip(arr + [-1]).select{|x| x[0] + 1 != x[1]}.flatten.reject{|i| i == -1}.each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join("..")}'
1..3
7..9
15
30..36

rotateしてzip

「配列をズラす」そのままのメソッドがArrayにある。

$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); p arr.zip(arr.rotate)'
[[1, 2], [2, 3], [3, 7], [7, 8], [8, 9], [9, 15], [15, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 1]]

先ほどと同じようにselectする。

$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i);p arr.zip(arr.rotate).select{|x| x[0] + 1 != x[1]}'
[[3, 7], [9, 15], [15, 30], [36, 1]]

rotateしてズラしたから、上記の結果も一番はじめの要素が一番最後の要素になって移動してしまっている。flattenしてから今度は逆方向にrotateすることで、一番はじめの要素が一番はじめに戻ってくる。

$ echo "$a" | ruby -e 'arr = readlines.map(&:to_i); arr.zip(arr.rotate).select{|x| x[0] + 1 != x[1]}.flatten.rotate(-1).each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join("..")}'
1..3
7..9
15
30..36

実用例

連番を含む数値が大量にあれば、それを資料に記載したり人に伝えたりする際に、Range化すると可読性が上がるため便利。

他にも次のような実用例が考えられる。

  • 開発サーバで複数のアプリがポートを分けて常駐している
  • 一時的にiptablesでアプリのポートを解放し、外部からアクセスさせたい

今回はアプリがNode.jsだとする。netstatの結果から起動しているアプリのポートの一覧を取得し、先ほど書いたワンライナーに食わせる。

$ ports=$(netstat -antp | grep `hostname -i` | grep node | awk '{sub(/.*:/,"",$4); print $4}' | sort -u)
$ echo "$ports"
7060
7061
7062
7063
7064
7065
7066
7067

ポートの一覧は上記の通り。
/etc/sysconfig/iptablesに記載する文字列をワンライナーで生成する。

$ echo "$ports" | ruby -e 'arr = readlines.map(&:to_i); arr.zip(arr.rotate).select{|x| x[0] + 1 != x[1]}.flatten.rotate(-1).each_slice(2){|a| puts a[0] == a[1] ? a[0] : a.join(":")}' | xargs -I% echo -A RH-Firewall-1-INPUT -m multiport -p tcp --dports % -j ACCEPT
-A RH-Firewall-1-INPUT -m multiport -p tcp --dports 7060:7067 -j ACCEPT