roy > naoya > 基礎プログラミングII[月2] > (3)配列の復習とARGV

(3) 10/22の授業内容:配列の復習とARGV

配列の復習への導入

5回の課題の平均点を計算するケースについて考えてみる。このプログラムは既に学んでいる繰り返し表現を用いることで記述可能である(sample.rb)。

#!/usr/koeki/bin/ruby

number = 0
times = 5
sum = 0

while number < times
  printf ("%d回目の得点を入力してください:",number+1)
  score = gets.chomp!.to_f
  sum += score
  number += 1
end

average = sum / number

printf ("平均点は%5.2f点です\n",average)

このプログラムを実行すると以下のようになる。ここで黄色の部分がユーザーが入力した値である。

irsv{naoya}% ruby sample.rb[Return]
1回目の得点を入力してください:30[Return]
2回目の得点を入力してください:40[Return]
3回目の得点を入力してください:50[Return]
4回目の得点を入力してください:60[Return]
5回目の得点を入力してください:70[Return]
平均点は50.00点です。

このプログラムではユーザーが入力した個別の得点を記憶しておくことができない。入力をした各回の得点は全てscoreに代入される。このため2回目の得点を入力すれば1回目の得点を忘れ、3回目の得点を入力すれば2回目の得点を忘れることになる。このプログラムでは各回の得点を覚えておく必要性はないが、前期の授業で取り上げたようなレジのプログラムでは、最後にレシートを作成する必要があり、個々の値を記憶させておく必要が生じる。このような場面で役に立つのが配列であった。

配列の復習

変数には通常1つの値しか代入しておくことができない。以下のケースでは、x = 10とすることで、以前xに代入されていた5は忘れてしまい、xの値は10となる。このように新しい値を代入すると、前に代入されていた値を忘れてしまうというのが通常の変数の特性である。

x = 5
print x,"\n"   #=>5
x = 10
print x,"\n"   #=>10

これに対して、通常の変数を拡張して、複数の値を入れられるようにしたものが配列である。

x = 150:これまでの変数(1つの値のみ保存可能)

y = [150, 200, 380, 160, 240, 400]:配列(複数の値を保存可能)

上記ではyという配列変数には6個の値が代入されている。配列変数は複数の値を,(カンマ)で区切って保存し、かつ全ての値を[]内に格納している。配列の中に代入されている値の呼び方について、単にyに代入されている値とすると、6個のうちのどれを指しているのかがわからなくなる。このため、配列変数を使用する場合は、配列内の何番目の値を表示するのか、または配列内の何番目に値を代入するのかを示す必要がある。

  • 150:yの中の0番目のデータ
  • 200:yの中の1番目のデータ
  • 380:yの中の2番目のデータ
  • 160:yの中の3番目のデータ
  • 240:yの中の4番目のデータ
  • 400:yの中の5番目のデータ

1番目からではなく0番目から開始していることに注意しよう。

次に0番目のデータや1番目のデータをRubyの記法にしたがって記述する方法を見てみよう。

  • yの中の0番目のデータ:y[0]
  • yの中の1番目のデータ:y[1]
  • yの中の2番目のデータ:y[2]
  • yの中の3番目のデータ:y[3]
  • yの中の4番目のデータ:y[4]
  • yの中の5番目のデータ:y[5]

配列の中の何番目の値であるかは、変数名[n]で表現することができる。一番最初の値であれば変数名[0]、2番目は変数名[1]となる。[]の中の数字のことを添字もしくはインデックスと呼ぶ。

sample.rbを配列を用いて書き直してみよう(sample2.rb)。

#!/usr/koeki/bin/ruby

score = []
number = 0
times = 5
sum = 0

while number < times
  printf ("%d回目の得点を入力してください:",number+1)
  score[number] = gets.chomp!.to_f
  sum += score[number]
  number += 1
end

average = sum / number

printf ("平均点は%5.2f点です\n",average)

冒頭でscore = []とし、scoreが配列である旨宣言している。そして、sample.rbでscoreであった場所をscore[number]に変更している。[]内はインデックスを指定する場所であるが、ここではnumberという初期値が0の変数を用いている。このためscore[number]score[0]と書いてあるのと同義である。while-endの繰り返しを行う中でnumber+=1を行っているため、繰り返しを行うたびにnumberの値は1、2、3と1ずつ増加していく。これによりユーザが入力した値はscore[1]score[2]score[3]に順番に代入されることになる。

