2009-02-05練習: Unix の 「ls -t」 コマンドのようなメソッド
『たのしいRuby』の第 17 章(Time クラス)の練習問題(2)
Unixの「ls -t」コマンドのように、ディレクトリを指定すると、そのディレクトリの下にある ファイルを時刻の順に並べるメソッド ls_t を定義しましょう。 このメソッドは引数を1つだけ取ります。 ls -t(調べるディレクトリ名) 指定されたディレクトリの下にあるファイルの名前を、その時刻の古い順に並べて表示します。
これだけだと、ファイルの名前だけ表示すればいいのか、それとも、時刻も表示するのかなど、仕様が決められないので、Unix の ls コマンドの t オプションについて調べた。
そしたら、UNIXコマンド [ls]では、t オプションをつけると、最新更新日時の最新のものから順に表示する、と説明されていた。
それで、Linux で実際に「ls -t」コマンドを使ってみたら、更新日時が新しいものが上にきていた。更新日時は表示されず、ファイル名だけが表示された。
実際に使ってみたときの結果から、以下のようなものをつくることにする。
- ディレクトリを指定すると、そのディレクトリの下にあるファイルの名前を、更新日時の新しいものから表示する
- 更新日時は表示しない
- 隠しファイル(ファイル名が . (ピリオド) で始まるもの)は表示しない
書いたもの
def ls_t(dir) entries = [] Dir.foreach(dir) {|path| next if /^\./ =~ path modified_time = File.mtime(File.join(dir, path)) entries << [path, modified_time] } entries.sort!{|a, b| b[1] <=> a[1] }.each{|entry| puts entry[0] } end ls_t(ARGV[0])
フォルダを「更新日時」で並べ替えたときと同じ並び順になったし、隠しファイルは表示されなかった。つくろうろしたものはできた、と思う。
2009-02-03たのしいRuby P.306 (1)
$: には、Ruby が利用するライブラリが置かれているディレクトリの名前が 配列の形で格納されています。 この変数を使って、Ruby が利用できるライブラリのファイル名を 順に出力するメソッド printLibrary メソッドを定義しましょう。
「Ruby が利用できるライブラリのファイル名」ってどういう意味なんだろ?基礎知識が足りなくて、こんなところでも詰まる。
Ruby 1.8.7 リファレンスマニュアルの variable $-I にこんな記述があったのだけど、
require 'foo' を実行すると、以下のように foo.rb と foo.so が交互に探索されます。
ライブラリを require したときに拡張子が rb のものと so のものを探す、ということは、それらが「Ruby が利用できるライブラリのファイル名」なんだろうか。
とりあえず、Ruby が利用するライブラリが置かれている、カレントディレクトリ以外のディレクトリの名前を表示して、さらに、そのディレクトリ以下に拡張子が rb、so のファイルがあったら、それらのファイルの名前も表示するようにする。
書いたもの
def print_library $:.each{|path| next unless FileTest.directory?(path) next if path == "." puts path Dir.foreach(path) {|name| if name =~ /\.rb$/i || name =~ /\.so$/i puts "|--" + name end } puts } end print_library
実行結果
C:/ruby187/lib/ruby/site_ruby/1.8 C:/ruby187/lib/ruby/site_ruby/1.8/i386-msvcrt C:/ruby187/lib/ruby/site_ruby C:/ruby187/lib/ruby/vendor_ruby/1.8 C:/ruby187/lib/ruby/vendor_ruby/1.8/i386-msvcrt C:/ruby187/lib/ruby/vendor_ruby C:/ruby187/lib/ruby/1.8 |--abbrev.rb |--base64.rb |--benchmark.rb (長いので省略) |--weakref.rb |--webrick.rb |--yaml.rb C:/ruby187/lib/ruby/1.8/i386-mswin32 |--bigdecimal.so |--curses.so |--dbm.so (長いので省略) |--Win32API.so |--win32ole.so |--zlib.so
つくろうとしたものはできた、と思う。
2009-02-02たのしいRuby P.313 (1)
「"2001年12月23日午後8時17分50秒"」といったように、「年・月・日・時・分・秒」を 使った時刻の文字列を Time オブジェクトに変換して返すメソッド jparsedate を定義しましょう。
Time.parse の仕様を参考にして、以下のようなメソッドをつくろう。
- 与えられた文字列に時刻の上位の要素がなかったら、now の要素が使われる。
- 与えられた文字列に時刻の下位の要素がなかったら、最小値( 1 か 0 )が使われる。
- 「午後○時」だったら、12 を足して 24 時間表示にする。
書いたもの
$KCODE = "SJIS" def jparsedate(str) now = Time.now year = now.year month = now.month day = now.day hour = now.hour min = now.min sec = 0 str.scan(/([午前|午後]*)?(\d*)([年|月|日|時|分|秒])/){ case $3 when "年" year = $2 month, day, hour, min = 1, 1, 0, 0 when "月" month = $2 day, hour, min = 1, 0, 0 when "日" day = $2 hour, min = 0, 0 when "時" if $1 == "午後" hour = $2.to_i + 12 else hour = $2 end min = 0 when "分" min = $2 when "秒" sec = $2 end } Time.mktime(year, month, day, hour, min, sec) end p jparsedate("2001年12月23日午後8時17分50秒") p jparsedate("01年12月23日午前8時50秒") p jparsedate("12月23日午後10時") p jparsedate("2001年12月") p jparsedate("8時17分50秒") p jparsedate("50秒")
実行結果
Sun Dec 23 20:17:50 +0900 2001 Sun Dec 23 08:00:50 +0900 2001 Wed Dec 23 22:00:00 +0900 2009 Sat Dec 01 00:00:00 +0900 2001 Mon Feb 02 08:17:50 +0900 2009 Mon Feb 02 21:55:50 +0900 2009
エクセルだと、「21時20分」とか「7時23分45秒」とかは時刻として認識されるけど、「8時」とか「8時50秒」とか「3分20秒」とかは文字列として認識されるらしいのよね。「50秒」みたいのまで時刻として受け入れるのは、ゆるふわすぎかな?どうしよう。
似たようなことを書いていて冗長なのが気に入らない。何とかしたいのに、ジグソーパズルでこっちのピースを入れたら、あっちのピースが外れてたみたいなかんじになって、妙案が浮かばない。ブレイクスルーしたい。
2009-01-15練習:たのしいRuby
P.203 (1) square メソッドを定義する
数値からなる配列 nums に対して、その個々の要素を自乗した要素からなる 配列を返すメソッド square を 3 通りの方法で定義しましょう。
(a) collect を使う
def square1(nums) nums.collect{|i| i**2 } end nums = [1, 3, 5, 7, 9] p square1(nums)
実行結果
[1, 9, 25, 49, 81]
(b) collect は使わず、each を使う
def square2(nums) result = [] nums.each{|i| result << i**2 } return result end nums = [2, 4, 6, 8, 10] p square2(nums)
実行結果
[4, 16, 36, 64, 100]
(c) collect も each も使わず、インデックスと[ ]を使う
def square3(nums) nums.each_index{|index| nums[index] **= 2 } end nums = [1, 9, 25, 49, 81] p square3(nums)
実行結果
[1, 81, 625, 2401, 6561]
P.203 (2) sum_array メソッドを定義する
数値からなる配列 nums1 と nums2 に対して、それらの個々の要素を足し合わせた 要素からなる配列を返すメソッド sum_array を定義しましょう。
書いたもの
def sum_array(ary1, ary2) result = [] ary1.zip(ary2){|a, b| result << a + b } return result end nums1 = [1, 2, 3] nums2 = [4, 5, 6] p sum_array(nums1, nums2)
実行結果
[5, 7, 9]
rochefort さんの日記 を見て、zip と map を使えば、もっとすっきり書けることを知った。なるほどねぇ。
あと、ためしに、要素数が異なる配列を足してみた。案の定、ary1 のほうが少ないときは ary1 と同じ要素数の配列が返ってきたけど、ary1 のほうが多いときは、nil と Fixnum は足せないよ、というエラーが返ってきた。
このメソッドで足せるかどうかを事前にチェックすることも考えた。チェックするなら、配列の要素数が同じか、と、配列の要素が数値オブジェクトだけか、の 2 つかな。でも、チェックを入れると、その分、sum_array するのに時間がかかるし、いまいちかも。で、やっぱり、要素数が異なる配列を足そうとしたときなどのエラーは、実行時のエラーにお任せすることにした。
P.203 (3) balanced? メソッドを定義する
(, ), {, } という 4 つの文字を要素とした配列がある。この配列に対して、
カッコが正しく対応しているかを調べるメソッド balanced? を定義しましょう。
「カッコが正しく対応している」とは、以下のような状態のことです。
・( と ) の数が同じ
・{ と } の数が同じ
・「( )」の対応と「{ }」の対応が交差することはない
書いたもの 1
正規表現を使ってみた。
def balanced?(ary) str = ary.join while (/\(\)/ =~ str) || (/\{\}/ =~ str) result = str.gsub!(/\(\)|\{\}/, "") end if result == "" return true else return false end end p balanced?(%w! ( !) p balanced?(%w! { } !) p balanced?(%w! ( { { } ( ) } ( ) ) !) p balanced?(%w! ( { { ( } ( ) } ( ) ) !)
実行結果
false true true false
書いたもの 2
今度は、スタックを使って。
def balanced2?(ary) stack = [] ary.each {|elem| case elem when "(" stack << elem when "{" stack << elem when ")" if stack.last != "(" return false else stack.pop end when "}" if stack.last != "{" return false else stack.pop end else return false end } if stack.size == 0 return true else return false end end p balanced2?(%w! ( !) p balanced2?(%w! { } !) p balanced2?(%w! ( { { } ( ) } ( ) ) !) p balanced2?(%w! ( { { ( } ( ) } ( ) ) !)
実行結果
false true true false
2009-01-14練習:たのしいRuby P.172 - 173
練習問題 (1)
華氏を摂氏に変換するメソッド fahr2celsius を定義しましょう。 摂氏と華氏の変換の公式:摂氏 = 5 × (華氏 - 32) ÷ 9
書いたもの
def fahr2celsius(fahr) 5 * (fahr - 32).quo(9) end p fahr2celsius(-40) p fahr2celsius(-17.8) p fahr2celsius(0) p fahr2celsius(98.6)
実行結果
-40.0 -27.6666666666667 -17.7777777777778 0.0 37.0
最初は、こんなふうに書いた。
def fahr2celsius(fahr) 5 * (fahr - 32) / 9 end
fahr2celsius(0) が -18 になった。この結果は正しくない。そこで、Rubyリファレンスマニュアル - trap::Numeric を参考に、Numeric#quo を使うことにした。
Rubyリファレンスマニュアル - Numeric#truncate にあるように任意桁の四捨五入を行うメソッドを定義し、小数点以下第 2 位を四捨五入して、小数点第 1 位まで表示するようにしようかとも思ったけど、変換した値を使った計算をしたら、誤差が出てしまうかもしれないから、丸めるのはやめた。
練習問題 (2)
摂氏を表すクラス Celsius を定義しましょう。 このクラスは、以下のメソッドを持つとします。 ■Celsius.new(c) 摂氏の温度 c の値を持つ Celsius クラスのインスタンスを作る。 ■Celsius#to_celsius 摂氏の温度を数値として返す。 ■Celsius#to_fahr 華氏の温度を数値として返す。 ■Celsius#+(cl) 摂氏オブジェクト cl と足し算をして、新しい摂氏オブジェクトを返す。
書いたもの
$KCODE = "SJIS" class Celsius # 摂氏の温度 c の値を持つ Celsius クラスのインスタンスを作る。 def initialize(c) @c = c end # 摂氏の温度を数値として返す。 def to_celsius @c end # 華氏の温度を数値として返す。 def to_fahr (@c * 9).quo(5) + 32 end # cl が摂氏オブジェクトか数値オブジェクトだったら、足し算をして、 # 新しい摂氏オブジェクトを返す。そうでなかったら、エラーを返す。 def +(cl) case cl when Celsius Celsius.new(@c + cl.to_celsius) when Numeric Celsius.new(@c + cl) else $stderr.puts "in `+': Celsius can't be coerced into #{cl.class} (TypeError)" end end end cel1 = Celsius.new(37.0) cel2 = Celsius.new(-2) cel3 = Celsius.new(-17.7777777777778) p "cel1: 摂氏 #{cel1.to_celsius} 度、華氏 #{cel1.to_fahr} 度" p "cel2: 摂氏 #{cel2.to_celsius} 度、華氏 #{cel2.to_fahr} 度" p "cel3: 摂氏 #{cel3.to_celsius} 度、華氏 #{cel3.to_fahr} 度" # 摂氏オブジェクトと摂氏オブジェクトの足し算 cel4 = cel1 + cel2 p cel4.is_a?(Celsius) p "摂氏 #{cel1.to_celsius} 度 + 摂氏 #{cel2.to_celsius} 度 = 摂氏 #{cel4.to_celsius} 度" # 摂氏オブジェクトと数値オブジェクトの足し算 cel5 = cel1 + 3.5 p cel5.is_a?(Celsius) p "摂氏 #{cel1.to_celsius} 度 + 摂氏 3.5 度 = 摂氏 #{cel5.to_celsius} 度" # 摂氏オブジェクトと文字列オブジェクトの足し算 cel6 = cel1 + "絶対零度"
実行結果
"cel1: 摂氏 37.0 度、華氏 98.6 度" "cel2: 摂氏 -2 度、華氏 28.4 度" "cel3: 摂氏 -17.7777777777778 度、華氏 -4.2632564145606e-014 度" true "摂氏 37.0 度 + 摂氏 -2 度 = 摂氏 35.0 度" true "摂氏 37.0 度 + 摂氏 3.5 度 = 摂氏 40.5 度" in `+': Celsius can't be coerced into String (TypeError)
ありゃ。cel3 が思っていた結果になっていない。練習問題 (1) の結果から、"摂氏 -17.7777777777778 度、華氏 0 度" になると思ったのに。
原因は、自分で丸めなくても、コマンドラインの制限?で計算結果が丸められていたからみたい。小数点以下を 1 桁増やして、cel3 = Celsius.new(-17.77777777777778) にしたら、"cel3: 摂氏 -17.7777777777778 度、華氏 0.0 度" になった。
別の方法で何とかならないかなと思い、ためしに、四捨五入して表示するようにしてみた。そうしたら、小数点以下第 3 位までなら「華氏 0.0 度」になったけど、小数点以下第 4 位までにすると「華氏 -0.0004 度」になった。
私にとっては温度は小数点以下第 2 位まであれば十分で、それなら、メソッドの定義はこのままで、小数点以下第 3 位を四捨五入して、小数点以下第 2 位までの結果を得るようにすればよさそう。
より誤差が少なくて、使い勝手もよいものにするには、どうすべきなんだろう。
摂氏 <=> 華氏 が成り立つといいなと思ったけど、Celsius#to_celsius も Celsius#to_fahr も数値を返すから、メソッドチェーンはできないし、摂氏から華氏に変換したものをまた摂氏に変換しなおすことってあまりなさそうだし、いつまでも悩むのも何だし、次の問題にいこうかな。そういえば、Celsius#+(cl) では、何となく、数値オブジェクトも足せたほうがいいかなと思って足せるようにしたけど、Celsius#to_celsius や Celsius#to_fahr では数値オブジェクトは扱えないから、いい仕様ではないのかも。・・・未消化だけど、先に進んでみよう。
練習問題 (3)
数値 num が素数であるかを調べるメソッド prime?(num) を定義しましょう。
書いたもの
def prime?(num) # 小数は素数ではない if num.is_a?(Float) return false end # 2 より小さい整数は素数ではない if num < 2 return false end # 1 とその数自身以外の約数を持つ 2 以上の整数は素数ではない(合成数) (2..Math::sqrt(num)).each {|i| if num % i == 0 return false end } # 2 以上の自然数で、かつ 合成数でないなら、素数 return true end p prime?(2.1) p prime?(1) p prime?(2) p prime?(997) p prime?(9971) p prime?(9973)
実行結果
false false true true false true
単に「数値」と言われたとき、自分が入れそうなもの = 小数と整数 について判定できるようにしてみた。
引数 nums を書き換えることは意図したことではないです。手元のものは以下のように修正しました。
def square3(nums)
copy = nums.dup
copy.each_index{|index|
copy[index] **= 2
}
end
蛇足ですが、square3 は、最初はこんなふうに書いていました。
def square3(nums)
indexes = (0...nums.size).to_a
result = []
while index = indexes.shift
result << nums[index] ** 2
end
return result
end
これなら、nums を書き換えてはいなかったです。
インデックスの配列をわざわざつくらなくても、Ruby にはインデックスに対して何かをするメソッドがあるのではと思っていたところ、Array#each_index を見つけ、これを使いました。
マニュアルを読んで使い方を理解したつもりだったのですが、まだまだですね。