(6) 11/13の授業内容:ハッシュ(2)−ハッシュと配列処理

ハッシュのvalueを配列にする

ハッシュはkeyとvalueを使い、任意の値に任意の値を結びつけ、ペアでデータを保存することができるものであった。では、次のようなデータ(name.txt)をハッシュを適用する場合はどうしたらよいだろうか。

氏  名    年齢 血液型
山田一郎         21      A
佐藤二郎         22      B
加藤三郎         23      B
藤田四郎         24      O
木村五郎         25      A
鈴木六郎         26      O

これをハッシュに代入しようとすると戸惑うことになる。key「山田一郎」のvalueが「21とA」という2つの値になっているためである。

このような場合でもハッシュに代入することが可能である。keyとvalueは1対1の対応をしなければならないというわけではなく、1つのkeyに対して複数のvalueを結びつけることができる。つまりvalueを配列にすることができる。上記の例は以下のように書くことができる。

name["山田一郎"] = [21, "A"]

Aは文字列なので""をつけており、21は数字なので""をつけていない。name.txtを読み込んでハッシュに代入し、結果を表示するプログラムを書くと以下の通りとなる(onamae.rb)。

#!/usr/koeki/bin/ruby

raw = Hash.new

while line = gets
  if /(\S+)\s+(\d+)\s+(\S+)/ =~ line
    # 1個目の() (\S+)→氏名が入る
    # 2個目の() (\d+)→年齢が入る
    # 3個目の() (\S+)→血液型が入る
    raw[$1] = [$2, $3] # 配列を代入
  end
end

for name, data in raw
  # data には、[年齢, 血液型] という配列が入っている
  printf("%-10sさんは%2d歳で、血液型は%s型です。\n", name, data[0], data[1])
  # dataの第0要素が年齢、第1要素が血液型
end
irsv{学籍番号}% ruby onamae.rb name.txt
鈴木六郎  さんは26歳で、血液型はO型です。
木村五郎  さんは25歳で、血液型はA型です。
藤田四郎  さんは24歳で、血液型はO型です。
加藤三郎  さんは23歳で、血液型はB型です。
佐藤二郎  さんは22歳で、血液型はB型です。
山田一郎  さんは21歳で、血液型はA型です。

配列とは異なり、ハッシュでは値を取り出すときの順番は、読み込んだ順番とは無関係になる。ハッシュはmd5(Message Digest 5)という方法でデータを暗号化して格納する。暗号化されたデータの大きさによって取り出される順番が決まるため、読み込んだ順番と異なることになる。配列はインデックスを使って順番にデータを格納、取出しができるのに対し、ハッシュは任意のkeyに基づいてvalueを取り出すことができるという性質を持つことから、データベースの用途に利用する際は都合が良い方法であるといえる。

参考までにこのプログラムを配列で書くと次のようになる(onamae-array.rb)。

#!/usr/koeki/bin/ruby

name = [] #氏名用の配列
age = []  #年齢用の配列
blood = [] #血液型用の配列
i = j = 0 #配列のインデックス

while line = gets
  if /(\S+)\s+(\d+)\s+(\S+)/ =~ line
    # 1個目の() (\S+)→氏名が入る
    # 2個目の() (\d+)→年齢が入る
    # 3個目の() (\S+)→血液型が入る
    name[i] = $1
    age[i] = $2
    blood[i] = $3
    i += 1
  end
end

while j < i
  printf("%-10sさんは%2d歳で、血液型は%s型です。\n", name[j], age[j], blood[j])
  j += 1
end

出席課題

onamae.rbはname.txtを読み込んで処理を行うプログラムである。以下の2つのうちいずれかの改良を行ってみよう。1番はif文の知識が必要であり、2番をやるためには正規表現の知識が必要である。どちらを行ってもかまわない。

  1. 血液型がA型の人のみ結果を表示する。
  2. name.txtに星座を追加する。結果を表示する際、年齢と血液型に加えて星座も表示する。

制限時間は10分。出席点は2点。提出要領は下記の通り。

  • 提出先:naoya@e.koeki-u.ac.jp
  • メールのSubject:ruby06
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降に変更したプログラムと実行結果を貼り付け、変更点について説明する。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:Mewによるメールの送り方はMewコマンドを参照

