-> English version



るびくる&RBのRubyプログラミング大作戦!
Ruby FacetsでおしゃれなRubyistになろう!(パート3:facets/time編)

このエントリーをはてなブックマークに追加

るびくる:
Rubyの自称マスコットキャラクター。
好きなサラダの食べ方は胡麻ドレッシング。
「どんなサラダも胡麻ドレがあればいつでもおいしい!」というのが本人の弁。

RB(あーるびー):
解説役兼るびくるの指導役。
好きなサラダの食べ方はオリーブオイルドレッシング、もしくはドレッシングなし。
「味も素っ気もないところが逆にいいよね」というのが本人の弁。



1. 今回のサンプルコード

それじゃ今回は、facets/timeを紹介するよ。
これは読み込むことで、Ruby標準のTimeクラスに
stamp、shift、agoなど、いくつかのメソッドを追加するライブラリなんだ。

file: time-example.rb

# encoding: utf-8
require 'facets/time'

time = Time.new(2013, 2, 5, 17, 42, 50)
p time  # => 2013-02-05 17:42:50 +0900

#-------------------------------------------------------------------------------
# Time.elapse: ブロックの実行時間を計測する
p Time.elapse{ sleep(1) }  # => 1.0000581741333008


#-------------------------------------------------------------------------------
# stamp:       対象の日時を指定したフォーマットで文字列化
# Time.stamp:  現在日時を指定したフォーマットで文字列化

p time.stamp(:db)         # => "2013-02-05 17:42:50"
p time.stamp(:utc)        # => "2013-02-05 17:42:50"
p time.stamp(:utcT)       # => "2013-02-05T17:42:50"
p time.stamp(:number)     # => "20130205174250"
p time.stamp(:time)       # => "17:42"

p time.stamp(:ruby18)     # => "Tue Feb 05 17:42:50 +0900 2013"

p Time.stamp(:db)         # Time.now.stamp(:db)と同じ

# stampに文字列を渡すと、strftimeとほぼ同じ動きになる
# ただしstrftimeと違い、前後の空白は削除される
p time.stamp('%Y/%m/%d')   # => "2013/02/05"
p time.stamp(' %Y/%m/%d ') # => "2013/02/05"

# 引数に何も渡さなかった場合は、to_sと同じ動きになる
p time.stamp          # => "2013-02-05 17:42:50 +0900"
p time.to_s           # => "2013-02-05 17:42:50 +0900"


#-------------------------------------------------------------------------------
# shift, hence, in: 時間を指定した分だけ先に進める
#                   単位には以下のものが使用可能(単数形もOK)
#                     :years, :months, :weeks, :days, :hours,
#                     :minutes(:mins), :seconds(:secs)
#
# ago, less       : 時間を指定した分だけ前に戻す(shift, hence, inの逆)

p time                            # => 2013-02-05 17:42:50 +0900
p time.shift(5, :days, 1, :hour)  # => 2013-02-10 18:42:50 +0900
p time.shift(-2, :weeks)          # => 2013-01-22 17:42:50 +0900
p time.ago(2, :weeks)             # => 2013-01-22 17:42:50 +0900

# 別記法: Hashで引数を渡すこともできる
p time.shift(:days => 5, :hour => 1)   # => 2013-02-10 18:42:50 +0900
p time.shift(days: 5, hour: 1)         # => 2013-02-10 18:42:50 +0900


#-------------------------------------------------------------------------------
# future?   : self > otherと同じ。「指定した日付よりも未来であるかどうか」を判定する
# past?     : self < otherと同じ。「指定した日付よりも過去であるかどうか」を判定する

p time                      # => 2013-02-05 17:42:50 +0900
p time + 30                 # => 2013-02-05 17:43:20 +0900
p time.future?(time + 30)   # => false
p time.past?(time + 30)     # => true


#-------------------------------------------------------------------------------
# trunc   : 時刻を指定した単位(秒)で丸める(切り捨てる)
#           60 なら1分単位、60 * 60 なら1時間単位
# round_to: 時刻を指定した単位(秒)でみて、近い方の時刻に寄せる(四捨五入のような処理を行う)

p time                              # => 2013-02-05 17:42:50 +0900
p time.trunc(60)                    # => 2013-02-05 17:42:00 +0900
p time.trunc(60 * 60)               # => 2013-02-05 17:00:00 +0900
p time.round_to(60)                 # => 2013-02-05 17:43:00 +0900
p time.round_to(60 * 60)            # => 2013-02-05 18:00:00 +0900

# 注意点: 日単位でtruncやround_toを実行すると、期待したような結果にならない
p time.trunc(60 * 60 * 24)          # => 2013-02-05 09:00:00 +0900

