# a1-1.rb def fahr2celsius(fahr) 5.0 * (fahr-32) / 9 end
「5」ではなく「5.0」でなければいけないことに注意してください。 fahrが整数の場合、全体の結果も小数点以下を切り捨てられて、整数になってしまうためです。
なお、「5.0」の前に、returnをつけても構いません。returnの場合は、最後に実行した文の値がメソッドの返り値になります。
# a1-2.rb class Celsius def initialize(celsius) @celsius = celsius end def to_celsius @celsius end def to_fahr (@celsius*9.0/5)+32 end def +(other) @celsius += other self end def -(other) @celsius -= other self end end if __FILE__ == $0 c1 = Celsius.new(30) c2 = c1 + 10 p c2 p c2.to_celsius p c2.to_fahr c2 = c1 - 10 p c2 p c2.to_celsius p c2.to_fahr end
「+」と「-」の定義で、最後が「self」になっているのは、返り値として自分自身を返すようにするためです。これがない場合、「c1 + c2 + c3」などと足し算をつなげた場合に正しい答えが得られなくなります。
「自分自身と1以外で割り切れない数」なのですから、2から自分自身の1つ前までの数と割り算をして、余りがでるかどうかを確認すればよさそうです。
これを素直に書くと、こうなります。
# a1-3.rb def prime?(num) (2...num).each{|i| if num % i == 0 return false end } return true end
もっとも、これはかなり効率が悪いやり方です。少し数学に明るい人なら、「nが素数であるかは、nより小さい数すべてについて確認する必要はなく、√nよりも小さい数で割り切れるかどうかを確認すればいい」ということを知っているかもしれません(これは、nをl*mで表せられるとした場合、lもmも√nより大きければ、l*mがnより大きくなってしまい、矛盾するからです)。
これをそのままプログラムにすると、このようになります。
# a1-3b.rb require 'mathn' include Math def prime?(num) p sqrt(num) (2..sqrt(num)).each{|i| if num % i == 0 return false end } return true end
一般に、小数点以下の数をコンピュータで扱おうとすると、誤差が生じる場合が少なくありません。これは、コンピュータが小数を扱う際に、2進数に直して扱っているからです。
このように、小数を適当な桁で打ち切ってしまうことを「丸める」といい、丸めた結果生じる誤差を「丸め誤差」といいます。
小数も10進数で扱うクラスを作ることはできますが、その場合、計算の効率が少し悪くなります。また、その場合でも、循環無限小数を扱う場合には誤差が生じます。
正確さにこだわるなら、有理数のまま扱うこともあります。Rubyでも、 Rationalクラスを使えば、有理数での計算が行えます。
# a1-4.rb require 'rational' a = Rational(1,10) + Rational(2,10) p a #=> Rational(3,10) b = (a == Rational(3,10)) p b #=> true
なお、Cの世界では、伝統的に小数計算をする場合には2進数で計算する方が多いようです。そのせいもあって、Rubyの実数の計算モデルも、2進数で計算するようになっています。
# a2-1a.rb def square(nums) return nums.collect{|num| num * num } end
# a2-1b.rb def square(nums) ret = Array.new() nums.each{|num| ret << num * num } ret end
# a2-1c.rb def square(nums) ret = Array.new() i = 0 while i < nums.length ret[i] = nums[i] * nums[i] i += 1 end ret end
なんというか、「やればなんとかなる」という感じの書き方です。このように、全ての要素に対してアクセスする場合には、無理にインデックスアクセスを使う必要はありませんが、特殊な条件化でアクセスしなければならないということもありえます。「訓練しておくに越したことはない」ということで。
配列nums1から要素を取り出す際にeachメソッドを使ってしまうと、nums2から要素が取り出せなくなってしまいます。そこで、少し工夫が必要になります。
# a2-2.rb def sum_array(nums1, nums2) ret = Array.new() nums1.each_with_index{|num, i| ret << num + nums2[i] } ret end
これは、each_with_index メソッドを使ったものです。 each_with_indexメソッドは、eachのように要素を一つずつ取り出すメソッドですが、さらにインデックスも取り出すので、 nums2はそのインデックスを使用しています。
# a2-2b.rb def sum_array(nums1, nums2) tmp_nums2 = nums2.dup return nums1.collect{|num| num + tmp_nums2.shift } end if __FILE__ == $0 p sum_array([1,2,3], [4,6,8]) end
こちらは、nums2の方をコピーして、それをshiftメソッドで取り出しています。返り値には、collectメソッドの返り値として生成される配列をそのまま使っています。
# a2-3.rb def balanced?(array) stack = Array.new() array.each{|elem| case elem when '(' stack.push(elem) when '{' stack.push(elem) when ')' prev_elem = stack.pop if prev_elem != '(' return false end when '}' prev_elem = stack.pop if prev_elem != '{' return false end else return false end } if stack.empty? return true else return false end end if __FILE__ == $0 p balanced?([]) #=> true p balanced?(["(",")"]) #=> true p balanced?(["{","(",")","}"]) #=> true p balanced?(["{","(",")"]) #=> false p balanced?(["(",")","}"]) #=> false p balanced?(["(", "{", "{", "}", "(", ")", "}", "(", ")", ")"]) #=> true p balanced?(["(", "{", "{", "}", "(", "}", ")", ")"]) #=> false end
このプログラムでは、「スタック」というデータ構造を使っています。変数stackがそれです。Rubyの場合、配列をスタックとして使用することができます。
スタックは、直前にスタックに積んだ要素を取り出すことができます。これを使って、「(」と「{」ではその要素をスタックに積み、「)」と「}」ではスタックから要素を取り出し、直前の要素が「(」や「{」だったかを調べています。最後に、スタックが空になっているかを調べます。スタックが空になっていない場合、対応する「}」や「)」がない要素がある、ということですから、 falseを返します。
さて、この問題では単にカッコの対応を調べるのみですが、実際にはカッコのみではなく中身が大事です。数式の場合は数値や変数が、また「Lisp」というプログラミング言語とその方言ではプログラムそのものが、カッコの間に入ることになります。コンパイラの教科書などでは、このようなカッコのある文字列の解析を行うアルゴリズムがよく紹介されているので、興味のある方はそちらも参照してください。
# a3-1.rb def kan2num(string) digit4 = '0' digit3 = '0' digit2 = '0' digit1 = '0' nstring = string.dup nstring.gsub!(/一/,'1') nstring.gsub!(/二/,'2') nstring.gsub!(/三/,'3') nstring.gsub!(/四/,'4') nstring.gsub!(/五/,'5') nstring.gsub!(/六/,'6') nstring.gsub!(/七/,'7') nstring.gsub!(/八/,'8') nstring.gsub!(/九/,'9') if nstring =~ /((\d)?千)?((\d)?百)?((\d)?十)?(\d)?$/ if $1 digit4 = $2 || '1' end if $3 digit3 = $4 || '1' end if $5 digit2 = $6 || '1' end digit1 = $7 || '0' end return (digit4+digit3+digit2+digit1).to_i end if __FILE__ == $0 print kan2num('七千百二十三'),"\n" end
このプログラムでは、まず最初に、「一」から「九」までの漢字を数字に変換してから、「千」「百」「十」などの漢字を含んだ文字列の処理を行っています。「100」の場合、「一百」などとは書かず「百」となる、といったように、十の位より大きい桁では「一」が書かれないので、それを補完するようになっています。
# a3-2.rb def num2astrisk(str) num = "" str.split(//).each{|char| num = num + num + num + num + num + num + num + num + num + num char.sub!("0","") char.sub!("1","*") char.sub!("2","**") char.sub!("3","***") char.sub!("4","****") char.sub!("5","*****") char.sub!("6","******") char.sub!("7","*******") char.sub!("8","********") char.sub!("9","*********") num = num + char } num end if __FILE__ == $0 num = ARGV[0] str = num2astrisk(num) print "astrisk: '#{str}'\n" print "length: #{str.length}\n" end
基本的には、数字を1桁ずつ取り出して、その数字の数だけ「*」をつなげた文字列をつなぎ合わせていきます。この時、次の桁の処理を行う前に、今まで処理してきた文字列を10個分連結させるのがポイントです。なお、この解答例では sub!メソッドを使っていますが、case・when文を使って場合わけすることもできます。
なお、この問題はその昔、「sedパズルブック」という本 (残念ながら現在は入手困難なようです) に掲載されたものをアレンジしたものです。元の問題は、このような文字列の操作だけで足し算を行う、というものでした。足し算を行うには、ここで行ったような数字から文字列への変換を、足し合わせる数それぞれに対して行い、文字列同士を連結させてから、今度は逆に文字列を数字に直す、という作業を行います。
# a3-3.rb class StringIO def initialize(str="") @str = str @pos = 0 end def read(n) if n < 0 raise ArgumentError, "negative length #{n} given" end ret = @str[@pos, n] @pos += n if @pos > @str.length @pos = @str.length end ret end def gets(sep="\n") if @pos >= @str.length return nil end pos = @str.index(sep, @pos) if pos.nil? pos = @str.length end ret = @str[@pos..pos] @pos = pos + 1 return ret end def rewind() @pos = 0 end end if __FILE__ == $0 sio = StringIO.new("密林\n嘘\n赤と黒\n罠\n罪") p sio.gets #=> "密林\n" p sio.gets #=> "嘘\n" p sio.gets #=> "赤と黒\n" p sio.gets #=> "罠\n" p sio.gets #=> "罪" p sio.gets #=> nil p sio.gets #=> nil sio.rewind p sio.gets #=> "密林\n" sio.rewind p sio.read(5) #=> "密林\n" p sio.read(3) #=> "嘘\n" p sio.read(7) #=> "赤と黒\n" p sio.read(3) #=> "罠\n" p sio.read(3) #=> "罪" p sio.read(2) #=> nil p sio.read(2) #=> nil end
文字列の内容を@strに、現在の「ファイルポインタ」(この場合は「ファイル」ではないのですが)を@posに保持しています。読み込みが進むにつれて、@posを動かしていくことによって、 IOっぽい処理を行っています。
では、それぞれのメソッドについてざっと解説してみます。
readメソッドは、nが正であることを確認した後で、文字列から指定したバイト数だけ文字を取り出します。さらにポインタを進めて、ポインタが文字列からはみ出したら、末尾に向け直す、という処理を行っています。
getsメソッドの方は、最後まで読み込んでいた場合にはnilを返しておいてから、そうではない場合の処理を行います。指定された改行文字までの文字列を切り出すわけですが、改行文字が見つからない場合は、文字列の終わりまで読み込ませるようにしています。
rewindメソッドは、ポインタを0に戻すだけです。
この問題の場合、素直に書くと、配列を使うことになるでしょう。
# a4-1.rb def str2hash(str) hash = Hash.new() array = str.split(/\s+/) while key = array.shift value = array.shift hash[key] = value end hash end if __FILE__ == $0 p str2hash("bule 青 white 白\nred 赤"); #=> {"blue"=>"青", "white"=>"白", "red"=>"赤"} end
whileの繰り返しでは、keyとvalueを取り出すため、一回の繰り返しの中で二回のshiftが行われています。
なお、これは短くこう書くこともできます。
# a4-1b.rb def str2hash(str) Hash[*str.split(/\s+/)] end if __FILE__ == $0 p str2hash("bule 青 white 白\nred 赤"); #=> {"blue"=>"青", "white"=>"白", "red"=>"赤"} end
これは Hash[〜] という方法でハッシュを生成しています。この場合、カンマ(「,」)で区切られた要素が、キーと値の並んだものとして扱われます。また、「*」は、配列をメソッドの引数の並びに変換します。
# a4-2.rb class OrderdHash def initialize() @keys = Array.new() @content = Hash.new() end def [](key) @content[key] end def []=(key, value) @content[key] = value if !@keys.include?(key) @keys << key end end def delete(key) @keys.delete(key) @content.delete(key) end def keys() @keys.each{|key| @content[key] } end def each() @keys.each{|key| yield(key, @content[key]) } end end if __FILE__ == $0 oh = OrderdHash.new() oh["one"] = 1 oh["two"] = 2 oh["three"] = 3 oh["two"] = 4 p oh.keys() oh.each{|key,value| p [key, value] } end
OrderdHashでは、オジェクトに対する操作を@contentへ転嫁しています。このように、あるオブジェクトに対する処理を、そのまま別のオブジェクトに任せてしまうことを、オブジェクト指向用語では「委譲(delegate)」といいます。継承を行わずに、あるクラスのオブジェクトに対する処理を別のクラスのメソッドを使って行いたい場合には多用されるテクニックです。
全てを@contentに振るだけなら意味がないのですが、ここでは@keysというキーの順番を保存する配列があります。キーに対する操作は全てこの変数にも反映されます。そのため、キーの順番を保存することができるようになります。
# a5-1.rb def get_local_and_domain(str) str =~ /^[^@]+@.*$/ localpart = $1 domain = $2 return localpart, domain end
メールアドレスにマッチする正規表現は非常に複雑になるのですが、コメント部分がなく、不正な文字も含まれていない、と仮定できるのであれば、容易になります。
# a5-2.rb "オブジェクト指向は難しい! なんて難しいんだ!".gsub(/難しい!/,"簡単だ!").gsub(/難しい/,"簡単な")
gsubメソッドを使う置換で気をつける必要があることとして、「思わぬ文字列まで変換してしまうこと」があります。
同じような文字列を変換する場合、変換したくないものまで変換してしまうかもしれません。この問題の場合、「難しい」という単語「簡単だ」と「簡単な」に変換しなければならないのですが、いきなり単純に「難しい」を変換前の単語に選んでしまうと、誤った結果になってしまいます。そこで、最初に、前の方の「難しい!」を変換してから、後の方の「難しい」を変換するようにしています。
# a5-3.rb def word_capitalize(str) return str.split(/\-/).collect{|w| w.capitalize}.join('-') end if __FILE__ == $0 p word_capitalize("in-reply-to") #=> "In-Reply-To" p word_capitalize("X-MAILER") #=> "X-Mailer" end
区切り文字がハイフンのみと決まっている場合、splitで分割し、個々の要素をcapitalizeしたものをcollectしてから、joinで連結し直せば、目的が達成されます。 split → collect → join、と流れる処理は、使いこなせるととても重宝します。
もし、ハイフン以外の文字も含まれている場合なら、scanメソッドを使う、という方法もあります。
# a5-3b.rb def word_capitalize(str) ret = "" str.scan(/(\w+)(\W*)/){|word, no_word| ret << word.capitalize + no_word } return ret end if __FILE__ == $0 p word_capitalize("in-reply-to") #=> "In-Reply-To" p word_capitalize("X-MAILER") #=> "X-Mailer" end
# a6-1.rb def copy(file1, file2) open(file1){|io1| open(file2, "w"){|io2| while data = io1.read(1024*64) io2.write(data) end } } end
コピー元のファイルを開いてからコピー先のファイルを開きます。順番を逆にすると、コピー元のファイルが開けない場合に、空のファイルが残ってしまいます。例外処理で削除することもできますが、例外が発生することを前提にして処理の順番を考えたほうがよいでしょう。一度にファイルを読み込んでしまうと、大きなファイルをコピーする場合に、メモリが不足してしまうかもしれません。ここでは、一度に読み込まずに、64kバイトずつコピーしています。
# a6-2.rb def tail(file, lines=10) queue = Array.new open(file){|io| while line = io.gets queue.push(line) if queue.size > lines queue.shift end end } queue.each{|line| print line } end
このプログラムでは、「キュー」というデータ構造を使っています。キューとはFIFO(first-in, first-out)を行うためのデータ構造です。変数queueがそれにあたります。実際はただの配列ですが、pushとshiftを組み合わせることによって、キューとして使うことができます。ファイルを1行ずつ読み込んではqueueに追加し、linesを越えている場合は先頭の行を破棄しています。
# a7-1.rb def printLibrary $:.each{|path| next unless FileTest.directory?(path) Dir.open(path){|dir| dir.each{|name| if name =~ /\.rb$/ puts name end } } } end
$: に含まれるディレクトリを順に参照して、そこに含まれるファイルのうち「.rb」で終るものを出力しています。
しかし、この方法ではRubyで書かれたライブラリの名前しか拾うことができません。拡張ライブラリの名前も出力するには、「.so」や「.dll」といった、プラットフォームによって異なる拡張子を取得しなければいけません。 Rubyをセットアップする際に決められた設定情報は、Configモジュールから取得することができます。 Configモジュールに含まれるConfig::CONFIGというハッシュには、拡張ライブラリの拡張子の他にも、インストール先のディレクトリ名や、バージョン番号といった情報が含まれています。
Configモジュールを利用するには、「require "rbconfig"」とします。拡張ライブラリの拡張子は「"DLEXT"」というキーで参照できるので、これをpriintLibraryに追加すると次のようになります。
# a7-1b.rb require 'rbconfig' def printLibrary dlext = Config::CONFIG["DLEXT"] $:.each{|path| next unless FileTest.directory?(path) Dir.open(path){|dir| dir.each{|name| if name =~ /\.(rb|#{dlext})$/ puts name end } } } end
# a7-2.rb def copydir(dir1, dir2) raise "Error: #{dir2} exists." if FileTest.exist?(dir2) Dir.open(dir1){|dir| Dir.mkdir(dir2) dir.each{|name| next if name == "." || name == ".." path1 = File.join(dir1, name) path2 = File.join(dir2, name) st = File.stat(path1) if st.file? copy(path1, path2) elsif st.directory? copydir(path1, path2) else $stderr.puts "unknow file type: #{path1}" end } } end
まず、コピー先にディレクトリやファイルが存在していれば、例外を発生させます。続いて、コピー元のディレクトリを開き、コピー先のディレクトリを作成します。無限ループを避けるために "." と ".." を読み飛ばし、それ以外のものについてFile.statを使ってディレクトリのエントリを順に調べ、次のように場合分けします。
IOの練習問題で作成したcopyメソッドを使ってコピーを行います。
新しいパス名を引数にしてcopydir自身を呼び出します。このように、メソッド内から同じメソッドを直接、または間接的に呼び出すことを「再帰呼び出し」といいます。
"unknown file type: コピー元のパス名" を標準エラー出力に出力します
ディレクトリへのシンボリックリンクがある場合は、その下のディレクトリもコピーしてしまいます。本文中では説明していませんが、これを避けるには File.statの代わりにFile.lstatを使い、Stat#symlink?による検査を追加します。シンボリックリンクを作成するには、File.symlinkを使います。
# a7-2b.rb def copydir(dir1, dir2) raise "Error: #{dir2} exists." if FileTest.exist?(dir2) Dir.open(dir1){|dir| Dir.mkdir(dir2) dir.each{|name| next if name == "." || name == ".." path1 = File.join(dir1, name) path2 = File.join(dir2, name) st = File.lstat(path1) # lstat を使う if st.file? copy(path1, path2) elsif st.directory? copydir(path1, path2) elsif st.symlink? File.symlimk(File.readlink(path1), path2) else $stderr.puts "unknow file type: #{path1}" end } } end
# a7-3.rb def du(path) st = File.stat(path) result = st.size if st.directory? Dir.open(path){|dir| dir.each{|name| next if name == "." || name == ".." result += du(File.join(path, name)) } } end printf("%8d %s\n", result, path) result end
ここでも再帰呼び出しによって、ディレクトリ以下のエントリを処理しています。再帰呼び出しによって、下位のエントリの合計を得ることができるように、結果をresultに足し込んでいき、そのままメソッドの戻り値としています。
の例と同じように、シンボリックリンクを無限に調べてしまう可能性がある
場合は、File.lstatを使ったほうがよいでしょう。
# a8-1.rb def jparsedate(str) year = month = day = hour = min = sec = 0 str.scan(/(午前|午後)?(\d+)(年|月|日|時|分|秒)/){ case $3 when "年"; year = $2.to_i when "月"; month = $2.to_i when "日"; day = $2.to_i when "時"; hour = $2.to_i hour += 12 if $1 == "午後" when "分"; min = $2.to_i when "秒"; sec = $2.to_i end } Time.mktime(year, month, day, hour, min, sec) end
年月日時分秒のフォーマットを固定せずに結果を得られるように、ひとまとめにした正規表現に使って、文字列をスキャンしています。あらかじめ全ての値を0で初期化している点に注意して下さい。
# a8-2.rb def ls_t(path) Dir.open(path){|dir| dir.sort{|ent1, ent2| t1 = File.mtime(File.join(path, ent1)) t2 = File.mtime(File.join(path, ent2)) t1 <=> t2 } } end
DirクラスはComparableをインクルードしているので、sortメソッドを使ってエントリをソートする事ができます。パス名を取得するために、ディレクトリ名とエントリ名を連結しています。
ただし、この方法では比較の度にファイルの更新時間を取得するため、大きなディレクトリをソートする場合にはあまり効率がよくありません。そこで、取得した更新時間をハッシュに溜めておくように変更してみます。 1000個のファイルを含むディレクトリで試したところ、筆者の環境では倍程度早くなりました。
# a8-2b.rb def ls_t(path) Dir.open(path){|dir| hash = {} ary = dir.sort{|ent1, ent2| unless hash[ent1] hash[ent1] = File.mtime(File.join(path, ent1)) end unless hash[ent2] hash[ent2] = File.mtime(File.join(path, ent2)) end hash[ent1] <=> hash[ent2] } } end