配列の生成と配列処理メソッド

ハッシュのvalueに配列を用いることができることから、ハッシュを利用するに当たっても、配列を処理するための各種メソッドを活用することができる。配列処理メソッドとしてこれまで利用してきたのはlengthのみであったが、他にも多数のメソッドがある。ここでまとめてみよう。

配列生成(リテラル配列)

プログラム中に書く「具体的な値」のことをリテラルと呼ぶ。リテラル配列では具体的な値をカンマで区切って表記する。例えば
x=[100,60,20,"xyz"]
では、xが要素として1、2、3、xyzを含む配列であることを意味している。

配列生成(Array.new)

決まった長さの初期配列や、決められた値で埋め尽くさされた初期配列を作ることもできる。この場合はArray.newを使用する。以下のような書き方がある。
a=Array.new(長さ)
a=Array.new(長さ,初期値)

例えば、a=Array.new(5)の場合は、[nil,nil,nil,nil,nil]のように各要素の初期値がnilの配列が生成される。nilはデータがないという意味。a=Array.new(5,10)の場合は[10,10,10,10,10]という配列が生成される。

配列の連結(+メソッド)

+メソッドは基の配列と別の配列をつなげた新しい配列を返す。
a = [50,20,80,10]、b = [30,90,60]の場合、
c = a + b
p c #=> [50,20,80,10,30,90,60]

配列cを作成した後でも配列aおよびbの値は変化しない。

配列末尾への破壊的要素追加(<<メソッド)

<<メソッドは、配列の末尾に新しい要素を追加する。元の配列自体が変更する。元の値を直接書き換える操作を破壊的操作と言う。
a = [50,20,80,10]の場合、
a << 70
p a #=> [50,20,80,10,70]

配列の破壊的結合(concatメソッド)

引数に指定した配列をもとの配列の末尾に破壊的に結合する。
a = [50,20,80,10]、b = [30,90,60]の場合、
a.concat(b)
p a #=> [50,20,80,10,30,90,60]

となり、配列aの値が変化する。bは変化しない。

要素を順番に取り出す(eachメソッド)

配列の中の値である要素を順番に取り出す方法としてeachメソッドがある。
 a.each do |i|
  〜配列aの要素をiに順番に代入しながら処理を繰り返し実施する
 end

eachメソッドのかわりにwhile文やfor文を使って書くこともできる。

指定要素の発見(index(val)メソッド)

indexメソッドは指定した配列に引数valと等しい値があるか調べ、最初に見つかった位置のインデックスを返す。見つからない場合はnilを返す。
a = [50,20,80,10]の場合、
p a.index(20) #=>1
p a.index(70) #=>nil

配列の長さを返す(lengthもしくはsizeメソッド)

lengthメソッドおよびsizeメソッドは配列の長さ(要素の数)を返す
a = [50,20,80,10]の場合、
p a.length #=>4
p a.size #=>4

配列を結合して文字列化(join(sep)メソッド)

配列の各要素の間に文字列sepを挟んで連結した文字列を返す。sepを省略した場合はそのまま各要素が連結される
a = [50,20,80,10]の場合、
p a.join("/") #=>"50/20/80/10"
p a.join #=>"50208010"

末尾への破壊的追加(push(val1,val2,...)メソッド)

pushメソッドは配列の末尾に引数で指定した要素valを破壊的に追加する。追加する要素は複数指定できる。
a = [50,20,80,10]の場合、
a.push(70, 60)
p a #=>[50,20,80,10,70,60]

破壊的操作なのでaの値が変化する。

末尾要素の破壊的取り出し(popメソッド)

配列の末尾の要素を破壊的に取り除き、取り除いた要素の値を返す。要素がない場合にはnilを返す。
a = [50,20,80,10]の場合、
p a.pop #=> 10
これにより配列aは10が取り除かれa=[50,20,80]となる。

配列の逆順への並び替え(reverseメソッド)

配列の要素をすべて逆順に並べ替えた配列を返す
a = [50,20,80,10]の場合、
p a.reverse #=>[10,80,20,50]
p a #=>[50,20,80,10]

