配列と文字列のメソッド、配列とハッシュの並べ替え


配列処理メソッド

「基礎プログラミングI」では配列の基礎について習った。今回は配列の操作を行う際に役に立ついくつかのメソッドを新たに紹介する。


文字列処理メソッド

以前も説明したとおり、Rubyプログラム中の文字列は文字が入っている配列のように考えることができる。実際に配列と文字列で共通しているメソッドが多くて、上記に説明したメソッドの中次のものは文字列に対しても利用できる。

文字列固有のメソッド


配列の要素の並べ替え

「基礎プログラミングI」の第5回では配列に備わっているメソッドの sortreverse について習った。

sort または sortreverse の組み合わせを使って、数値や文字列を格納した配列の要素を小さい順(昇順)あるいは大きい順(降順)に整列することができる。

 練習問題 
letters = ["d", "う", "b", "お", "あ", "c", "a"]
numbers = [7, 5, 1, 22]
p letters.sort # -> 結果: ["a", "b", "c", "d", "あ", "う", "お"] 
numbers.sort!.reverse! # -> numbers は [22, 7, 5, 1] になる 
p numbers

独自規準のソート(ブロック付きsort, sort_by)

数値の大小や文字列の辞書順以外の条件を使って並べ替えを行いたい場合は、sortsort! にはブロック{ })を付けてその中で独自のソート基準を指定することができる。

配列名.sort {|要素1, 要素2| 要素1と要素2の比較基準}

具体的な例で見てみよう。例えば、複数の文字列を格納した配列をその文字列の長さの昇順で整列したい場合は以下のように記述すれば良い。

 練習問題 
yoda = ["your", "father", "I", "am"]
vader = yoda.sort {|a,b| a.size <=> b.size}
p vader # -> 結果: ["I", "am", "your", "father"] 

sort {|a,b| a.size <=> b.size} は「任意の配列要素 a とその次の要素 b の長さ(size)を比較した時、b の方が長いように並べ替えよう」という意味である。 ブロックの中で配列の要素を表している a,b は違う変数名でも構わない。

ソートの比較基準の記述で使われている「<=>」とはRubyにおける比較演算子の一つで、比較する値の大小によって異なる結果を整数として返す:

降順でソートしたい場合は、比較基準の中で配列要素を表す変数を逆に書けば良い。

 練習問題 
yoda = ["your", "father", "I", "am"]
vader_rev = yoda.sort {|a,b| b.size <=> a.size}
p vader_rev # -> 結果: ["father", "your", "am", "I"] 

昇順でのソートの場合は sort_by というもう一つのメソッドでも同じ結果が得られる。

 練習問題 
yoda = ["your", "father", "I", "am"]
vader = yoda.sort_by {|x| x.size} # sort {|a,b| a.size <=> b.size} と同じ結果 
p vader # -> 結果: ["I", "am", "your", "father"] 
vader_rev = yoda.sort_by {|x| x.size}.reverse # sort {|a,b| b.size <=> a.size} と同じ結果 
p vader_rev # -> 結果: ["father", "your", "am", "I"] 

比較基準の記述で利用できるのは配列要素に備わっているメソッドだけではなく、自分で定義したメソッドも活用できる。次の例では、単語が含むひらがな文字の数を調べるメソッドを作ってその結果をソート基準として用いる。

 練習問題 
def count_hiragana(word) # word が含むひらがな文字の数を調べるメソッド 
  count = 0 # ひらがな文字を数えるための変数
  word.each_char do |char| # each_charメソッドは文字列から各文字を順次に取り出す
    if /[ぁ-ん]/ =~ char # ひらがな文字かどうかの確認
      count += 1
    end
  end
  return count
end

words = ["ご飯", "パン", "じゃがいも", "食べる"]
sorted = words.sort_by {|w| count_hiragana(w)} # または sort {|a,b| count_hiragana(a) <=> count_hiragana(b)} 
p sorted # -> 結果: ["パン", "ご飯", "食べる", "じゃがいも"] 

複数の基準による並べ替え(マルチソート)

下記のプログラムでは、各配列要素を3で割った時の剰余をソート基準にしている。

 練習問題 
numbers = [0,1,2,3,4,5,6,7,8,9,10,100,120,140,160,180,200,300,400,500,650,800,950]

puts "<x> <x%3>"
for n in numbers.sort_by {|x| x%3}
  printf("%3d %5d\n", n, n%3)
end

実行結果:

<x> <x%3>
  0     0
  9     0
180     0
  3     0
  6     0
300     0
120     0
400     1
  1     1
 10     1
