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

数値(Numeric)クラス

(1)の解答

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

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

与えられた摂氏温度celsを、to_fを使ってFloatに変換しています(「10.4 数値型の変換」参照)。この変換を行わないと、celsがIntegerだった場合、正しく計算されません(5で割るところの結果がIntegerになってしまいます)。

(2)の解答

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

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

さらに、華氏を1度から100度まで変えるには、uptoメソッドを使います。

1.upto(100) do |i|
  print i, " ", fahr2cels(i), "\n"
end

(3)の解答

「10.6 乱数」で説明したrandメソッドを使います。単に「rand(6)」とした場合は、0から5の値が返るので結果に1を足します。

def dice
  return rand(6) + 1
end

(4)の解答

単純にdice+dice+...と書いても可能ではありますが、やはりここは繰り返しを使うべきでしょう。

def dice10
  ret = 0
  10.times do
    ret += dice
  end
  ret
end

(5)の解答

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

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

配列(Array)クラス

(1)の解答

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

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

# 91ページで少しだけ触れた範囲オブジェクトのto_aメソッドを使う
a = (1..100).to_a

(2)の解答

普通にArray#collectを使えば、新しい配列が作れます。同じ配列をそのまま100倍したいときは、「!」がついたArray#collect!を使います。

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

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

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

(3)の解答

条件に当てはまるものをとりのぞくには、Array#rejectを使います。

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

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

# なお、条件に当てはまるものだけ返す、!が不要なselectというメソッドもあります
a4 = a.select{|i| i % 3 == 0 }
p a4

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

(4)の解答

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

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

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

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

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

(5)の解答

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

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

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

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

(6)の解答

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

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

(7)の解答

Array#eachでary1の各要素にループさせるのと同時に、インデックスを使ってary2の各要素にアクセスするのがポイントです。また、別解として、Array#zipというメソッドを使うと、2つの配列に対して同時に各要素を参照することができます。

def sum_array(ary1, ary2)
  result = Array.new
  i = 0
  ary1.each do |elem1|
    result << elem1 + ary2[i]
    i+=1
  end
  return result
end

# Array#zipを使った別解
def sum_array_zip(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])

文字列(String)クラス

(1)の解答

単純にsplitで分割すればだいじょうぶです。

str = "Ruby is an object oriented programming language"
ary = str.split
p ary

(2)の解答

アルファベットだけの場合、引数なしのArray#sortでソートされます。この辺りは配列の復習にもなっています。

str = "Ruby is an object oriented programming language"
ary = str.split
p ary.sort

(3)の解答

こちらではArray#sort_byを使って、引数の比較をしています。その際、String#downcaseで強制的に小文字にしてから比較するため、大文字小文字の区別がなくなります。

str = "Ruby is an object oriented programming language"
ary = str.split
p ary.sort_by{|s| s.downcase }

(4)の解答

文字列の先頭を大文字にするにはString#capitalizeを使います。これを配列の各要素に行うため、Array#collectと組み合わせています。

str = "Ruby is an object oriented programming language"
ary = str.split
cap_ary = ary.collect{|word| word.capitalize }

str = ""
cap_ary.each do |s|
  str << s+" "
end
p str

## 別解
p cap_ary.join(" ")

なお、文字列の連結には別解で示したArray#joinを使うと簡単にできです。 このメソッドは配列に含まれる文字列を連結するもので、 要素と要素の間に挿入する文字列を引数で指定することもできます。

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

(5)の解答

ハッシュを作成して、文字をキー、出現した回数を値として記録します。最後に文字をソートして回数個数のアスタリスクとともに出力します。

str = "Ruby is an object oriented programming language"
count = Hash.new
str.each_char do |c|
  count[c] = 0 unless count[c]
  count[c] += 1
end
count.keys.sort.each do |c|
  printf("'%s': %s\n", c, "*" * count[c])
end

ハッシュを初期化する際にデフォルト値として0を返すようにすると次のようになります。

str = "Ruby is an object oriented programming language"
count = Hash.new(0)
str.each_char do |c|
  count[c] += 1
end
count.keys.sort.each do |c|
  printf("'%s': %s\n", c, "*" * count[c])
end

(6)の解答

この問題は応用問題でかなり難しいです。

各桁の数字を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("十三")
p kan2num("三")

ハッシュ(Hash)クラス

(1)の解答

