課題
もくじ
作成するプログラムでは, プログラム実行後に「プロンプト」を表示して,そこで指定される命令に基づいて プログレスバーをインタラクティブ(interactive;対話的)に操作することを想定しています. 要するに,この授業で利用している「Terminal」のようにプログラムを動作させるわけです.
プログラムテンプレート
次に示すプログラムのテンプレート(雛型)を適宜名前を変えて使って下さい.
テンプレートはそのまま実行できます(ファイル名をprogress.rbと変更したものとします)
$ ruby progress.rb
実行すると「Terminal(端末)」と同様に,次のような「プロンプト」が表示されます.
>>
このプロンプトにつづいて,何か適当に文字列を入力して最後に[Enter]を押すと, 次のように入力した文字列がオウム返しされます.
>> hello! ⏎
input = 'hello!'
このとき,プログラムではプログレスバー(ProgressBar)のechoメソッドが実行されています.
入力は何度でも受け付けられます. Terminalでの操作と同様に履歴も使えます.
>> [↑]
[Ctrl]+[d]([Ctrl]を押しながら[d]を押す)を入力すると,プログラムを終了します.
>> [Ctrl]+[d]
exited
$
このテンプレートで「Terminal」のようにインタラクティブな処理を行う仕組みの基盤が提供されていることが分かるでしょう.
今回のプログラムは,このテンプレートに基づいて作成してください. テンプレートに記述されている定義,処理の詳細については,以下で確認してください.
プログレスバー
一般にプログレスバーとは,コンピュータで時間のかかる処理の実行中に, その進行状況を逐次表示するものです. この課題では,次のような擬似的なプログレスバー(Bar)を考えます.
- Barはn個のUNITで構成される
- Barを「起動」すると,何も表示されていない状態から始まって,一定の時間間隔でBarのUNITが一つずつ左から順に表示されていく(表示されるバーが長くなっていく). UNITは適当な文字で表現する. すべてのUNITを表示したら(UNITをn個表示したら),Barの動作は停止する.
- Barの動作中にUNITを表示させる時間間隔はBPM(Beats Per Minute)で定める
# BPMとは1分間の動作回数を意味する.たとえば60bpmなら1秒に1回動作することになる.
次にプログラムを実行した後, プログレスバーを起動して,すべてのUNITを表示し終えた状態の例を示します. この例ではBarを構成するUNITの個数は20(n=20)としています. UNITは「#」で表示しています. また「go」という命令によって, プログレスバーが起動して,動作が開始されることにしています. プログレスバーの動作が終わった後は,次のプロンプトが表示されています. 最後に[Ctrl]+[d]と入力して,プログラムの実行を終了しています.
$ ruby progress.rb >> go ⏎ #################### >> >> [Ctrl]+[d] exited $
課題のプログラムでは,プログレスバーのクラスを定義して, そのインスタンスを利用して, インタラクティブな操作を行うことにします. テンプレートでは次のようにクラスの雛形を提供してます.
## [課題] ProgressBarクラスを完成させてください class ProgressBar # n: barを構成するunit数 # b: 更新の間隔(as BPM) def initialize(n,b) end : : end
インスタンス変数は適宜導入してください. 必要ならクラス定数も導入してください.
プログレスバーのメソッド
プログレスバーには,少なくとも以下のメソッドを定義することにします(テンプレートには空の定義を最初から組み込んであります). これらのメソッドをインタラクティブな操作(命令)によって呼び出して, プログレスバーの処理を実行するようにします.
- initialize(n,b)
n個のUNITで構成され,更新間隔がbで定められるプログレスバーを初期化する. bはBPMで指定する. - launch
プログレスバーを起動して動作させる. bpmで定まる時間間隔で,UNITを順に一つずつ表示していく. すべてのUNITを表示したら,メソッドを終了する. - full!(n)
プログレスバーを構成するUNIT数をnに設定(変更)する. 名前の末尾の「!」も含めて「full!」がメソッド名である. - bpm!(b)
プログレスバーのBPMをbに設定(変更)する. 名前の末尾の「!」も含めて「bpm!」がメソッド名である.
これら以外のメソッドを追加することも歓迎します.
プログラム実行の一時停止(sleep)
時間間隔を空けて,プログレスバーのunitを表示させるには, sleepでプログラムを一時停止させます.
sleep(s) # s秒一時停止する(1秒未満を指定することも可能)
停止させる時間は秒単位で指定します. 指定する時間は整数でなくても構いません.1秒未満を指定することもできます.
「REPL」によるプログレスバーのインタラクティブな操作
REPLとは, 「Read-Eval-Print Loop」の略で, コマンドを入力,実行して,結果を示すという処理の繰り返しを意味しています. 授業で利用しているTerminal(端末)でもお馴染みのものです. REPLによる操作は,コマンドを一つずつ入力して,その結果を見ながら,順に作業を進めていくスタイルから,インタラクティブ(interactive;対話的)と言われます.
REPLの「コマンド入力→実行→結果の表示」の繰り返し処理は次のように表現できます.
- コマンドを読み取る(Read)
- コマンドを評価(処理)する(Eval)
- 処理の結果を表示する(Print)
- 1-3を終了の指示があるまで繰り返す(Loop)
テンプレートでは, 次のようにして,まずプログレスバーのインスタンスを生成してから, REPLに入るようになっています.
PROMPT='>> ' # 実行中に表示するプロンプト(変更してもよい) DEFAULT_FULL=20 # バーを構成するUNITの個数の初期値 DEFAULT_BPM=240 # 更新間隔の初期値(BPM: Beats Per Minute) # プログレスバーのインスタンスを生成する pbar = ProgressBar.new(DEFAULT_FULL,DEFAULT_BPM) ## [課題] プログレスバーを操作するREPL(Read-Eval-Print Loop)を ## 完成させてください ## (上で生成しているpbarがプログレスバーのインスタンスです) # loop do ... endは無限ループ(無条件の繰り返し) loop do # プロンプトを表示して1行読み取る # → buffにキーボードから読み込んだ文字列が代入される # (※ Terminal同様に履歴が使える) buff = Readline.readline(PROMPT,true) # [Ctrl]+[d]が入力されたらloopから脱出して終了 # (そのときbuff==nilとなる) break unless buff # 読み取った行を表示する(テスト用) pbar.echo(buff) end puts "\nexited"
REPLの動作は「テンプレート」で確認したとおりです.
今回のREPLではコマンドを次のような形式で指定することで, プログレスバーのメソッドのうち,「launch」「full!」「bpm!」に対応する操作が実行できるようにしてください.
# コマンドの仕様(例) go # ProgressBarのインスタンスのlaunchを実行 set 30 # ProgressBarのインスタンスのfull!(30)を実行 tempo 120 # ProgressBarのインスタンスのbpm!(120)を実行
コマンド名は,必ずしもこの通りにしなくても構いません.自由に決めてください. ProgressBarクラスに「launch」「full!」「bpm!」以外にも(public)メソッドを 定義した場合には,それらを利用するコマンドも追加してください.
コマンドの解釈と実行
REPLの実行中に,キーボードから読み取られた命令(buffに格納)を解釈して, 命令にしたがって,プログレスバーのメソッドを実行します. プログレスバーが起動(launch)されたら, bpmに合わせてUNITをすべて順に表示します. UNITの個数の変更,bpmの変更が指示されたら,それにしたがって, プログレスバーのデータを更新します.
技術要素
プログラムを作成するために,以下を参考にしてください.
- 文字列の単語への分解(split)
splitメソッドにより,文字列を(連続した)空白で区切った単語の配列を得ることができる.生成される配列の各要素はすべて文字列であることに注意せよ.str = "color 224 64 96" words = str.split # words == ["color","224","64","96"]
- 配列の先頭要素の取り出し(shift)
shiftを使うと配列から先頭の要素を取り出すことができる. 取り出した先頭の要素は配列から削除される. その結果,残りの要素は一つずつ前にずれる.a = [2,3,4,5,6] len = a.size # len == 5 b = a.shift # ==> b == 2, a == [3,4,5,6] len2 = a.size # len2 == 4 (先頭が取り除かれた→要素が1個減った)
- 文字列の数値への変換(to_i,to_f)
文字列にto_iを適用すると,文字列の先頭部分(最初に±があれば,それも合わせて)の数字の列が整数に変換され, to_fを適用すると,(小数点があればその後の数字列も含めて)小数点数に変換される.str0 = '1.2' a = str0.to_i # a == 1 x = str0.to_f # x == 1.2 str1 = '+12.3+4' b = str1.to_i # b == 12 y = str1.to_f # y == 12.3 str2 = '-12.3.4' c = str2.to_i # c == -12 z = str2.to_f # z == -12.3
「to_i」は文字列の先頭から数字列(数字のみが並んでいる部分)を取り出して, 整数に変換する.先頭に数字列がない場合は0に変換する. 「to_f」は文字列の先頭から「数字列」または「数字列.数字列」を取り出して 小数点数に変換する.先頭に数字列がない場合は0.0に変換する. いずれも文字列の先頭が正負の符号(±)の場合はそれも含めて変換する. - メソッドを指定した実行(send)
sendメソッドにより,メソッド名(Symbolか文字列で指定)と引数を指定して,そのメソッドを実行できる. なおメソッドの引数として,配列名の前に「*」を付加しておくことで, 配列を要素に展開して,それぞれを引数として渡すことができる. 空の配列を渡した場合は何も指定しないのと同じ結果となる. なおsendメソッドに限らず,どのメソッドでもこの記法は使える.a = [0,1,2,3] b = a.send(:shift) # 「b = a.shift」と同じ.メソッド名「shift」はSymbolで指定 # b == 0, a == [1,2,3] arg = ['insert',0,4] a.send(*arg) # 「a.insert(0,4)」と同じ.メソッド名「insert」は文字列で指定.['insert',0,4]を要素に分解してsendに適用. # a == [4,1,2,3]
- メソッドの確認(respond_to?)
respond_to?メソッドにより, 引数に指定した名前(Symbolか文字列で指定)のメソッドがオブジェクトで利用可能かどうかを判定できる.a = [0,1,2,3] # 配列 a.respond_to?(:push) # ==> true; 配列には「push」メソッドが存在する a.respond_to?(:size) # ==> true; 配列には「size」メソッドが存在する a.respond_to?('sort') # ==> true; 配列には「sort」メソッドが存在する a.respond_to?(:abs) # ==> false; 配列には「abs」メソッドは存在しない a.respond_to?('open') # ==> false; 配列には「open」メソッドは存在しない
- ハッシュ(Hash)
「キー」と「値」を対応させた構造をもつデータをハッシュ(Hash)という. ハッシュは整数以外の値を添字とする配列のように使える.## (注) 以下の例ではハッシュのキーに文字列を使っている # ハッシュの定義 wday = {'mon'=>'月','tue'=>'火','wed'=>'水','thu'=>'木','fri'=>'金','sat'=>'土','sun'=> '日'} # ハッシュの要素の参照 wday['sun'] # ==> "日" wday['mon'] # ==> "月" # ハッシュの要素の追加 wday['holiday'] = "祝日" # 追加した要素の参照(最初からある要素と全く同じ) wday['holiday'] # ==> "祝日" # 値が存在しないキーに対してはnilが得られる # (別の値が得られるように設定することも可能) wday['today'] # ==> nil
- 文字列とSymbolの間の変換
to_symメソッドで文字列をSymbolに変換できます. 逆にto_sメソッドでSymbolを文字列に変換できます.a_string = 'hello' # 文字列 a_symbol = a_string.to_sym # a_symbol == :hello (Symbol) c_symbol = :color # Symbol c_string = c_symbol.to_s # c_string == "color" (文字列)
[参考] 端末上で色の表示
端末(Terminal)上の文字,文字の背景に色をつけることができます. 課題として必須ではありませんが,色を使う場合には以下を参考にしてください.
require 'ansi_sgr' # プログラムの先頭に追加 # 色の付け方の仕様例(文字→赤色,背景→黄色) spec={ :fg=>:red, :bg=>:yellow } msg = ANSISgr.sgr('Hello, World!',spec) # specの仕様にしたがって色指定をした文字列(Hello, World!)を生成 puts msg # 端末画面への表示(最後に改行)
上の例で示したように文字色と背景色は次の形式でHashで指定します.
# fg→文字色 # bg→背景色 color = { :fg=>色名, :bg=>色名 }
文字色(:fg)のみ,背景色(:bg)のみを指定することもできます.
「:fg」「:bg」はSymbolです.それぞれ文字色,背景色を指定するためのキーワードで,そのまま記述します.
利用する色もSymbolで指定します. 次のいずれかを文字色あるいは背景色に指定可能です.
:black ■ :red ■ :green ■ :yellow ■ :blue ■ :magenta ■ :cyan ■ :white ■
[参考] Symbol
ここではRubyの「Symbol」(シンボル)について簡単に説明します. 課題のためには以下を理解する必要はありません.
Symbolは「文字列」(文字が並んだデータ)と似ていますが,異なる種類のものです. Symbolとは「名前」で区別されるデータです. コロン(:)のあとに「名前」を書いて定義します. なお「:」は名前には含まれません.それにつづく部分が名前です.
:a_symbol # シンボル
上で述べたようにSymbolは文字列(文字の並び)ではありません. 文字列のデータは書き換えられます.一方でSymbolは書き換えられません. 同じ文字の並びで構成される文字列(の実体)はいくつでも作れますが,同一の名前のSymbol(の実体)は一つしかありません.
# # 文字列とそのデータの実体について調べてみる # s = "ace" # 文字列 t = "ace" # 文字列 s == t # ==> true (s,tの値は等しいか?→等しい) s.equal?(t) # ==> false (s,tは同一の実体か?→そうではない.「equal?」はデータの実体の同一性を判定する) # sの1文字目を"A"に書き換える. s[0,1] = "A" # s == "Ace" # sへの変更はtには影響しない.つまりs,tの実体は同じではない.たまたま同じ文字が並んでいただけ. s == "Ace" # ==> true (sの値は"Ace"に等しいか→等しい) t == "ace" # ==> true (tの値は"ace"に等しいか→等しい) s == t # ==> false (s,tの値は等しいか?→等しくない) s.equal?(t) # ==> false (s,tは同一の実体か?→そうではない) # # Symbolとそのデータの実体について調べてみる # x = :ace # Symbol y = :ace # Symbol z = :Ace # Symbol x == y # ==> true (x,yの値は等しいか?→等しい.名前(ace)が同一である) x == z # ==> false (x,zの値は等しいか?→等しくない.名前(ace,Ace)が異なる) x.equal?(y) # ==> true (x,yの実体は同一である) x.equal?(z) # ==> false (x,zの実体は異なる) # # 文字列とそのデータの実体について調べてみる(2) # t = s # tをsと同じ文字列データと対応させる t.equal?(s) # ==> true (s,tは同一の実体である) t[2,1] = "t" # tの3番目の文字を"t"に書き換える(t=="Act") s == "Act" # ==> true (sの値は"Act"に等しいか→等しい)