このプログラムを実行すると、結果はsample.rbと変わらないが、sample2.rbでは以下のように5回分の得点が全て記憶されている(30、40、・・・の数字はユーザが入力した値で、実際は何でも良い)。

score

[0]

[1]

[2]

[3]

[4]

30

40

50

60

70

scoreを配列にすることで各回の得点を全て記憶しているということのメリットは、次のようなプログラムを書くことで明らかとなる。ここでは上記のプログラムに黄色の部分を追加している。この部分を追加することで各回の得点を全て表示することが可能となる。

#!/usr/koeki/bin/ruby

score = []
number = 0
times = 5
sum = 0

while number < times
  printf ("%d回目の得点を入力してください:",number+1)
  score[number] = gets.chomp!.to_f
  sum += score[number]
  number += 1
end

average = sum / number

1.upto(number) do |i|
  printf ("%d回目の得点は%d点でした\n",i,score[i-1])
end

printf ("平均点は%5.2f点です\n",average)

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

配列処理メソッドとしてこれまで利用してきたのは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 #=>50
p a #=>[20,80,10]

破壊的操作なのでaの値も変化する。
先頭への要素の破壊的追加(unshift(val)メソッド)
配列の先頭に要素valを破壊的に追加し、その配列を返す。配列内の要素はひとつずつ後ろにずれる。
a = [50,20,80,10]の場合、
p a.unshift(70) #=>[70,50,20,80,10]
p a #=>[70,50,20,80,10]

破壊的操作なので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とsort!メソッド)
配列内の要素を小さい順(昇順)に並び替える。sortは破壊的メソッドではないが、sort!は破壊的メソッドとなる。
a = [50,20,80,10]の場合、
p a.sort #=>[10,20,50,80]
p a #=>[10,80,20,50]

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

コマンドラインとの情報のやりとり

ktermにおいてrubyプログラムを実行する際、

irsv{学籍番号}% ./prog.rb[Return]

とか

irsv{学籍番号}% ruby prog.rb[Return]

と入力した。

特殊な方法として、

irsv{学籍番号}% ruby prog.rb data.txt[Return]

のようにプログラム名の後ろにファイル名を指定すると、そのファイルを読み込ませ、プログラムの中で取り扱うことができた。

ktermからプログラムを実行する際に、プログラム名の後ろに値を指定することで、その値をプログラム内で使用することができる。前期はファイル名のみを指定したが、以下のように値を指定することもできる。

irsv{学籍番号}% ruby prog.rb 100 200 300[Return]

ktermからプログラムを実行する際に上記のように与えた値のことを引数(ひきすう)という。Rubyでは、プログラム実行時に与えた引数は自動的にARGVという配列変数に代入される。

上記の例では100、200、300という引数が与えられており、配列変数のARGVのインデックス0、1、2にそれぞれ
ARGV[0] = "100"
ARGV[1] = "200"
ARGV[2] = "300"
のように代入される。いずれも文字列であることに注意する必要がある。なお100 200 300の間はスペースのみでありカンマ等はないことに注意せよ

これを利用すると、プログラム実行時に与えた値に基づいて動作を決定することができる。

ARGVについてまとめ

ktermからプログラムを実行する際に引数を指定すると、ARGVという配列変数に代入される

  • 引数が1つの場合はARGV[0]に代入される
  • 引数が2つの場合は1つ目の値がARGV[0]、2つ目の値がARGV[1]に代入される
  • 引数が3つの場合は1つ目の値がARGV[0]、2つ目の値がARGV[1]、3つ目の値がARGV[2]に代入される
  • 引数が増えるごとにARGVのインデックスを1ずつ増やしながら順番に代入される
  • 引数に与えた値はARGVにすべて文字列として代入される
  • 引数として複数の値を与える場合は、引数の間はスペースとする

1からユーザが指定した任意の値までの足し算を行うプログラムを書いてみよう。これまでは、プログラム実行後に「好きな数字を入力してください」と表示し、キーボードから入力された値をgetsメソッドで取得するという以下のようなプログラムを書いてきた(sum-gets.rb)。

#!/usr/koeki/bin/ruby
sum = 0
print "好きな数字を入力してください"
goal = gets.chomp!.to_i

