ハッシュは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番をやるためには正規表現の知識が必要である。どちらを行ってもかまわない。
制限時間は10分。出席点は2点。提出要領は下記の通り。
Tips:emacsでの日本語入力のオンオフはCtrl-oです
Tips:Mewによるメールの送り方はMewコマンドを参照
ハッシュのvalueに配列を用いることができることから、ハッシュを利用するに当たっても、配列を処理するための各種メソッドを活用することができる。配列処理メソッドとしてこれまで利用してきたのはlengthのみであったが、他にも多数のメソッドがある。ここでまとめてみよう。
プログラム中に書く「具体的な値」のことをリテラルと呼ぶ。リテラル配列では具体的な値をカンマで区切って表記する。例えば
x=[100,60,20,"xyz"]
では、xが要素として1、2、3、xyzを含む配列であることを意味している。
決まった長さの初期配列や、決められた値で埋め尽くさされた初期配列を作ることもできる。この場合は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]
引数に指定した配列をもとの配列の末尾に破壊的に結合する。
a = [50,20,80,10]、b = [30,90,60]の場合、
a.concat(b)
p a #=> [50,20,80,10,30,90,60]
となり、配列aの値が変化する。bは変化しない。
配列の中の値である要素を順番に取り出す方法としてeachメソッドがある。
a.each do |i|
〜配列aの要素をiに順番に代入しながら処理を繰り返し実施する
end
eachメソッドのかわりにwhile文やfor文を使って書くこともできる。
indexメソッドは指定した配列に引数valと等しい値があるか調べ、最初に見つかった位置のインデックスを返す。見つからない場合はnilを返す。
a = [50,20,80,10]の場合、
p a.index(20) #=>1
p a.index(70) #=>nil
lengthメソッドおよびsizeメソッドは配列の長さ(要素の数)を返す
a = [50,20,80,10]の場合、
p a.length #=>4
p a.size #=>4
配列の各要素の間に文字列sepを挟んで連結した文字列を返す。sepを省略した場合はそのまま各要素が連結される
a = [50,20,80,10]の場合、
p a.join("/") #=>"50/20/80/10"
p a.join #=>"50208010"
pushメソッドは配列の末尾に引数で指定した要素valを破壊的に追加する。追加する要素は複数指定できる。
a = [50,20,80,10]の場合、
a.push(70, 60)
p a #=>[50,20,80,10,70,60]
破壊的操作なのでaの値が変化する。
配列の末尾の要素を破壊的に取り除き、取り除いた要素の値を返す。要素がない場合にはnilを返す。
a = [50,20,80,10]の場合、
p a.pop #=> 10
これにより配列aは10が取り除かれa=[50,20,80]となる。
配列の要素をすべて逆順に並べ替えた配列を返す
a = [50,20,80,10]の場合、
p a.reverse #=>[10,80,20,50]
p a #=>[50,20,80,10]
破壊的操作ではないのでaの値は変わらない。
配列の要素をすべて破壊的に逆順に並べ替えた配列を返す。
a = [50,20,80,10]の場合、
p a.reverse! #=>[10,80,20,50]
p a #=>[10,80,20,50]
破壊的操作であるためaの値も変化する。
配列の先頭要素を破壊的に取り出しその値を返す。配列内の要素はひとつずつ前につめられる
a = [50,20,80,10]の場合、
p a.shift #=>10
p a #=>[80,20,50]
破壊的操作なのでaの値も変化する。
配列の先頭に要素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!は破壊的メソッドとなる。
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の値も変化する。
配列内の要素を小さい順(昇順)に並び替える。
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という配列を作成し、その中に選択肢を入れ込んでいる。自由記述とした場合、同じ赤でも「赤」「あか」「赤色」など多様な回答方法が可能になり、結果的に赤が何人に選ばれたのか集計をする際に極めて面倒な処理が必要となる。選択肢にすれば各数字が選ばれた回数を調べればよいので、その後の処理が楽になる。
以下のうちいずれかを選んで解答する。次回の授業では問題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メソッドを用いて結果をファイルに出力し、集計時にそのファイルを読み込んで処理を行うように書き換えよ。
Tips:emacsでの日本語入力のオンオフはCtrl-oです
Tips:ktermでのプログラムの実行結果をメールに貼り付けるには、コピーしたい箇所をマウスで選択し、emacs(Mew)上でマウスの真ん中ボタンをクリックする
Tips:Mewによるメールの送り方はMewコマンドを参照