破壊的操作ではないのでaの値は変わらない。

配列の破壊的な逆順への並び替え(reverse!メソッド)

配列の要素をすべて破壊的に逆順に並べ替えた配列を返す。
a = [50,20,80,10]の場合、
p a.reverse! #=>[10,80,20,50]
p a #=>[10,80,20,50]

破壊的操作であるためaの値も変化する。

先頭要素の破壊的な取り出し(shiftメソッド)

配列の先頭要素を破壊的に取り出しその値を返す。配列内の要素はひとつずつ前につめられる
a = [50,20,80,10]の場合、
p a.shift #=>10
p a #=>[80,20,50]

破壊的操作なのでaの値も変化する。

先頭への要素の破壊的追加(unshift(val)メソッド)

配列の先頭に要素valを破壊的に追加し、その配列を返す。配列内の要素はひとつずつ後ろにずれる。
a = [50,20,80,10]の場合、
p a.unshift(70) #=>[70,10,80,20,50]
p a #=>[70,10,80,20,50]

破壊的操作なのでaの値も変化する。

重複要素の削除(uniqとuniq!メソッド)

配列から重複した要素を取り除き、取り除いた部分を前につめた配列を返す。uniqは破壊的メソッドではないが、uniq!は破壊的メソッドとなる。
c = [10,80,20,50,30,20,50,10,80]の場合
p c.uniq #=>[10,80,20,50,30]
p c #=>[10,80,20,50,30,20,50,10,80]

c = [10,80,20,50,30,20,50,10,80]の場合
p c.uniq! #=>[10,80,20,50,30]
p c #=>[10,80,20,50,30]

uniq!は破壊的操作なのでcの値も変化する。

小さい順に並び替える(sortメソッド)

配列内の要素を小さい順(昇順)に並び替える。
a = [50,20,80,10]の場合、
p a.sort #=>[10,20,50,80]
p a #=>[10,80,20,50]

破壊的操作ではないのでaの値は変化しない。配列内の要素が文字の場合文字コードの小さい順に並び替えを行う。

ハッシュを用いたアンケート

ハッシュを用いてアンケートのデータ収集をしてみよう。以下は好きな色の調査を行うプログラムである。色のリストをあらかじめ作成しておき、そのリストの中から好きな色を1位から3位まで選んでもらうものである。複数の人が連続して回答することができ、最終的に結果の一覧を表示する。プログラム名をcolor_choice.rbとして保存し、実行してみよう。なお、このプログラムは\nが多数出現している。このためいつも通りemacsに貼り付けてから円マークを書き直しているとものすごく時間がかかってしまう。今回はあらかじめプログラムのファイルをアップロードしてある。上記のファイル名はハイパーリンクとなっている。ここを右クリックしてメニューより対象ファイルの保存を選び、ruby用のディレクトリ内に保存する。

#!/usr/koeki/bin/ruby

list = ["赤","青","黄","緑","茶","紫","朱鷺","萌黄","白","黒"]

raw = Hash.new

while true

  print "好きな色の調査をします。\n"
  print "ご協力いただける場合は1を、そうでない場合は0を\n"
  print "入力して下さい。:"
  answer = gets.chomp!

  if answer == "1"
    print "ご協力ありがとうございます\n\n"
  elsif answer == "0"
    print "どうもありがとうございました\n"
    break
  else
    print "\n1か0のどちらかでお願いします。\n\n\n"
    redo   
  end

  print "先ずはお名前を入力して下さい(ニックネームでも可):"
  namae = gets.chomp!

  print "つづけて性別を入力して下さい(m もしくは f):"
  seibetsu = gets.chomp!

  print "以下が色のリストです。好きな色を3つ選び\n"
  print "1位、2位、3位の順に左横の番号でお答え下さい。\n\n"

  1.upto(list.length) do |i|
    printf ("%d %s\n",i,list[i-1])
  end

  print "\n\n"

  print "1番好きな色はどれですか:"
  no1 = gets.chomp!.to_i
  print "2番目に好きな色はどれですか:"
  no2 = gets.chomp!.to_i
  print "3番目に好きな色はどれですか:"
  no3 = gets.chomp!.to_i

  print "これで終了です。ご協力どうもありがとうございました。\n\n"

  raw[namae] = [seibetsu,no1,no2,no3]

