RubyのDateTimeとTimeはどちらを使うべきか
TimeはもともとPOSIXのtime_t型のラッパーで、内部的には1970年1月1日を起点とする秒数を保持している。
WikipediaによるUnix timeの記述を見ると、time_tは、なんと初期のUnixでは32ビット整数であったものの、60Hzのクロックに同期してカウントアップしていたそうだ。1秒間に60カウント進む。これでは2年半しか表現できない。のちに1Hz、つまり1秒1カウントで進むように変更され、これで前後130年の時刻を表現できるようになったそうな。
よく知られる2038年問題は32ビットの符号付き整数で表現できる1970年1月1日を起点とした場合の上限が2038年1月19日の午前3時14分7秒であるという話。
RubyのTimeクラスも、この制限を受けていた。
「受けていた」というのは、Ruby 1.9.2や、1.9系の新機能を1.8系へバックポートした1.8.7以降では、この制限はなくなりBIGNUMで扱える数字を扱うようになっているからだ。例えば、time.cの時刻の足し算を見ると、
static VALUE add(VALUE x, VALUE y) { if (FIXNUM_P(x) && FIXNUM_P(y)) { long l = FIX2LONG(x) + FIX2LONG(y); if (FIXABLE(l)) return LONG2FIX(l); return LONG2NUM(l); } if (RB_TYPE_P(x, T_BIGNUM)) return rb_big_plus(x, y); return rb_funcall(x, '+', 1, y); }
と、Fixnumに収まらない場合は何ごともなかったかのようにBignumで計算するようになっている。西暦1万年でも西暦100万年でも扱える。いや重要なのは、2038年を超えても大丈夫とか、1901年より前の時刻も指し示せるということなんだろうか(しかし、数百年単位で時間を扱うのは現実的に可能なんだろうか。物理的な経過時間と歴史上の時刻なんて対応させるのは極めて困難だろうし、暦だって非連続なところがいっぱいあるだろうし、現実的な意味としては「指し示せる」というより「表現できる」ということか)。
Rubyの標準クラスにはTimeに似た時間を扱うクラスとして、DateクラスとDateTimeクラスというのもがある。requre 'date' とすることで利用できる。これらは文字通り、日付、もしくは時刻も含む日付を扱う。DateTimeはDateクラスのサブクラス。
DateTimeクラス最強。なんでDateやTimeがあるんだ? というか、この混乱はなんだ?
まず1つ目の理由として、もともとTimeがtime_tの制限を受けていたことがある。Timeで表現できる日付には制限があったのだ。だからDateTimeが必要だった。でも、これは今では当てはまらない。
もう1つの理由として、処理速度が大きく違ったというのがある。Ruby1.8.6がリリースされた頃、2007年ごろのRubyのソースを見ると、lib/date.rbにあるDateTimeクラスはRubyによる実装だったことが分かる。だから、Timeクラスのほうが断然処理が速かった。ベンチを取ると以下の通り。
require 'benchmark' require 'date' Benchmark.bm do |bm| bm.report('DateTime:') do n1 = DateTime.now n2 = DateTime.now 1_000_000.times{ n1 < n2 } end bm.report('Time: ') do n1 = Time.now n2 = Time.now 1_000_000.times{ n1 < n2 } end end
$ ruby -v timebench.rb ruby 1.8.6 (2010-09-02 patchlevel 420) [i686-darwin11.4.2] user system total real DateTime: 4.710000 0.000000 4.710000 ( 4.809847) Time: 0.280000 0.000000 0.280000 ( 0.295962) $ rvm use 1.9.3 Using /Users/ken/.rvm/gems/ruby-1.9.3-p194 $ ruby -v ~/Desktop/timebench.rb ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-darwin11.3.0] user system total real DateTime: 0.240000 0.000000 0.240000 ( 0.252748) Time: 0.250000 0.000000 0.250000 ( 0.255503) $
1.8.6や1.8.7のDateTimeはRuby実装のため、Timeよりも遅い。でも、Ruby 1.9.3ではDateTimeもC実装になっていて、これもDateTimeクラスを使わない理由にならない。
Timeクラスと、Date/DateTimeの最大の違いは、秒を扱うか、日を扱うかという点だろうか。Timeのインスタンスに1を足すと1秒後になり、Date/DateTimeに1を足すと1日後になる。データ表現としてTimeは整数、Date/DateTimeではRationalという違いもある。
$ irb -rdate [1] pry(main)> t = Time.now => 2012-10-01 06:12:50 +0900 [2] pry(main)> t + 1 => 2012-10-01 06:12:51 +0900 [3] pry(main)> d = DateTime.now => #<DateTime: 2012-10-01T06:13:04+09:00 ((2456201j,76384s,174250000n),+32400s,2299161j)> [4] pry(main)> d + 1 => #<DateTime: 2012-10-02T06:13:04+09:00 ((2456202j,76384s,174250000n),+32400s,2299161j)> [5] pry(main)> d + Rational(1, 3) => #<DateTime: 2012-10-01T14:13:04+09:00 ((2456202j,18784s,174250000n),+32400s,2299161j)> [6] pry(main)> d + Rational(1, 24) => #<DateTime: 2012-10-01T07:13:04+09:00 ((2456201j,79984s,174250000n),+32400s,2299161j)> [7] pry(main)>
DateTimeのオブジェクトにRational(1, 3)、つまり3分の1日を加えると8時間後の時刻が得られる。日付より小さい単位を計算したいときには、Timeのほうが便利かもしれない。のだけど、この辺はActiveSupportによるRailsの拡張を勉強しないと、何をどう使うべきなのか、まだよく分からない。
このほかのTimeとDateTimeの違いは、
- require 'date' しないとDate/DateTimeは使えない
- require 'time' としてTimeクラスの拡張メソッドをロードしておかないと時刻文字列のパースは苦しそう
- 文字列に変換すると、TimeはC言語のctime相当、DateTimeはISO 8601
[7] pry(main)> t.to_s => "2012-10-01 06:12:50 +0900" [8] pry(main)> d.to_s => "2012-10-01T06:13:04+09:00" [9] pry(main)>
というのもある。
結局、TimeかDateTimeのどちらを使うべきかというのはよくわからない。Timeの欠点(130年分しか扱えない)もDateTimeの欠点(遅い)も解消されている今、ますます違いが良くわからない。これは、Javaのような委員会できっちり決めて実装が進む言語と異なり、Rubyという言語では「Timeじゃ足りねぇ、DateTimeを作るべ」、「Time拡張して32ビット制限はずしたよ」、「DateTime遅いからCにしたよ」と同時多発的にビミョウに目的の違ったライブラリの進化が起こったから、という理解でいいんだろうか。
参考:
アトキンの篩
素数を求めるアルゴリズムとして、エラトステネスの篩よりも計算量的には速いというアトキンの篩というのを実装してみようと思って検索したら、StakOverflowに Pythonの例があった。Wikipediaの記述のまんま。これをRubyに書き直してみた。
import math def AtkinSieve (limit): results = [2,3,5] sieve = [False]*(limit+1) factor = int(math.sqrt(limit))+1 for i in range(1,factor): for j in range(1, factor): n = 4*i**2+j**2 if (n <= limit) and (n % 12 == 1 or n % 12 == 5): sieve[n] = not sieve[n] n = 3*i**2+j**2 if (n <= limit) and (n % 12 == 7): sieve[n] = not sieve[n] if i>j: n = 3*i**2-j**2 if (n <= limit) and (n % 12 == 11): sieve[n] = not sieve[n] for index in range(5,factor): if sieve[index]: for jndex in range(index**2, limit, index**2): sieve[jndex] = False for index in range(7,limit): if sieve[index]: results.append(index) return results
def AtkinSieve(limit) primes = [2, 3, 5] sieve = [false] * (limit + 1) factor = Math.sqrt(limit).to_i + 1 sqr_arr = (1..factor).map { |i| i**2 } sqr_arr.each_with_index do |i2, i| sqr_arr.each_with_index do |j2, j| n = 4*i2 + j2 if (n <= limit) and (n % 12 == 1 or n % 12 == 5) sieve[n] = !sieve[n] end n = 3*i2 + j2 if (n <= limit) and (n % 12 == 7) sieve[n] = !sieve[n] end if i > j n = 3*i2 - j2 if (n <= limit) and (n % 12 == 11) sieve[n] = !sieve[n] end end end end (5..factor).each do |i| if sieve[i] (i**2..limit).step(i**2) do |j| sieve[j] = false end end end primes += (7..limit).find_all {|i| sieve[i]} end
ダイレクトな翻訳じゃなくて、ちょっと計算の重複を省いたりRubyっぽさプラスしたりした。
で、思ったんだけど、こういうコードだとPythonのほうが読みやすい。Pythonだと、Rubyのendのようにブロックの閉じが不要なので全体が短いのがいい。縦に短いので全体のループのネスト構造が分かりやすい。コードハイライトした状態だとなおさらだけど、ループが全部 for になるので構造がクッキリと良く分かる。同様の理由で、rangeとイチイチ書くのって面倒だしダサいと思えるけど、コードを見るときにはハイライトされるので、よく分かる。RubyのEnumerableだと、いろんなメソッドがあるし、レシーバとしてもArrayとかRangeとかEnumeratorとか、まあ色々ありえる。each_charとかならレシーバは文字列だ。で、それらがリテラルだったりすると、そこがループになっていることがforよりも分かりづらい、ような気がする。今回はじめて知ったけど、Pythonでは range(start, end, step)という風に書ける。これはRubyなら、Range.new(start, end).step(step) と書く。ふつうRubyならリテラルに書いてしまうので、(0..100).step(2) となる。初めてこのRange#stepの記法を見た時には、すごいなRubyと思ったけど、Pythonのように馬鹿正直に range(s, e, s) とやるのもいいじゃん、と思ってしまった。Eloquentじゃないほうがいいこともあるよな、という。
配列へのappendも、Rubyだと、いろんなやり方があったりする。全体にPythonってそういう文脈依存や好みによるブレがない感じ。それはアルゴリズムの表現なんかには良いことに思える。とか思ってDjangoのソースを見ると、やたらとアンダースコアやらselfやらが一杯でてきてウゲっーとなったりもするんだけど。
RubyのDATAという特殊なグローバル定数
Rubyにはスクリプト言語らしい機能が色々ある。::DATAを使うと、スクリプト本体の後ろにテキストデータを貼り付けておける。
def show_data puts "DATA: #{DATA.class}" DATA.each_line do |l| puts "line: #{l}" end end show_data __END__ 1 2 3
$ ruby data.rb DATA: File line: 1 line: 2 line: 3 $
DATAというのは、Fileオブジェクトで、ruby.c の load_file_internal という関数で初期化されている。
static VALUE load_file_internal(VALUE arg) { : : : : : : if (script && tree && rb_parser_end_seen_p(parser)) { /* * DATA is a File that contains the data section of the executed file. * To create a data section use <tt>__END__</tt>: * * $ cat t.rb * puts DATA.gets * __END__ * hello world! * * $ ruby t.rb * hello world! */ rb_define_global_const("DATA", f); } else if (f != rb_stdin) { rb_io_close(f); } return (VALUE)tree; }
もしかして、これって sinatra のテンプレートでも使われてるのかな? と思って lib/sinatra/base.rb を見てみたら、
# Load embeded templates from the file; uses the caller's __FILE__ # when no file is specified. def inline_templates=(file=nil) file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file begin io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file) app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2) rescue Errno::ENOENT app, data = nil end
とあって、__END__を split してた。なんでだろう。
printfで文字列のところにintを渡す
#include <stdio.h> int main(int argc, char *argv[]) { int i = 100; printf("%s:\n", i); return 0; }
とするとSEGVで落ちるけど、このとき起こっていることを理解してなかったと、久しぶりに使った valgrind のメッセージで気付いた。
$ valgrind ./seg ==25237== Memcheck, a memory error detector ==25237== Copyright (C) 2002-2011, and GNU GPL'd, by Julian Seward et al. ==25237== Using Valgrind-3.7.0 and LibVEX; rerun with -h for copyright info ==25237== Command: ./seg ==25237== ==25237== Invalid read of size 1 ==25237== at 0xC884: strlen (mc_replace_strmem.c:398) ==25237== by 0x1728C2: __vfprintf (in /usr/lib/system/libsystem_c.dylib) ==25237== by 0x17118D: vfprintf_l (in /usr/lib/system/libsystem_c.dylib) ==25237== by 0x17A2CF: printf (in /usr/lib/system/libsystem_c.dylib) ==25237== by 0x100000F12: main (seg.c:6) ==25237== Address 0x64 is not stack'd, malloc'd or (recently) free'd ==25237== ==25237== ==25237== Process terminating with default action of signal 11 (SIGSEGV) ==25237== Access not within mapped region at address 0x64 ==25237== at 0xC884: strlen (mc_replace_strmem.c:398) ==25237== by 0x1728C2: __vfprintf (in /usr/lib/system/libsystem_c.dylib) ==25237== by 0x17118D: vfprintf_l (in /usr/lib/system/libsystem_c.dylib) ==25237== by 0x17A2CF: printf (in /usr/lib/system/libsystem_c.dylib) ==25237== by 0x100000F12: main (seg.c:6) ==25237== If you believe this happened as a result of a stack ==25237== overflow in your program's main thread (unlikely but ==25237== possible), you can try to increase the size of the ==25237== main thread stack using the --main-stacksize= flag. ==25237== The main thread stack size used in this run was 8388608. ==25237== ==25237== HEAP SUMMARY: ==25237== in use at exit: 6,316 bytes in 33 blocks ==25237== total heap usage: 33 allocs, 0 frees, 6,316 bytes allocated ==25237== ==25237== LEAK SUMMARY: ==25237== definitely lost: 0 bytes in 0 blocks ==25237== indirectly lost: 0 bytes in 0 blocks ==25237== possibly lost: 0 bytes in 0 blocks ==25237== still reachable: 6,316 bytes in 33 blocks ==25237== suppressed: 0 bytes in 0 blocks ==25237== Rerun with --leak-check=full to see details of leaked memory ==25237== ==25237== For counts of detected and suppressed errors, rerun with: -v ==25237== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 1 from 1) zsh: segmentation fault valgrind ./seg
「Access not within mapped region at address 0x64」というのは、つまりメモリアドレスの100のところに文字列を読みに行ったってことか。コンパイラは型の不一致について警告を出すけど、実行自体は愚直というか何も複雑なことをしないのね。生っぽい。うーん、もう少しCを勉強したい。
素数列を enumerable ぽく作る
下から数えて1万番目の素数を求めよ、という何を求められているのか実はよく分からないコーディング課題に対してRubyで以下のように書いてみた。PrimeSeq[n]で、n番目の素数が得られる。
class PrimeSeq include Enumerable def initialize @primes = [2, 3] @i = 3 end def [](idx) if @primes.size >= idx @primes[idx - 1] else take(idx - @primes.size).last end end private def each loop do loop do @i += 2 if @primes.none? {|p| (@i % p == 0)} @primes << @i break end end yield @i end end end if __FILE__ == $0 prime = PrimeSeq.new (1..1000).each {|i| p prime[i]} end
何となく、enumerable にすることで列っぽいものが扱えるだろうと思って、each を実装したけど、結局内部的に take のために使っているだけになった。ただ、列生成を再開すべきポインタを自分で意識的に保持しておかなくても「続きのm個を持って来い」と言えるのは良い、ように思うのだけど、いきなり self に対して take しているとか、しかもその take が暗黙的に private の each を呼んでるのだとか、そういう「Rubyだからね!」というやり方がいいのかどうか、よく分からないな。
そういえば、 MiniTestというのを使ってみた。しかし、RSpecとそんなに違うのかな、ていうか、また別のDSLなのか、ていうか、オレがやりたいのはassertなんだけど! という感じに萎え萎え気分。RSpecより速いらしいし、Ruby1.9標準というのがいいのかな。
require './primeseq.rb' require 'minitest/autorun' describe PrimeSeq do before do @p = PrimeSeq.new end describe "from 1st to 5th" do it "should be" do @p[1].must_be :==, 2 @p[2].must_be :==, 3 @p[3].must_be :==, 5 @p[4].must_be :==, 7 @p[5].must_be :==, 11 end end describe "1000th, 2000th" do it "should be" do @p[1_000].must_be :==, 7919 @p[2_000].must_be :==, 17389 @p[1_000].must_be :==, 7919 end end describe "fetch from the cache" do it "should be" do @p[2_000].must_be :==, 17389 @p[2_000].must_be :==, 17389 @p[1_000].must_be :==, 7919 end end end
あみだくじジェネレータを状態をなるべく扱わない方向で書く
みなとRuby会議01というイベントで、ペアプロ大会みたいなのがあったらしい。
あみだくじ。
いろんな人の書いた結果が残ってるらしいし、勉強になるかなと思って、ぼくもやってみた。
どう書くのかなと思って、まず2、3行ほど何かを書いてみた。すぐに「同じ高さで隣り合う場所に横線があっちゃダメ」という当たり前のようで当たり前じゃない仕様に気付いた。あみだくじってそうだよな。手書きだと分かりづらいけど。
で、ふつうに手続き的な発想でいうと、2重ループにして、水平方向に結果を作っていくときに直前の状態を考慮して横線を引くかどうかを決めるということになるんだろうけど、もしかして、関数の適用だけでやったほうが書きやすくて読みやすくなるんじゃないのと思った。
1. 半分の確率で0か1を返す 2. 隣り合う1の一方を0に
という2つの関数があれば、0と1の並びに関数を適用していくだけでできそう。1が奇数回繰り返している場合は、2の操作を2度適用すればオッケー。で、書いてみたのが以下。
class Amida def initialize(size, height = 15) @height, @size = height, size @rows = [] height.times { @rows << Row.new(size - 1) } prepare_header_footer end def prepare_header_footer @header = ("A".."Z").take(@size).join(" ") arr = Array.new(@size, " ") arr[0] = "o " @footer = arr.shuffle.join end def show puts @header @rows.each do |r| puts "|" + r.with_bars + "|" end puts @footer end class Row < Array def initialize(col) super(col) { rand(2) } remove_continuous_ones end def remove_continuous_ones double_one_to_single_one double_one_to_single_one end def double_one_to_single_one self[0..-1] = join.gsub(/11/, '01').split(//).map(&:to_i) end def with_bars map {|i| i == 0 ? " " : "--" }.join('|') end end end if __FILE__ == $0 col = (ARGV.shift || 8).to_i a = Amida.new(col) a.show end
出力は以下のような感じ。
状態を持つ長いループがない分、スッキリしているように思う。
結局のところ、
join.gsub(/11/, '01').split(//).map(&:to_i)
という正規表現による力技の部分が状態を扱っているわけで、逐次的にやるのと本質的には変わらない。でも、DSLの力で読みやすいように思う。
書き終わってから、他の人の書いたあみだくじの実装を見て、バリエーションの多さにちょっと驚いた。いちばん違うなと思ったのは、どこまでRubyっぽいか、Railsっぽいかというところだったりする。moduleやclassを使わず、しかも頭から上に処理して結果が出るというようにミニマルなスクリプト的な書き方から、RSpecまで書きながら妙にGemっぽく完成した形で書く人までいる。
xhrの抽象化を眺めていてプログラミングの勉強って何よとか不毛な悩みが浮かんできた
Ruby on Railsで英辞郎CD-ROMをインポートしてオレオレ単語帳が作れるアプリを作ってる。復習間隔を徐々に伸ばすことで記憶定着率を高めるというアプローチのもの。これがいい感じになってきた。でも単語帳アプリって、やっぱりローカルで動かすだけだとイマイチ。気付くと復習すべき単語が30個も40個も溜まってしまうので、やっぱりモバイルで動かしたい。サーバに置けば、空き時間にスマフォでチェックできてよさげ。結局、語彙力増強は一定頻度で語彙と接触するのがキモで、これを効率良く計画してくれるアプリは助けになる、ような気がする。
サーバにデプロイして動かしてみたら、ビューがモバイルに最適化されていないこと以外は、いい感じ。だと思ったら、iPadでは動くのにAndroidではAjaxで挙動がヘン。ボタンを押しても起こるべきことが起こらない。ログを見るとXHRが飛んでいない。
スカっと解決したいぜ、頼むぜjQueryとか思ったけど、実際にはUnobtrusiveなRailsのJavaScriptライブラリの抽象化レイヤもあるし、なんか面倒だなーという印象。
ブラウザがリクエストをキャッシュしてる? イベントが発火してない? そんなことあるんか? とか思って、よく考えると、そもそもXHRのことが全く分かってないのがいけない気がして、W3CとMozillaあたりで、これまでのブラウザの実装の歴史や解説を少し読んだ。で、想像以上に話が簡単じゃないので、jQueryも見てみた。
jquery/src/ajax/xhr.jsには、以下のようなコードがあって、基本的にjQueryはxhr周りだけでも、この数倍は汚れ仕事をやってくれていることを知った。
// Create the request object // (This is still attached to ajaxSettings for backward compatibility) jQuery.ajaxSettings.xhr = window.ActiveXObject ? /* Microsoft failed to properly * implement the XMLHttpRequest in IE7 (can't request local files), * so we use the ActiveXObject when it is available * Additionally XMLHttpRequest can be disabled in IE7/IE8 so * we need a fallback. */ function() { return !this.isLocal && createStandardXHR() || createActiveXHR(); } : // For all other browsers, use the standard XMLHttpRequest object createStandardXHR; // Determine support properties (function( xhr ) { jQuery.extend( jQuery.support, { ajax: !!xhr, cors: !!xhr && ( "withCredentials" in xhr ) }); })( jQuery.ajaxSettings.xhr() );
AndroidでAjaxが動きません! とかいってググるだけしかできないのは、抽象化の漏れというヤツかもなぁと思ったりしたけど、なんか、こういうのってつらい。
現実は常に汚いということかもしれないけど、本質的じゃない。プログラミングの勉強がしたいというとき、「現実は汚いものなので、そういうものをどう抽象化したりするのかを学ぶのも勉強のうち」と考えるべきなんだろうか。2012年は、もっと普遍的で本質的なことをやりたいと思ったりしていたんだけどなぁ。完全なジョークとも言い切れない処理系を作ってみるとか、アルゴリズムの本を「つまみ食いする」のではなく、これだという1冊を選んで頭から最後までキチンと読むとか、そういうようなこと(とか言いつつ、2週間でできる!スクリプト言語の作り方とかいう本を読んでいたりする)。