長っ! ざっと8種類くらいのメソッドがあるよ!?
しかも機能も使い方もまちまちだし、これ本当にわたしのUSBメモリよりも容量が小さな脳ミソで覚えきれるの!?

それ、少なくとも2GBくらいの容量はあるってことだよね? それくらいあれば余裕だと思うけど……。

そうだね、確かにすぐに全部を把握するのは無理かもしれない。
ただ、前にも話したとおり、全部を一気に覚えようとする必要はないよ
基本的には自分が使いたくなるようなメソッドから、1つずつ使い始めてみればいいんじゃないかな。

それじゃ、1つ1つ見ていこうか!


2. Time.elapseメソッドで処理時間を計測しよう!

まずはシンプルだけど地味に使う機会が多い、Time.elapseメソッドから。

#-------------------------------------------------------------------------------
# Time.elapse: ブロックの実行時間を計測する
p Time.elapse{ sleep(1) }  # => 1.0000581741333008

ブロックを渡すことで、そのブロック内の処理の実行時間を計測してくれるメソッドだよ。

benchmarkライブラリの、Benchmark.realtimeと同じようなメソッドなんだね。
ちょっとしたベンチマーク代わりに、「この処理どれくらい時間かかってるのかなー」ってのを調べたいときに
使えそうだね!

ところで、elapseってどういう意味の単語なの?

英語で「(時間が)経過する」っていう意味の言葉だね。elapsed timeで経過時間って訳されたりするよ。


3. stampメソッドで日時を簡単に文字列化しよう!

次は、日時をいろいろなフォーマットで文字列化してくれるstampメソッド。

#-------------------------------------------------------------------------------
# stamp:       対象の日時を指定したフォーマットで文字列化
# Time.stamp:  現在日時を指定したフォーマットで文字列化

p time.stamp(:db)         # => "2013-02-05 17:42:50"
p time.stamp(:utc)        # => "2013-02-05 17:42:50"
p time.stamp(:utcT)       # => "2013-02-05T17:42:50"
p time.stamp(:number)     # => "20130205174250"
p time.stamp(:time)       # => "17:42"

p time.stamp(:ruby18)     # => "Tue Feb 05 17:42:50 +0900 2013"

p Time.stamp(:db)         # Time.now.stamp(:db)と同じ

# stampに文字列を渡すと、strftimeとほぼ同じ動きになる
# ただしstrftimeと違い、前後の空白は削除される
p time.stamp('%Y/%m/%d')   # => "2013/02/05"
p time.stamp(' %Y/%m/%d ') # => "2013/02/05"

# 引数に何も渡さなかった場合は、to_sと同じ動きになる
p time.stamp          # => "2013-02-05 17:42:50 +0900"
p time.to_s           # => "2013-02-05 17:42:50 +0900"

えーと……これって、要するに決められたSymbolを渡すことで
いい感じに日時をフォーマットしてくれるっていうメソッド?

その通り。time.stamp(:utc)って書くことで、timeをUTC(世界協定時)形式の文字列にしてくれる……というふうに
渡した引数に応じた形式で、文字列への変換をやってくれるメソッドなんだよ。
特に使う機会が多そうなのは、time.stamp(:db)あたりかな。

といっても、実際にはコードサンプルの末尾にあるような感じで
Symbolを渡して呼び出すことよりは
文字列(String)を渡したり、引数なしで呼び出したりすることの方が多いかもしれないね。

# stampに文字列を渡すと、strftimeとほぼ同じ動きになる
# ただしstrftimeと違い、前後の空白は削除される
p time.stamp('%Y/%m/%d')   # => "2013/02/05"
p time.stamp(' %Y/%m/%d ') # => "2013/02/05"

# 引数に何も渡さなかった場合は、to_sと同じ動きになる
p time.stamp          # => "2013-02-05 17:42:50 +0900"
p time.to_s           # => "2013-02-05 17:42:50 +0900"

え、なんで?

文字列を渡せばstrftimeメソッドの動き引数なしで呼び出せばto_sメソッドの動き、ってところが
けっこう取り回しがよくて
「文字列化したいときはとりあえずstampメソッド」って感じで、気軽に使えるから。

そっか、普段なら目的に応じてstrftimeto_sを使い分けたりする必要があるけれど
このstampメソッドなら、その2つを使い分ける必要がないんだね!

……ところでこのメソッドって、Symbolではどんな引数を渡すことができるの?

それもきっと聞かれるだろうと思って、ちゃんと引数とフォーマットの対応表を用意してある。

