「たのしいRuby 第2版」練習問題の解答例

数値(Numeric)クラス

(1)の解答

問題で与えられた式をそのまま使ってメソッドを定義します。

def cels2fahr(cels)
  return Float(cels) * 9 / 5 + 32
end

(2)の解答

(1)の逆の計算を定義します。 「+」と「*」の優先順位の違いから括弧が必要なことに注意してください。

def fahr2cels(fahr)
  return (Float(fahr) - 32) * 5 / 9
end

(3)の解答

単に「rand(6)」とした場合は、0から5の値が返るので結果に1を足します。

def dice
  return rand(6) + 1
end

(4)の解答

まず、2より小さい数は素数でないとします。それ以上の数は、2からその数の平方根まで割り算を行って、すべての剰余が0でないことを確認します。

def prime?(num)
  return false if num < 2
  2.upto(Math.sqrt(num)){|i|
    if num % i == 0
      return false
    end
  }
  return true
end

配列(Array)クラス

(1)の解答

二つの方法を挙げます。もっと小さな配列なら「ary = [1, 2, 3, ...]」とリテラルで定義してもいいでしょう。

# 空の配列を作って、1から100までの値を格納する
ary = []
100.times{|i| ary[i] = i + 1 }

# Rangeオブジェクトのto_aメソッドを使う
ary = (1..100).to_a

(2)の解答

# 配列を作成する
ary = (1..100).to_a

# 配列の全ての要素を100倍した値を含む新しい配列を作る
ary2 = ary.collect{|i| i * 100 }
p ary2

# 配列の全ての要素を100倍する
ary.collect!{|i| i * 100 }
p ary

(3)の解答

# 配列を作成する
ary = (1..100).to_a

# aryから3の倍数だけを取り出す
ary2 = ary.select{|i| i % 3 == 0 }
p ary2

# aryから3の倍数以外の数を削除する
ary.reject!{|i| i % 3 != 0 }
p ary

(4)の解答

Array#sortArray#sort_byを使う方法では、ブロックの結果を-1倍することによって、逆にソートされるようにしています。

# 配列を作成する
ary = (1..100).to_a

# (a) Array#reverseを使う
ary2 = ary.reverse
p ary2

# (b) Array#sortを使う
ary2 = ary.sort{|a, b| -(a <=> b) }
p ary2

# (c) Array#sort_byを使う
ary2 = ary.sort_by{|i| -i }
p ary2

(5)の解答

Array#eachを使う場合は値を蓄えておくための変数(例中のresult)を用意する必要がありますが、Array#injectを使う場合は必要ありません。

# 配列を作成する
ary = (1..100).to_a

# (a) Array#eachで和を求める
result = 0
ary.each{|i| result += i }
p result

# (b) Array#injectで和を求める
p ary.inject(0){|memo, i| memo += i }

(6)の解答

# 配列を作成する
ary = (1..100).to_a

# randを使って配列をかき混ぜる
ary2 = ary.sort_by{|i| rand }
p ary2

(7)の解答

取り出す要素の先頭のインデックスと必要な要素の数を指定します。

# 配列を作成する
ary = (1..100).to_a
result = Array.new
10.times{|i|
  result << ary[i*10, 10]
}
p result

(8)の解答

Array#zipを使います。

def sum_array(ary1, ary2)
  result = Array.new
  ary1.zip(ary2){|a, b| result << a + b }
  return result
end

p sum_array([1, 2, 3], [4, 6, 8])

(9)の解答

開き括弧に出会ったらをスタックに積んで、閉じ括弧に出会ったときに取り出します。取り出した開き括弧が、閉じ括弧に対応するものかどうかを調べます。

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

p balanced?([])                   #=> true
p balanced?(["(",")"])            #=> true
p balanced?(["{","(",")","}"])    #=> true
p balanced?(["{","(",")"])        #=> false
p balanced?(["(",")","}"])        #=> false

p balanced?(["(", "{", "{", "}", "(", ")", "}", "(", ")", ")"]) #=> true
p balanced?(["(", "{", "{", "}", "(", "}", ")", ")"])           #=> false

文字列(String)クラス

(1)の解答

str = "Ruby is an object oriented programming language"

# (a) 各単語を要素とする配列を作る
ary = str.split
p ary