1.upto(goal) do |i|
  sum += i
end

printf ("1から%dまでの合計は%dです\n",goal, sum)

これをARGVを使用して書き換えることを考えてみよう。仮に30までの合計を求める場合、ktermでプログラムを実行する際に以下のように引数を与える必要がある(プログラム名はsum-ARGV.rbとする)

irsv{学籍番号}% ruby sum-ARGV.rb 30[Return]

プログラムは以下の通りとなる。ktermにおけるプログラム実行時に与えた引数である30はARGV[0]に代入される。文字列として取り扱われるため、goal = ARGV[0].to_iとし、整数に変換してからgoalに代入している。

#!/usr/koeki/bin/ruby

sum = 0
goal = ARGV[0].to_i

1.upto(goal) do |i|
  sum += i
end

printf ("1から%dまでの合計は%dです\n",goal, sum)

出席課題

sum-ARGV.rbを実行してみよう。実行するときに

  1. 引数として100を与える
  2. 引数として-5を与える
  3. 引数を与えない

とした場合、それぞれどのような結果になるだろうか。実行結果を確認した上で、なぜそのような結果になるのかを考えてみよう。

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

  • 提出先:naoya@e.koeki-u.ac.jp
  • メールのSubject:attend03
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降にプログラムの実行結果を貼り付け、なぜそのようになったのかについて理由を述べること。

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

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

プログラムの強制終了

sum-ARGV.rbでは、プログラム実行時に引数を与えることが、プログラムを正常に実行する上での前提条件となっている。引数を与えなければ正しく実行することができない。しかし、実行しても何も起こらないプログラムでは、ユーザーは何がおかしいのかを理解することができない。おそらく自分の実行の方法が間違っているのではなく、プログラム自体に誤りがあるのではないかと考えるはずである。この問題を解決するために、プログラムを改良してみよう。

#!/usr/koeki/bin/ruby
if ARGV[0] == nil
  STDERR.print "1からプログラム起動時に指定した値までの合計を出します\n"
  STDERR.print "50までの総和を出したい場合 ruby goukei-ARGV.rb 50と入力します\n"
  exit(1)
end

sum = 0
goal = ARGV[0].to_i

1.upto(goal) do |i|
  sum += i
end

printf ("1から%dまでの合計は%dです\n",goal, sum)

このプログラムでは上部の5行(黄色の部分)を追加している。これは1つのif文である。ifの横の条件としてARGV[0] == nilを指定している。nilとはデータがないという意味であり、ARGV[0]にデータがないということは、プログラムを実行する際に引数を与えていないということを意味する。この場合にendの前にある3行を実行する。

STDERR.printの2行:メッセージをディスプレイに出力する。このメッセージを出すことでユーザはプログラムの実行方法が間違っていたことがわかる。このプログラムに関して言えば、STDERR.printではなくprintでもよい。STDERR.printは出力先としてファイルとディスプレイのいずれも指定することができる場合に、ディスプレイを指定する際の記法である(詳細は前期のopenメソッドの回を確認しよう)。STDERR.printとすることでディスプレイに表示することを明示的に示していると考えておけばよい。

exit(1):exitはプログラムの強制終了をあらわす。このプログラムでは引数を入力せずにプログラムを実行した場合、プログラムを継続する意味がない。そこでexitを使用して強制的にプログラムを終了している。プログラムを終了させる場合、プログラムの実行がうまくいったかどうかをシステムに対して示すことができる。これを終了ステータスというが、プログラムがうまくいった場合は0を用い(exit(0))、うまくいかなかった場合は0以外とする(exit(1))決まりになっている。

ARGVとgets

ARGVを用いてプログラム実行時に引数を与えた場合、キーボードからの入力を受け取るためにgetsメソッドを用いると予想外の挙動を示す。このプログラムは実行時に与えた引数がvar1に代入され、プログラム実行後にキーボードから入力した値がvar2に代入される。そしてvar1var2を足した結果を出力する(gets-ARGV.rb)。

#!/usr/koeki/bin/ruby

var1 = ARGV[0].to_i

print "好きな数字を入力してください\n"
var2 = gets.chomp!.to_i
answer = var1 + var2
printf ("%d足す%dは%dです\n",var1, var2, answer)

