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

数値(Numeric)クラス

(1)の解答

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

def cels2fahr(cels)
  return cels * 9.0 / 5.0 + 32.0
end

問題の式は「華氏=摂氏×9÷5+32」でしたが、途中の計算の結果が浮動小数点数になるように、9と5はそれぞれ9.05.0にしています。こうしないと、celsが整数だった場合、正しく計算されません(5で割るところの結果がIntegerになってしまいます)。

(2)の解答

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

def fahr2cels(fahr)
  return (fahr.to_f - 32) * 5.0 / 9.0
end

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

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

(3)の解答

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

def dice
  return Random.rand(6) + 1
end

(4)の解答

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

def dice
  return Random.rand(6) + 1
end

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

1.upto(10) do |n|
  puts n if prime?(n)
end

配列(Array)クラス

(1)の解答

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

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

# 範囲オブジェクト(p.176)のto_aメソッドを使う
a = (1..100).to_a

(2)の解答

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

# 配列を作成する
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#sortを使う方法では>=<の左右を逆にすることにより、Array#sort_byを使う方法ではブロックの結果を-1倍することにより、逆順にソートされるようにしています。

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

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

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

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

(5)の解答

素直な回答として、Array#eachで値を足していきます。

別解として、Array#inject(またはArray#reduce)を使う方法があります。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#eachary1の各要素にループさせるのと同時に、インデックスを使って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)の解答

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

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

(5)の解答

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

各桁の数字を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)の解答

(a) これは個別にスクリプトを書いてみます。まず、ファイルの逆順です。 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])

(b) 次に、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])

(c) そして、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__)

(4)の解答

1文字の書き込みを100,000回繰り返して、書き込むごとにファイルのサイズを調べます。書き込んだ回数とサイズが一致したときの値を表示させて様子を観察します。

filename = "test.txt"
writen = 0

File.open(filename, "w") do |f|
  while writen < 100_000
    writen += 1
    f.write("a")
    size = File.size(filename)
    p [writen, size] if writen == size
  end
end

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] || ".")

エンコーディング(Encoding)クラス

(1)の解答

String#encodeメソッドで各文字列をUTF-8に変換してから連結します。

# encoding: utf-8

def to_utf8(str_euc, str_sjis)
  ## encodeメソッドを使ってそれぞれUTF-8に変換してから連結します
  str_euc.encode("UTF-8") + str_sjis.encode("UTF-8")
end

## 以下のように実行します。
str_euc  = "こんにちは".encode("EUC-JP")
str_sjis = "さようなら".encode("Shift_JIS")

puts to_utf8(str_euc, str_sjis)

(2)の解答

設問のとおりの処理を順に行っています。出力されたファイルのエンコーディングを確認する方が大変かもしれませんね。

# encoding: utf-8

## Shift_JISでsjis.txtに出力します
File.open("sjis.txt","w:Shift_JIS") do |f|
  f.write("こんにちは")
end

## sjis.txtを開いて、それをUTF-8で出力します
File.open("sjis.txt","r:Shift_JIS") do |f|
  str = f.read
  ## strはShift_JISなので、putsで出力する際にUTF-8にします
  puts str.encode("UTF-8")
end

(3)の解答

Windows-31JはIANA(Internet Assigned Number Authority:インターネット上のプロトコルで使用されるアドレスなどの標準化を行う機関)で登録された名前で、Character Setsの説明によると、Shift_JIS(JIS X0201:1997、JIS X0208:1997)に、以下を追加したものとされています。丸つきの数字などが拡張部分に該当します。

# encoding: utf-8

str1 = '①'
str2 = '①'

str01 = str1.encode(Encoding::Windows_31J)
p str01.force_encoding("Windows-31J").valid_encoding?

str02 = str1.encode(Encoding::Windows_31J)
p str02.force_encoding("Shift_JIS").valid_encoding?



TimeクラスとDateクラス

(1)の解答

誕生日を迎えるごとに年を取ると考えると以下のようになります。

require "date"

class Person
  attr_reader :birth_date

  # Person.new でキーワード引数で生年月日を受け取る
  def initialize(birth_date: )
    @birth_date = birth_date
  end

  # ある日付の年齢を返す。日付を指定されない場合は今日の年齢になる。
  def age(date=Date.today)
    # 生まれる前なら -1 を返す(エラーケース)
    return -1 if date < birth_date

    # 年数を計算する
    years = date.year - birth_date.year

    if date.month < birth_date.month
      # 誕生月の前なら1引く
      years -= 1
    elsif date.month == birth_date.month && date.day < birth_date.day
      # 誕生月と同月で誕生日の前なら1引く
      years -= 1
    end
    return years
  end
end

ruby = Person.new(birth_date: Date.new(1993, 2, 24))
p ruby.birth_date                  # 生年月日
p ruby.age                         # 今日
p ruby.age(Date.new(2013, 2, 23))  # 20歳の前日
p ruby.age(Date.new(2013, 2, 24))  # 20歳の誕生日
p ruby.age(Date.new(1988, 2, 24))  # 生まれる前

ちなみに、4月1日生まれの人は早生まれの扱いになります。これは「年齢計算ニ関スル法律」の「起算日(出生日)に応答する日の前日をもって満了」するという決まりによるもので、考え方としては、「誕生日」から「翌年の誕生日の前日」までの期間でカレンダー上は1年間の全ての日付を過ごしたことになるため、誕生日の前日をもってその年齢を満了したとするためです。そのため、4月1日生まれの人は3月31日時点で満年齢が増えることから早生まれとなるのです。

この計算を考慮したlegal_ageメソッドにも挑戦してみてください。

(2)の解答

日時を表す日本語文字列の解析は正規表現でがんばります。現在の時刻は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秒")

(3)の解答

「.」で始まるファイルを削除してから、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] || ".")

(4)の解答

文房具の万年カレンダーの要領でカレンダーを整形する例を紹介します。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

if arg = ARGV.first
  d = Date.parse(arg)
else
  d = Date.today
end
Calendar.cal(d.year, d.month)

Procクラス

(1)の解答

objからeachメソッドで要素を取り出してブロックを適用した結果を配列に格納していきます。
def my_collect(obj, &block)
  buf = []
  obj.each do |elem|
    buf << block.call(elem)
  end
  buf
end

ary = my_collect([1,2,3,4,5]) do |i|
  i * 2
end

p ary  #=> [2, 4, 6, 8, 10]

(2)の解答

以下のような実行結果が得られます。
to_class = :class.to_proc
p to_class.call("test")    #=> String
p to_class.call(123)       #=> Fixnum
p to_class.call(2 ** 100)  #=> Bignum

(3)の解答

メソッド内で作ったローカル変数に値を足し込んでいきます。
def accumlator
  total = 0
  Proc.new do |x|
    total += x
  end
end

acc = accumlator
p acc.call(1)    #=> 1
p acc.call(2)    #=> 3
p acc.call(3)    #=> 6
p acc.call(4)    #=> 10