100     1
  7     1
  4     1
160     1
140     2
  8     2
200     2
  5     2
  2     2
500     2
650     2
800     2
950     2

剰余が同じである配列要素同士はさらに数値自体の大小で並べ替えた方が読みやすいだろう。このような場合には複数のソート基準を指定できる。プログラムを改善しよう。

 練習問題 
numbers = [0,1,2,3,4,5,6,7,8,9,10,100,120,140,160,180,200,300,400,500,650,800,950]

puts "<x> <x%3>"
for n in numbers.sort_by {|x| [x%3, x]} # または sort {|a,b| [a%3, b] <=> [a%3, b]} 
  printf("%3d %5d\n", n, n%3)
end

実行結果:

<x> <x%3>
  0     0
  3     0
  6     0
  9     0
120     0
180     0
300     0
  1     1
  4     1
  7     1
 10     1
100     1
160     1
400     1
  2     2
  5     2
  8     2
140     2
200     2
500     2
650     2
800     2
950     2

ハッシュの要素の並べ替え

ハッシュに対しても sort メソッド、sort_by メソッドで並べ替えができる。ただし、並べ替えの結果はハッシュでなく配列になるので注意が必要である。

例:

 練習問題 
alt = {"富士山" => 3776, "鳥海山" => 2236, "月山" => 1984}
sorted = alt.sort
p sorted # -> 結果: [["富士山", 3776], ["月山", 1984], ["鳥海山", 2236]] 

ハッシュの各要素が配列 [キー, ] になった要素の配列に変換されて、key に基づいて並べ替えが行われた。

ブロックを使った方法

ハッシュ.sort {|a, b| ソート基準}

この場合、ハッシュの各要素が「a=[キー, ]」と「b=[キー, ]」に代入されるので、キーは a[0]b[0]、値は a[1]b[1] でアクセスできる。

また、

ハッシュ.sort_by {|x| ソート基準}

の場合は、ハッシュの各要素が「x=[キー, ]」に代入されるので、キーは x[0]、値は x[1] になる。

例:

 練習問題 
alt = {"富士山" => 3776, "鳥海山" => 2236, "月山" => 1984}
sorted = alt.sort_by {|x| x[1]} # value に基づいて並べ替える => [["月山", 1984], ["鳥海山", 2236], ["富士山", 3776]] 
for k, v in sorted
  printf("%3s は %dm\n", k, v)
end

実行結果:

月山 は 1984m
鳥海山 は 2236m
富士山 は 3776m


本日の課題

 基本課題 

今回習ったことを使って、ユーザーが指定した条件のもとで辞書項目を整列するプログラム sort_dictionary.rb を作成せよ。

データ: dictionary.txt

(出典:Noah Webster著, 「American dictionary of the English language」(1828))

データは各行が  単語 : 定義文  というフォーマットになっている。 プログラムではそれを 単語 が key で、定義文(単語の意味の説明)が value のハッシュとして読み込み、ユーザーが指定した条件に沿ってハッシュの並べ替えをすること。

具体的には、ユーザーが入力した単語がそれぞれの辞書項目の定義文に出現している頻度の大きい順にソートし、その順番で並べたハッシュのキーと問い合わせ単語の出現頻度を表示すること。ここで、特定の単語がある辞書項目の定義文に出現している「頻度」の定義とは、定義文が格納された文字列に対して downcase.split(/\s/) をした結果として返される配列において、その単語に等しい文字列が入っている要素の数とする(downcase は例えば 「The」と「the」を同じ単語として数えるためである)。

実行結果の例:

{c11xxxx}% ruby sort_dictionary.rb
整列の条件として使う単語を入力してください。
of
<辞書項目> <of の出現頻度>
   evening       12
   morning        7
     night        5
  forenoon        4
      noon        2
  midnight        1
after-noon        1

ヒント:問い合わせ単語の出現頻度を計算する手順はメソッドとして定義し、ソートに指定するブロックの中と結果を表示するところからそのメソッドを呼び出すこと。

 発展課題 

  1. 項目ごとの出現頻度の平均値が最大である単語を探して結果を表示するようにプログラムを改良せよ。
  2. 問い合わせ単語の出現頻度は複数の辞書項目において同じ場合があるので、単語の頻度でソートした後さらに辞書順でソートするようにプログラムを改良せよ。
  3. split(/\s/) だけをすれば定義文中の単語に記号(カンマやピリオドなど)が付いている場合は別の単語の扱いになってしまう(例えば「night」の頻度を調べる場合は「night.」や「night;」はカウントされない)。改善せよ。

目次