stampメソッドの引数・フォーマット対応表(Facets 2.9.3時点)
引数に渡す値フォーマット出力例
:utc,:db,:databaseUTC(世界協定時)形式2013-02-01 17:01:02
:utcT UTC形式。日付と時刻の間をTで区切る2013-02-01T17:01:02
:day1st,:dmYHMUTC形式。日-月-年の順で出力01-02-2013 17:01
:time 時間のみ17:01
:number 記号を含まない、数字だけの形式20130201170102
:long 英語圏での日時表記February 01, 2013 17:01
:short 英語圏での短縮日時表記01 Feb 17:01
:rfc822 RFC822やRFC2822で規定された、メール向けの日時表記
(標準添付のtimeライブラリで追加される
Time#rfc282, Time#rfc2822と同じ)
Fri, 01 Feb 2013 17:01:02 +0900
:ruby18 Ruby 1.8以前でto_sしたときと同じFri Feb 01 17:01:02 +0900 2013
'フォーマット文字列'strftimeメソッドを呼んだ場合と同じ
ただし前後の空白は削除される(stripされる)
(フォーマット文字列によって違う)
省略、もしくはnilto_sメソッドと同じ2013-02-01 17:01:02 +0900

さっすが先生! この表を見れば、いつでもstampの使い方はばっちりだね!

ただ、指定可能なフォーマットの種類は、Facetsのバージョンによって増えたりしてるみたいだし
この先のバージョンアップで、使える引数が変わる可能性もあるから
あくまでこの対応表は参考程度にとどめておいてね。


4. shiftメソッドやagoメソッドで日時をずらそう!

さて、ここからは数も多いし、サクサク進めていこう。
次は日時をずらすことができるshiftメソッドと、agoメソッド。
この2つのメソッドには、それぞれいくつかの別名があるけど、使い方は同じだよ。

#-------------------------------------------------------------------------------
# shift, hence, in: 時間を指定した分だけ先に進める
#                   単位には以下のものが使用可能(単数形もOK)
#                     :years, :months, :weeks, :days, :hours,
#                     :minutes(:mins), :seconds(:secs)
#
# ago, less       : 時間を指定した分だけ前に戻す(shift, hence, inの逆)

p time                            # => 2013-02-05 17:42:50 +0900
p time.shift(5, :days, 1, :hour)  # => 2013-02-10 18:42:50 +0900
p time.shift(-2, :weeks)          # => 2013-01-22 17:42:50 +0900
p time.ago(2, :weeks)             # => 2013-01-22 17:42:50 +0900

簡単に言うと、ある日時を「1日分未来に進めたい」ってときや「30分だけ過去に戻したい」って時なんかに
使えるメソッドだよ。

タイムスリップしたいときに使えるメソッドだってこと?

あー、うん。SFぽく言うならそういうことだね。

で、使い方はだいたい見た目通りだね。

time.shift(3, :days) → 日時を3日分未来にずらす。
time.shift(1, :hour, 30, :minutes) → 日時を1時間30分未来にずらす。

でもこれって、なんというか……引数の順番にちょっと違和感があるんだけど、こういうものなの?

たぶんそれは、英語としてそのまま読めるように、英語の語順に合わせた順番になってるからだね。
上の例なら「3 days」「1 hour 30 minutes」って読み下せるよね?
その反面、日本語圏に住んでる僕たちから読むと、ちょっと違和感が出てきちゃうんだと思うよ。

この順序になじめない場合は、サンプルコードにもあるように、Hashを渡してもいい。

# 別記法: Hashで引数を渡すこともできる
p time.shift(:days => 5, :hour => 1)   # => 2013-02-10 18:42:50 +0900
p time.shift(days: 5, hour: 1)         # => 2013-02-10 18:42:50 +0900

なるほど、こっちの書き方なら、わたしみたいな生粋の日本生まれ・日本育ちにも違和感が少ないね!

……どうしたのRB、その微妙な表情?

いや、なんでもないよ。
(るびくるって、自分が日本人であることを強くアピールするわりに
見た目あんまり日本人っぽくないよなあ……)


5. future?メソッドやpast?メソッドで時間の比較をしよう!

次は日時の比較を行う、future?と、past?

#-------------------------------------------------------------------------------
# future?   : self > otherと同じ。「指定した日付よりも未来であるかどうか」を判定する
# past?     : self < otherと同じ。「指定した日付よりも過去であるかどうか」を判定する

p time                      # => 2013-02-05 17:42:50 +0900
p time + 30                 # => 2013-02-05 17:43:20 +0900
p time.future?(time + 30)   # => false
p time.past?(time + 30)     # => true

これは使い方も見たまんまだし、特に付け加えることはないね。
あくまで time > other の代わりに time.future?(other) って書けるよー、っていうメソッドだから。

これもどっちかって言うと、英語圏の人向けのメソッドなのかな?


6. round_toメソッドやtruncメソッドで時間を丸めよう!

最後は、日時の丸めを行うround_toと、trunc

#-------------------------------------------------------------------------------
# trunc   : 時刻を指定した単位(秒)で丸める(切り捨てる)
#           60 なら1分単位、60 * 60 なら1時間単位
# round_to: 時刻を指定した単位(秒)でみて、近い方の時刻に寄せる(四捨五入のような処理を行う)

p time                              # => 2013-02-05 17:42:50 +0900
p time.trunc(60)                    # => 2013-02-05 17:42:00 +0900
p time.trunc(60 * 60)               # => 2013-02-05 17:00:00 +0900
p time.round_to(60)                 # => 2013-02-05 17:43:00 +0900
p time.round_to(60 * 60)            # => 2013-02-05 18:00:00 +0900

# 注意点: 日単位でtruncやround_toを実行すると、期待したような結果にならない
p time.trunc(60 * 60 * 24)          # => 2013-02-05 09:00:00 +0900

日時の丸め……? ってどういうこと?

えーっと、たとえば数値(Numeric)にはroundfloorってメソッドがあるよね?
小数なんかに対して四捨五入したり、切り捨てしたりするやつ。

# 四捨五入
1.5.round # => 2

# 切り捨て
1.5.floor # => 1

今回紹介するround_totruncも、考え方としてはそれに似てる。

たとえば、Time.now.round_to(60)ってすれば、現在時刻を分単位で近い方に丸めてくれるんだ。
現在時刻が20時5分29秒なら、分単位で近い方の時刻→切り捨て→20時5分00秒になる。
現在時刻が20時5分31秒なら、分単位で近い方の時刻→切り上げ→20時6分00秒になる。
……っていう感じにね。

そっか、時間の四捨五入や切り捨てができる、ってことなんだね!
round_toが四捨五入で、truncが切り捨て、だよね。

そういうこと。

ただ、この2つのメソッドには、1つ注意点がある。
欠陥……ではないんだけど、期待した動きにならない箇所があるんだ。
それは日単位での丸めをしようとした場合

# 注意点: 日単位でtruncやround_toを実行すると、期待したような結果にならない
p time.trunc(60 * 60 * 24)          # => 2013-02-05 09:00:00 +0900

あれ!? この結果って「2013-02-05 09:00:00」(9時)になるの?
日単位で丸めるから「2013-02-05 00:00:00」(0時)じゃなくて!?

うん。ちょっと不思議な感じがするよね?

なんで9時になってるのかって言うとね、round_totruncが、常に日時を世界協定時(UTC)で考えて丸めようとするからなんだ。
要するに、時差やタイムゾーンのところは考慮してくれないってこと。

あー、そういうことかぁ……。
私たちが住んでるのは、世界協定時からの時差が+9時間の日本だから
世界協定時で丸めると2月5日の0時。でも、日本時間では2月5日の9時になる。っていうことなんだね。

そう。だから残念だけど、これらのメソッドでは
日単位の丸めは期待したような動きにならないと認識しておいた方がいいだろうね。


それじゃ、パート3のfacets/time編はここで終わり。
次回のパート4では、Rails()でも大人気の、blank?メソッドやpresent?メソッドを使えるようにしてくれる
facets/kernel/blankを紹介するよ!

えー、でもblank?メソッドやpresent?メソッドの使い方って
ほかにもTECHSCOREの人とか、いろいろな人がRailsの解説記事を通して説明してくれてるよね?
わたしたちが改めて説明する意味、あんまりなくない?

るびくるストップ! それ言い始めるとパート2のfacets/kernel/try解説
ささたつさんが解説してくれてるから要らないってことになっちゃうから!

続きます。


注意事項
  • 本記事の内容は、Facets 2.9.3 時点での動作を元にしています。
  • 本記事内で使用しているアイコン画像は、桜去ほとりさんが制作されたものです。クリエイティブ・コモンズ 表示-非営利 3.0ライセンスのもとで、再配布や変更などを行っていただくことができます。
  • 本記事内の文章やソースコードは、CC0ライセンスのもとで、ご自由に利用していただくことができます。
  • ツンツク・モモコのお買い物大作戦と形式が類似していますが、本記事側が一方的に参考にさせていただいただけであり、とくに関連性はありません。

一言メッセージフォーム

るびくるへの質問や、やってほしい企画のリクエスト、Webサイト管理スタッフへの感想・意見・質問など、なんでもお気軽にどうぞ。