3章の練習問題の解答例

数値クラス

(1)

# a1-1.rb
def fahr2celsius(fahr)
  5.0 * (fahr-32) / 9
end

「5」ではなく「5.0」でなければいけないことに注意してください。 fahrが整数の場合、全体の結果も小数点以下を切り捨てられて、整数になってしまうためです。

なお、「5.0」の前に、returnをつけても構いません。returnの場合は、最後に実行した文の値がメソッドの返り値になります。

(2)

# 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」などと足し算をつなげた場合に正しい答えが得られなくなります。

(3)

「自分自身と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

(4)

一般に、小数点以下の数をコンピュータで扱おうとすると、誤差が生じる場合が少なくありません。これは、コンピュータが小数を扱う際に、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進数で計算するようになっています。

配列クラス

(1)

# 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

なんというか、「やればなんとかなる」という感じの書き方です。このように、全ての要素に対してアクセスする場合には、無理にインデックスアクセスを使う必要はありませんが、特殊な条件化でアクセスしなければならないということもありえます。「訓練しておくに越したことはない」ということで。

(2)

配列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メソッドの返り値として生成される配列をそのまま使っています。

(3)

# 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」というプログラミング言語とその方言ではプログラムそのものが、カッコの間に入ることになります。コンパイラの教科書などでは、このようなカッコのある文字列の解析を行うアルゴリズムがよく紹介されているので、興味のある方はそちらも参照してください。

文字列クラス

(1)

# 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」の場合、「一百」などとは書かず「百」となる、といったように、十の位より大きい桁では「一」が書かれないので、それを補完するようになっています。

(2)

# 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パズルブック」という本 (残念ながら現在は入手困難なようです) に掲載されたものをアレンジしたものです。元の問題は、このような文字列の操作だけで足し算を行う、というものでした。足し算を行うには、ここで行ったような数字から文字列への変換を、足し合わせる数それぞれに対して行い、文字列同士を連結させてから、今度は逆に文字列を数字に直す、という作業を行います。

(3)

# 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に戻すだけです。

ハッシュクラス

(1)

この問題の場合、素直に書くと、配列を使うことになるでしょう。

# 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[〜] という方法でハッシュを生成しています。この場合、カンマ(「,」)で区切られた要素が、キーと値の並んだものとして扱われます。また、「*」は、配列をメソッドの引数の並びに変換します。

(2)

# 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というキーの順番を保存する配列があります。キーに対する操作は全てこの変数にも反映されます。そのため、キーの順番を保存することができるようになります。

正規表現クラス

(1)

# a5-1.rb
def get_local_and_domain(str)
  str =~ /^[^@]+@.*$/
  localpart = $1
  domain = $2
  return localpart, domain
end

メールアドレスにマッチする正規表現は非常に複雑になるのですが、コメント部分がなく、不正な文字も含まれていない、と仮定できるのであれば、容易になります。

(2)

# a5-2.rb
"オブジェクト指向は難しい! なんて難しいんだ!".gsub(/難しい!/,"簡単だ!").gsub(/難しい/,"簡単な")

gsubメソッドを使う置換で気をつける必要があることとして、「思わぬ文字列まで変換してしまうこと」があります。

同じような文字列を変換する場合、変換したくないものまで変換してしまうかもしれません。この問題の場合、「難しい」という単語「簡単だ」と「簡単な」に変換しなければならないのですが、いきなり単純に「難しい」を変換前の単語に選んでしまうと、誤った結果になってしまいます。そこで、最初に、前の方の「難しい!」を変換してから、後の方の「難しい」を変換するようにしています。

(3)

# 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

IOクラス

(1)

# 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バイトずつコピーしています。

(2)

# 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を越えている場合は先頭の行を破棄しています。

Fileクラス・Dirクラス

(1)

# 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

(2)

# 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

(3)

# 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に足し込んでいき、そのままメソッドの戻り値としています。

  1. の例と同じように、シンボリックリンクを無限に調べてしまう可能性がある

場合は、File.lstatを使ったほうがよいでしょう。

Timeクラス

(1)

# 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で初期化している点に注意して下さい。

(2)

# 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