このプログラムを実行すると以下のようになる。ここで黄色の部分がユーザーが入力した値である。

irsv{c10xxxx}% ruby gets-ARGV.rb 20[Return]
好きな数字を入力してください
gets-ARGV.rb:6:in `gets': No such file or directory - 20 (Errno::ENOENT)
        from gets-ARGV.rb:6

実行するとエラーが出てしまう。エラーメッセージを見ると「No such file or directory-20」すなわち「20というファイル、ディレクトリはありません」というメッセージである。

前期の授業で正規表現を扱ったときのことを思い出そう。外部に検索対象となるテキストファイル(例えばpiyo.txt)を保存しておき、そのファイル内の検索を行って正規表現にマッチする行のみを表示させるようなプログラムを書いてきたが、こうしたプログラムを実行する際には、下のように実行するプログラム名の後ろに読み込むファイル名を指定した。そしてプログラム内に書かれたgetsメソッドは実行時に指定したファイルから1行ずつ読み込むという働きを行った。

irsv{c10xxxx}% ruby hoge.rb piyo.txt[Return]

gets-ARGV.rb内のgetsメソッドもこれと同じ働きをしている。プログラムを実行する際にプログラムの後ろに引数を与えているが、これを読み込むファイル名だと判断し、読み込もうとしたがファイルが見つからないというエラーが発生しているのである。このままでは都合が悪いので、var2 = STDIN.gets.chomp!.to_iとする。これによりキーボードからの読み込みが可能となる。

STDINは標準入力という意味で、STDIN.getsでgetsメソッドの読み込み元を標準入力にせよという指定になる。標準的な入力はキーボードから行われるので、このような指定を行うことで、キーボードからの入力が受け付けられるようになる。

レポート課題

以下のうちいずれかを選んで解答する。ARGVおよび配列処理メソッドを用いて記述することARGV・配列処理メソッドを用いなかった場合はそれぞれ1点減点する。

問題1(8点満点):次のルールで採点が行われる競技の得点を求めるプログラム(mark.rb)を作成せよ。ただしXはktermでのプログラム実行時に引数として与えること。

  • X人の審査員がそれぞれ10点満点(9.85、8.45など小数点第2位まで)で採点する。
  • 最も高い点と最も低い点を除外した残りの点の合計が得点となる。
  • ヒント:審査員の採点結果を繰り返し表現を用いて人数分(X回)入力させて配列に代入し、小さい順に並び替えてから先頭と末尾を除去し、再度繰り返し表現を用いて配列内の全ての要素を足し算する。

問題2(10点満点)point.txtは、年間16戦で構成されるK2グランプリの結果である(名前は平成名前辞典から引用)。各レースでは順位に応じて

  • 1位:10点
  • 2位:6点
  • 3位:4点
  • 4位:3点
  • 5位:2点
  • 6位:1点

が与えられる。7位以降は0点であり、リタイヤは「-」と表示される。リタイヤも0点としてカウントする。獲得した得点に応じてランキングが決まるが、16戦全ての結果は使用しない。個人ごとに良い成績をマークした上位X戦のポイントの合計を求め、このポイントにより最終的な順位が決定される。結果は、

  • 1位:公益太郎   85ポイント
  • 2位:飯森山二郎  72ポイント

のように表示される。point.txtを見ると、全部で15名がエントリしているが、最終的に結果を表示するのは上位Y位までとする

このような条件を満たすプログラムrace.rbを作成しなさい。なお、X、Yはktermでプログラムを実行する際に引数として与えること。


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

  • 採点基準(問題1):期限内提出点(2点)、メールの体裁(1点)、プログラム(3点)、プログラムの説明(2点)
  • 採点基準(問題2):期限内提出点(2点)、メールの体裁(1点)、プログラム(4点)、プログラムの説明(3点)
  • プログラムの説明:今回新しく出てきたARGV、配列処理メソッドに関連する部分を中心に説明すればよい。1行1行説明する必要はない。
  • わかりにくい説明や、Webページを単にコピー&ペーストしただけの説明は減点することがある。一度読み直してから提出すること。
  • 驚異的に良くできているレポートについては満点を超える得点をつけることがある。
  • よくできていたレポートは、他の人の参考になるよう、本人が特定できないような形で掲載する。掲載してほしくない場合はメールでの課題提出時にその旨記載すること。

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

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

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