様々な条件での並べ換え

ソート(並べ換えること)は、大量のデータ処理には欠かせない。 数値だけが並ぶ配列であればsortメソッドでソートできるが、 複雑な配列やハッシュを様々な条件で並べ換えるときに有用な sort_by の利用方法に慣れておこう。

以下の説明では分かりやすくするため、 ソート対象となる配列(またはハッシュ)に初期値を代入しているが、 実践プログラムではプログラム起動時に 入力されたデータなどを元にその都度変わる。

一般的なソート

たくさんの値が要素として配列に格納されている場合、それらを 昇順にソートしたものを得たいならたんにsortメソッドを 呼ぶだけでよい。

x = [9, 20, 10, 15, 8]
y = x.sort
y
 => [8, 9, 10, 15, 20]
x = ["orange", "apple", "strawberry", "pear"]
y = x.sort
y
 => ["apple", "orange", "pear", "strawberry"]

数値でも文字列でも昇順に並べ換えられる。降順にしたい場合は 結果を reverse で反転すればよい。

x = [9, 20, 10, 15, 8]
y = x.sort.reverse
y
 => [20, 15, 10, 9, 8]
foo = ["orange", "apple", "strawberry", "pear"]
bar = foo.sort.reverse
bar
 => ["strawberry", "pear", "orange", "apple"]

独自規準のソート

降順順、昇順以外の複雑な並べ換えもできる。 たとえば、次のようなCSVデータの並べ換えを考える。

ja-math.csv

氏名,国語,数学
山田太郎,50,40
中町太郎,90,70
飯森花子,91,90
鶴岡一人,60,50
酒田三吉,52,80
三川一二三,12,96

このCSVファイルを

score = CSV.read("ja-math.csv", headers:true)

として読み込むと概念的に以下のような構造で読み込まれる(実際には CSV::Tableの値だがハッシュの配列と同じ性質を持つ)。

score = [			# 内部にハッシュを複数持つ配列
  {"氏名" => "山田太郎",	"国語"=> 50, "数学"=>40},
  {"氏名" => "中町太郎",	"国語"=> 90, "数学"=>70},
  {"氏名" => "飯森花子",	"国語"=> 91, "数学"=>90},
  {"氏名" => "鶴岡一人",	"国語"=> 60, "数学"=>50},
  {"氏名" => "酒田三吉",	"国語"=> 52, "数学"=>80},
  {"氏名" => "三川一二三",	"国語"=> 12, "数学"=>96},
]
score.length			# 要素数は6
 => 6
# たとえば添字番号2の要素は "飯森花子" 行の値となる

このようなときに「6つの要素を「国語」キーの値(つまり国語の点)の 高い順に並び換える」ことを考える。

普通の sort メソッドは各要素どうしをそのまま直接比較して ソートするが、要素そのままではなくハッシュのキーを取り出した「値」で 比べることを指定する。そのために使うのが sort_by メソッドで、以下のように用いる。

配列.sort_by{|変数| 変数から比較すべきものを取り出す式}

score 変数の例の場合では、たとえば

x = score[2]

とした場合に、x から国語の点数を取り出すには、 x["国語"].to_i とすればよいことから、

score.sort_by{|x| x["国語"].to_i}

とすることで国語の点数で昇順にした配列が得られ、さらにこれを reverse メソッドに渡して降順の結果が得られる。

score.sort_by{|x| x["国語"].to_i}.reverse
 =>
 [#<CSV::Row "氏名":"飯森花子" "国語":"91" "数学 ":"90">,
  #<CSV::Row "氏名":"中町太郎" "国語":"90" "数学":"70">,
  #<CSV::Row "氏名":"鶴岡一人" "国語":"60" "数学":"50">,
  #<CSV::Row "氏名":"酒田三吉" "国語":"52" "数学":"80">,
  #<CSV::Row "氏名":"山田太郎" "国語":"50" "数学":"40">,
  #<CSV::Row "氏名":"三川一二三" "国語":"12" "数学":"96">]

このソート結果を順序よく出力するには、each で1つずつ取り出して処理する。

score.sort_by{|x| x["国語"].to_i}.reverse.each do |i|
  ……出力処理……
end

以上をまとめたプログラムを示す。

kokugo-rank.rb

#!/usr/bin/env ruby
# coding: utf-8
require "./kprintf.rb"
require 'csv'

# ja-math.csv: 氏名,国語,数学
score = CSV.read("ja-math.csv", headers:true)

puts "元の並び:"
print "--氏名--------------+-国語-+-数学-+-合計--\n"
score.each do |row|
  name, ja, math = row["氏名"], row["国語"], row["数学"]
  total = ja.to_i + math.to_i
  printf("%-20s %5d  %5d  %5d\n", name, ja.to_i, math.to_i, total)
end
puts "-"*42

puts "国語上位から:"
print "--氏名--------------+-国語-+-数学-+-合計--\n"
score.sort_by{|x| x["国語"].to_i}.reverse.each do |row|
  name, ja, math = row["氏名"], row["国語"], row["数学"]
  total = math.to_i + ja.to_i
  printf("%-20s %5d  %5d  %5d\n", name, math.to_i, ja.to_i, total)
end
puts "-"*42

以下の出力が得られる。

./kokugo-rank.rb
元の並び:
--氏名--------------+-国語-+-数学-+-合計--
山田太郎                50     40     90
中町太郎                90     70    160
飯森花子                91     90    181
鶴岡一人                60     50    110
酒田三吉                52     80    132
三川一二三              12     96    108
------------------------------------------
国語上位から:
--氏名--------------+-国語-+-数学-+-合計--
飯森花子                90     91    181
中町太郎                70     90    160
鶴岡一人                50     60    110
酒田三吉                80     52    132
山田太郎                40     50     90
三川一二三              96     12    108
------------------------------------------

※発展※ →ハッシュ値をまるごとソート


本日の目次