問題で与えられた式をそのまま使ってメソッドを定義します。
def cels2fahr(cels) return cels * 9.0 / 5.0 + 32.0 end
問題の式は「華氏=摂氏×9÷5+32」でしたが、途中の計算の結果が浮動小数点数になるように、9と5はそれぞれ9.0
や5.0
にしています。こうしないと、cels
が整数だった場合、正しく計算されません(5で割るところの結果がIntegerになってしまいます)。
(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
「12.7 乱数」で説明したRandom.rand
メソッドを使います。単に「Random.rand(6)
」とした場合は、0から5の値が返るので結果に1を足します。
def dice return Random.rand(6) + 1 end
単純にdice+dice+...
と書いても可能ではありますが、やはりここは繰り返しを使うべきでしょう。
def dice return Random.rand(6) + 1 end def dice10 ret = 0 10.times do ret += dice end ret end
まず、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
二つの方法を挙げます。もっと小さな配列なら「ary = [1, 2, 3, ...]
」とリテラルで定義してもいいでしょう。
# 空の配列を作って、1から100までの値を格納する a = [] 100.times{|i| a[i] = i + 1 } # 範囲オブジェクト(p.176)のto_aメソッドを使う a = (1..100).to_a
普通に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
条件に当てはまるものをとりのぞくには、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
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
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 }
取り出す要素の先頭のインデックスと必要な要素の数を指定します。
# 配列を作成する ary = (1..100).to_a result = Array.new 10.times do |i| result << ary[i*10, 10] end p result
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])
単純にsplit
で分割すれば大丈夫です。
str = "Ruby is an object oriented programming language" ary = str.split p ary
アルファベットだけの場合、引数なしのArray#sort
でソートされます。この辺りは配列の復習にもなっています。
str = "Ruby is an object oriented programming language" ary = str.split p ary.sort
こちらではArray#sort_by
を使って、引数の比較をしています。その際、String#downcase
で強制的に小文字にしてから比較するため、大文字小文字の区別がなくなります。
str = "Ruby is an object oriented programming language" ary = str.split p ary.sort_by{|s| s.downcase }
ハッシュを作成して、文字をキー、出現した回数を値として記録します。最後に文字をソートして回数個数のアスタリスクとともに出力します。
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
この問題は応用問題でかなり難しいです。
各桁の数字を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("三")
1つずつ個別に定義してもよいのですが、まとめて定義してみます。
wday = { "sunday" => "日曜日", "monday" => "月曜日", "tuesday" => "火曜日", "wedensday" => "水曜日", "thursday" => "木曜日", "friday" => "金曜日", "saturday" => "土曜日", }
普通にHash#size
を使えば求まります。
wday = { "sunday" => "日曜日", "monday" => "月曜日", "tuesday" => "火曜日", "wedensday" => "水曜日", "thursday" => "木曜日", "friday" => "金曜日", "saturday" => "土曜日", } p wday.size #=> 7
普通に配列でキーを与えてもいいのですが、簡単のため%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
文字列を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 赤");
メールアドレスは複雑なルールがあったり、さらにはルールに適合しないのに実際には使われているアドレスもあったりするなど、ややこしい事情もあるのですが、ここではわりきった形で解析してみました。
def get_local_and_domain(str) str =~ /^([^@]+)@(.*)$/ localpart = $1 domain = $2 return [localpart, domain] end p get_local_and_domain("info@example.com")
「難しい」という部分が2回出現しますが、1回の置換で置き換えるのは難しいので2回に分けています。「難しい」を「簡単だ」に置き換えると、「難しいんだ」の部分が「簡単だんだ」となってしまうため、先に「難しいんだ」を「簡単なんだ」に置き換えています。
s = "オブジェクト指向は難しい!なんて難しいんだ!" puts s.gsub(/難しいんだ/, "簡単なんだ").gsub(/難しい/, "簡単だ")
基本的には文字列クラスの練習問題(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"
行数、単語数、文字数を出力する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__)
(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])
いったん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__)
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
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
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] || ".")
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)
設問のとおりの処理を順に行っています。出力されたファイルのエンコーディングを確認する方が大変かもしれませんね。
# 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
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?
誕生日を迎えるごとに年を取ると考えると以下のようになります。
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
メソッドにも挑戦してみてください。
日時を表す日本語文字列の解析は正規表現でがんばります。現在の時刻は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秒")
「.」で始まるファイルを削除してから、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] || ".")
文房具の万年カレンダーの要領でカレンダーを整形する例を紹介します。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)
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]
to_class = :class.to_proc p to_class.call("test") #=> String p to_class.call(123) #=> Fixnum p to_class.call(2 ** 100) #=> Bignum
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