# (b) 配列をアルファベット順にソートする
p ary.sort

# (c) 配列を大文字と小文字を区別せずに単語順にソートする
p ary.sort_by{|s| s.downcase }

# (d) 全ての単語の先頭を大文字にする
ary = str.split
cap_ary = ary.collect{|word| word.capitalize }
p cap_ary.join(" ")

# (e) 文字の出現数をカウントする
result = Hash.new(0)             # 集計用のHashを用意する
chars = str.split(//)            # 文字列を文字単位に分割する
chars.each{|c| result[c] += 1 }  # 文字毎に出現回数を数える
result.keys.sort.each{|c|    
  puts "'#{c}': #{"*" * result[c]}"
}

補足:Array#joinを利用していますが本文中で紹介されていません。 このメソッドは配列に含まれる文字列を連結します。 要素と要素の間に挿入する文字列を引数で指定することもできます。

p ["a", "b", "c"].join        #=> "abc"
p ["a", "b", "c"].join("-")   #=> "a-b-c"

(2)の解答

各桁の数字をString#gsubで置き換えていきます。

def kan2num(string)
  digit4 = digit3 = digit2 = 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

p kan2num("七千八百二十三")
p kan2num("千八百二十三")
p kan2num("八百二十三")
p kan2num("百二十三")
p kan2num("二十三")
p kan2num("十三")
p kan2num("三")

(3)の解答

この問題はちょっとしたパズルです。各桁の数字を決まった数のアスタリスクに置き換え、前の桁までの部分は10回足し込むことによって10倍します。

def num2astrisk(str)
  num = ""
  str.split(//).each{|char|
    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 + num + num + num + num + num + num + num + num + num + char
  }
  return num
end

p num2astrisk("1")
p num2astrisk("2")
p num2astrisk("3")
p num2astrisk("4")
p num2astrisk("5")
p num2astrisk("10")
p num2astrisk("15")
p num2astrisk("20")
p num2astrisk("25")
p num2astrisk("30")
p num2astrisk("35")
p num2astrisk("40")

ハッシュ(Hash)クラス

(1)、(2)、(3)の解答

# (1) wdayを定義する
wday = {
  "sunday"    => "日曜日",
  "monday"    => "月曜日",
  "tuesday"   => "火曜日",
  "wedensday" => "水曜日",
  "thursday"  => "木曜日",
  "friday"    => "金曜日",
  "saturday"  => "土曜日",
}

# (2) wdayのサイズを求める
p wday.size  #=> 7

# (3) wdayのサイズを求める
%w(sunday monday tuesday wedensday thursday friday saturday).each{|day|
  puts "「#{day}」は#{wday[day]}のことです。"
}

(4)の解答

def str2hash(str)
  hash = Hash.new()
  array = str.split(/\s+/)
  while key = array.shift
    value = array.shift
    hash[key] = value
  end
  return hash
end

p str2hash("bule 青 white 白 red 赤");

(5)の解答

class OrderedHash
  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
  end

  def each()
    @keys.each{|key|
      yield(key, @content[key])
    }
  end

end

oh = OrderedHash.new()
oh["one"] = 1
oh["two"] = 2
oh["three"] = 3
oh["two"] = 4
p oh.keys()
oh.each{|key,value|
  p [key, value]
}

正規表現(Regexp)クラス

(1)の解答

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

p get_local_and_domain("info@example.com")

(2)の解答

「難しい」という部分が2回出現しますが、1回の置換で置き換えるのは難しいので2回に分けています。

message = "オブジェクト指向は難しい! なんて難しいんだ!"
message.sub!(/難しい/, "簡単だ")
message.sub!(/難しいんだ/, "簡単なんだ")
puts message

(3)の解答

def word_capitalize(str)
  return str.split(/\-/).collect{|w| w.capitalize}.join('-')
end

p word_capitalize("in-reply-to") #=> "In-Reply-To"
p word_capitalize("X-MAILER")    #=> "X-Mailer"

IOクラス

(1)の解答

行数、単語数、文字数を出力するwcメソッドを作成しました。この例ではString#splitを使って行を単語に分割していますが、行頭に空白を含む場合はString#splitの結果に空白の文字列が含まれるため、これを削除していることに注意してください。

def wc(file)
  nline = nword = nchar = 0
  File.open(file){|io|
    io.each{|line|
      words = line.split(/\s+/).reject{|w| w.empty? }
      nline += 1
      nword += words.length
      nchar += line.length
    }
  }
  puts "lines=#{nline} words=#{nword} chars=#{nchar}"
end

wc(__FILE__)

(2)の解答

def tee(input, *outputs)
  input.each{|line|
    outputs.each{|io| io.write(line) }
  }
end

open(__FILE__){|io|
  tee(io, $stdout, $stderr)
}

(3)の解答

def tail(lines, file)
  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

puts "==="
tail(10, __FILE__)

puts "==="
tail(3, __FILE__)

FileクラスとDirクラス

(1)の解答

def print_libraries
  $:.each{|path|
    next unless FileTest.directory?(path)
    Dir.open(path){|dir|
      dir.each{|name|
        if name =~ /\.rb$/i
          puts name
        end
      }
    }
  }
end

print_libraries

本文中では詳しく取り上げていませんが、RubyのライブラリにはRubyで記述されたものの他に、C言語などで記述された拡張ライブラリがあります。拡張ライブラリは「.rb」ではなく、「.dll」や「.so」といったプラットフォームによって異なる拡張子を持ったファイル名になっています。

rbconfigライブラリから、この拡張子を取得して拡張ライブラリにも対応させたバージョンを以下に示します。

require "rbconfig"

def print_libraries
  $:.each{|path|
    next unless FileTest.directory?(path)
    dlext = RbConfig::CONFIG["DLEXT"]
    Dir.open(path){|dir|
      dir.each{|name|
        if name =~ /\.rb$/i || name =~ /\.#{dlext}$/i
          puts name
        end
      }
    }
  }
end

print_libraries

(2)の解答

require "find"

def du(path)
  result = 0
  Find.find(path){|f|
    if File.file?(f)
      result += File.size(f)
    end
  }
  printf("%d %s\n", result, path)
  return result
end

du(ARGV[0] || ".")

TimeクラスとDateクラス

(1)の解答

def jparsedate(str)
  now = Time.now
  year = now.year
  month = now.month
  day = now.day
  hour = now.hour
  min = now.min
  sec = now.sec
  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
  }
  return Time.mktime(year, month, day, hour, min, sec)