end

print "--名前------1位---2位---3位---\n"
for name, data in raw
  printf ("%-12s%-6s%-6s%-6s\n",name,list[data[1]-1],list[data[2]-1],list[data[3]-1])
end

このプログラムについて、重要な部分を説明する。一見難しそうなプログラムに見えるが単に長いだけである。同じようなことを何度も繰り返しやっているだけであり、じっくり眺めれば多くの部分は理解できるはずである。なお、このプログラムでは冒頭でlistという配列を作成し、その中に選択肢を入れ込んでいる。自由記述とした場合、同じ赤でも「赤」「あか」「赤色」など多様な回答方法が可能になり、結果的に赤が何人に選ばれたのか集計をする際に極めて面倒な処理が必要となる。選択肢にすれば各数字が選ばれた回数を調べればよいので、その後の処理が楽になる。

  • redo:while-end内に配置し、その回のループを最初からやり直す。無効な値が入力された場合などに利用する。
  • raw[namae] = [seibetsu,no1,no2,no3]:ハッシュに値を代入している。keyをnamaeとし、valueにはseibetsuとno1、no2、no3の3つの値を指定している。
  • list[data[1]-1]:複雑に見えるが、配列変数listのインデックスをdata[1]-1としている。data[1]には1番好きな色の番号が代入されているのでそこから1を引いているだけである。

レポート課題

以下のうちいずれかを選んで解答する。次回の授業では問題2を発展させていくため、可能であれば問題2もしくは問題3に取り組んでみること。

問題1(7点満点):color_choice.rbのように何かを3つ選んでもらい、結果を出力するプログラムを作成する。color_choice.rbを改良しても良いし、最初から自分で書いても良い。独自性の高いテーマを設定すること。プログラムの中では必ずハッシュを利用すること。

問題2(8点満点):問題1を行った上で、最後に1位を3点、2位を2点、3位を1点とし、全員分の結果を集計して表示するという機能を追加する。表示する順番は特に変更する必要はない(=得点の高い順にしなくて良い)。

問題3(10点満点):問題2と同様の条件でプログラムを作成するが、集計結果を求める際に前回や前々回のプログラム実行時に入力したデータも活用できるようにせよ。すなわちopenメソッドを用いて結果をファイルに出力し、集計時にそのファイルを読み込んで処理を行うように書き換えよ。


  • 提出先:naoya@e.koeki-u.ac.jp
  • 提出期限:11/19(日)23:59
  • メールのSubject:自分の学籍番号-kadai04
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降は下記の構成とする
  1. 作成したプログラム
  2. (問題3を選んだ人は)出力したデータ
  3. プログラムの実行結果
  4. プログラムの説明
  5. 感想

  • 採点基準:期限内提出点(2点)、メールの体裁(1点)、プログラム(2〜4点)、独自性(1点)、プログラムの説明(2点)
  • プログラムの説明:取り上げたテーマ(何について調べるプログラムなのか)、入力データのハッシュへの代入方法、ハッシュ内のデータを用いた結果表示方法について説明すること。問題2や3を行った人は新たに追加した部分についての説明もあわせて行うこと。
  • 独自性について:今回も自分で自由にテーマを決めることができるため、他の人と全く同じプログラムになる可能性は低いと考えられる。ほかの人と同じプログラムの場合は0点、ほかに同じプログラムを書いた人がいない場合は1点とする。
  • 説明に関して、文章の意味がわかりづらい場合や、Webページを単にコピー&ペーストしたものは減点することがある。一度読み直してから提出すること。
  • 驚異的に良くできているレポートについては満点を超える得点をつけることがある。
  • よくできていたレポートは、他の人の参考になるよう、本人が特定できないような形で掲載する。掲載してほしくない場合はメールでの課題提出時にその旨記載すること。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:ktermでのプログラムの実行結果をメールに貼り付けるには、コピーしたい箇所をマウスで選択し、emacs(Mew)上でマウスの真ん中ボタンをクリックする

Tips:Mewによるメールの送り方はMewコマンドを参照