1つずつ個別に定義してもよいのですが、まとめて定義してみます。

wday = {
  "sunday"    => "日曜日",
  "monday"    => "月曜日",
  "tuesday"   => "火曜日",
  "wedensday" => "水曜日",
  "thursday"  => "木曜日",
  "friday"    => "金曜日",
  "saturday"  => "土曜日",
}

(2)の解答

普通にHash#sizeを使えば求まります。

wday = {
  "sunday"    => "日曜日",
  "monday"    => "月曜日",
  "tuesday"   => "火曜日",
  "wedensday" => "水曜日",
  "thursday"  => "木曜日",
  "friday"    => "金曜日",
  "saturday"  => "土曜日",
}

p wday.size  #=> 7

(3)の解答

普通に配列でキーを与えてもいいのですが、簡単のため%wを使ってみます。

wday = {
  "sunday"    => "日曜日",
  "monday"    => "月曜日",
  "tuesday"   => "火曜日",
  "wedensday" => "水曜日",
  "thursday"  => "木曜日",
  "friday"    => "金曜日",
  "saturday"  => "土曜日",
}

%w(sunday monday tuesday wedensday thursday friday saturday).each do |day|
  puts "「#{day}」は#{wday[day]}のことです。"
end

(4)の解答

文字列をString#splitで分割したあと、Array#shiftで一つずつ取り出して、ハッシュを作っていきます。

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 白\nred 赤");

正規表現(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回に分けています。「難しい」を「簡単だ」に置き換えると、「難しいんだ」の部分が「簡単だんだ」となってしまうため、先に「難しいんだ」を「簡単なんだ」に置き換えています。

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

(3)の解答

基本的には文字列クラスの練習問題(4)と同様ですが、「-」の正規表現を作るときにはエスケープします。回答例ではメソッドチェインを使って1行で書いてみました。

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)の解答

これは個別にスクリプトを書いてみます。まず、ファイルの逆順です。 IO#readlinesで行ごとに読み込んだ後、IO#rewindで先頭に戻し、IO#truncateで空にしてから、IO#reverseで逆順にしたものを書きこみます。

def reverse(input)
  open(input, "r+") do |f|
    lines = f.readlines
    f.rewind
    f.truncate(0)
    f.write lines.reverse.join()
  end
end

reverse(ARGV[0])

次に、1行先頭出力です。これは、実は先ほどのスクリプトを少しいじるだけで実現できます。

def reverse(input)
  open(input, "r+") do |f|
    lines = f.readlines
    f.rewind
    f.truncate(0)
    f.write lines[0]
  end
end

reverse(ARGV[0])

そして、1行末尾出力です。こちらはちょうど(a)(b)を組み合わせた形です。

def reverse(input)
  open(input, "r+") do |f|
    lines = f.readlines
    f.rewind
    f.truncate(0)
    f.write lines.reverse[0]
  end
end

reverse(ARGV[0])

(3)の解答

いったんqueueという変数に読み込んだ行を保存させるのがポイントです。

def tail(lines, file)
  queue = Array.new
  open(file) do |io|
    while line = io.gets
      queue.push(line)
      if queue.size > lines
        queue.shift
      end
    end
  end
  queue.each{|line| print line }
end

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

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

FileクラスとDirクラス

(1)の解答

FileTest.directory?を使ってディレクトリではないものを排除した後、Dir.openを使ってディレクトリ内のファイルのファイル名を調べていきます。

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

print_libraries

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

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

require "rbconfig"

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

print_libraries

(2)の解答

findライブラリの応用です。

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)の解答

日時を表す日本語文字列の解析は正規表現でがんばります。現在の時刻はTime.nowで取得し、与えられた文字列から取得できなかった項目を補います。最後に、Time.mktimeで時刻を生成します。

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+)(年|月|日|時|分|秒)/) do
    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
  end
  return Time.mktime(year, month, day, hour, min, sec)
end

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

(2)の解答

「.」で始まるファイルを削除してから、File.mtimeで取得できる日時の順にソートします。最後にファイル名と日付を出力します。

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

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

  entries.each do |name|
    printf("%-40s %s\n", name, mtimes[name]) # ファイル名とmtimeを表示
  end
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 do |week|
      buf = ""
      week[start, 7].each do |day|
        if day > end_of_month
          buf << "   "
        else
          buf << sprintf("%3d", day)
        end
      end
      puts buf
    end
  end
end

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