end

p jparsedate("2006年12月23日午後8時17分50秒")
p jparsedate("12月23日午後8時17分50秒")
p jparsedate("午前8時17分50秒")
p jparsedate("8時17分50秒")

(2)の解答

def ls_t(path)
  entries = Dir.entries(path)                # エントリを取得
  entries.reject!{|name| /^\./ =~ name }     # "."で始まるファイルを削除

  mtimes = Hash.new                          # mtimeを収集しながらソート
  entries = entries.sort_by{|name|
    mtimes[name] = File.mtime(File.join(path, name))
  }

  entries.each{|name|
    printf("%-40s %s\n", name, mtimes[name]) # ファイル名とmtimeを表示
  }
rescue => ex
  puts ex.message
end

ls_t(ARGV[0] || ".")

(3)の解答

文房具の万年カレンダーの要領でカレンダーを整形する例を紹介します。2月30日などの存在しない日付は、月末の日付と比較することで弾いています。また、テーブル中の日付が存在しない部分も同じ条件で弾くために「99」で初期化しています。

require "date"

module Calendar
  WEEK_TABLE = [
    [99, 99, 99, 99, 99, 99,  1,  2,  3,  4,  5,  6,  7],
    [ 2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14],
    [ 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21],
    [16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28],
    [23, 24, 25, 26, 27, 28, 29, 30, 31, 99, 99, 99, 99],
    [30, 31, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99],
  ]

  module_function

  def cal(year, month)
    first = Date.new(year, month, 1)       # 指定された月の1日
    end_of_month = ((first >> 1) - 1).day  # 翌月の1日の前日
    start = 6 - first.wday                 # テーブルの何処から表示するか

    puts first.strftime("%B %Y").center(21)
    puts " Su Mo Tu We Th Fr St"
    WEEK_TABLE.each{|week|
      buf = ""
      week[start, 7].each{|day|
        if day > end_of_month
          buf << "   "
        else
          buf << sprintf("%3d", day)
        end
      }
      puts buf
    }
  end
end

t = Date.today
Calendar.cal(t.year, t.month)