やりたいこととしては、
2012/03/04 AA:AA:AA
BB/BB/BB 12:34:56
の二つを組み合わせて 2012/03/04 12:34:56
を作るにはどうしたらいいか考えた話です。
あんな書き方こんな書き方の練習的なメモになります。 Ruby 嫌いな人は読まないほうがいいかもです。
Ruby 2.4.2
/ Rails 5.2.0pre1
で確認しています。
最終的に採用した方法
datetime_a.midnight.since(datetime_b.seconds_since_midnight)
midnight
は beginning_of_day
でも同じです。1
この記事 https://qiita.com/hirokishirai/items/93ca9b566dddc815063c を見て time of day を使うのも良さそうだと思いましたが、 since
に入れることを考えると意味がなさそうです。
もっと簡潔な方法はあるのでしょうか。
日付と時刻を別々に取得して Time.new
を使う方針で作ってみる
Time.new
を使おうと思いました。
Time.new(*[:year, :month, :day].map(&datetime_a.method(:send)) + [:hour, :min, :sec].map(&datetime_b.method(:send)))
見やすくすると
Time.new(
*
[:year, :month, :day].map(&datetime_a.method(:send))
+
[:hour, :min, :sec].map(&datetime_b.method(:send))
)
こういうことです。個人的には気に入っています。
追記: %i
を使えば少し短くなります。あと、splat 展開 *
を二回使えば +
の演算は不要になります (コメント より)
もう一段 map したけど
上記だと、 .method(:send)
あたりが重複しているのでまとめてみようと思いました。
Time.new(*{[:year, :month, :day] => datetime_a, [:hour, :min, :sec] => datetime_b}.flat_map { |keys, time| keys.map(&time.method(:send)) })
却って長くなってしまいました。ハッシュの記号と map
の仮引数(?)部分が結構長くつくようです 2
時間をの構成要素を to_a
で取り出す
> datetime_b.to_a
=> [56, 34, 12, 1, 1, 2018, 3, 101, false, "UTC"]
時刻に対する to_a
は、配列で、ただし逆順で返ってきます。ここから一部を取り出したいのですが、 Range
を使うと昇順しか指定できなさそうなので、 values_at
を使います。
Time.new(*datetime_a.to_a.values_at(5, 4, 3) + datetime_b.to_a.values_at(2, 1, 0))
これも、さっきと同様に splat展開 ( *
) を二回使えば +
は不要になります。
しかし、 values_at
は長いのが玉に傷です。代わりに、一旦 Range
で拾ってから全体を reverse
することもできそうです。この場合は使うレシーバーの順も変わります (例だと b
, a
の順) 。
Time.new(*(datetime_b.to_a[0..2] + datetime_a.to_a[3..5]).reverse)
これは割と短めです。
seconds_since_midnight
を再発明する悲しい作業記録
当初、 seconds_since_midnight
を知るまでは、これでいこうかと思っていました。
datetime_a.midnight.since(datetime_b - datetime_b.midnight)
datetime_b
が二回出てくるなあと思っていました。
instance_eval
使っうちゃう
変数が複数回出てくるときは instance_eval
を使うと一回で済むことがあります。
datetime_a.midnight.since(datetime_b.instance_eval { self - midnight })
残念。記述量が増えました(そういう問題?
愚直に計算
seconds_since_midnight
を愚直に計算してみました。
datetime_a.midnight.since([:hour, :min, :sec].map(&datetime_b.method(:send)).zip([3600, 60, 1]).map { |value_and_unit| value_and_unit.inject(&:*)}.inject(:+))
見やすく (?) すると
datetime_a.midnight
.since(
[:hour, :min, :sec].map(&datetime_b.method(:send))
.zip([3600, 60, 1])
.map { |value_and_unit| value_and_unit.inject(&:*) }
.inject(:+)
)
友達をなくしそうなコードです。
どういうことかというと、
> [:hour, :min, :sec].map(&datetime_b.method(:send))
=> [12, 34, 56]
こうやって取れた [時・分・秒]
の、それぞれを秒単位に揃えるため [3600, 60, 1]
とそれぞれの成分を掛け合わせます。
> [12, 34, 56].zip([3600, 60, 1]).map { |value_and_unit| value_and_unit.inject(&:*) }
=> [43200, 2040, 56]
んで、得られたこれの和をとる (最後の.inject(:+)
) と、全体としては [時・分・秒]
と [3600, 60, 1]
との内積が取れるということです。
ところで掛け算を .inject(&:*)
で表現したことについて、掛け算の単位元は 1 なわけで、 .inject(1, &:*)
のように初期値として 1 を渡さないと 0 への掛け算になってしまうと思ったのですが、そうではないようでした。
=> 追記: コメント より、 inject
の引数で初期値を渡さない場合はそもそも演算が行われないそうです
map
は配列を展開できる
Array#sum
とか普通に実装されているし、二者の掛け算は inject
よりも配列にバラして受け取り、計算式を書いたほうが短いことに気づきました。
datetime_a.midnight
.since(
[:hour, :min, :sec].map(&datetime_b.method(:send))
.zip([3600, 60, 1])
.map { |value, unit| value * unit }.sum
)
少し短くなりました。そして速度の面から見ても sum
のほうが良いようです https://qiita.com/Nabetani/items/c46ab71353ad9b0c621d#comment-a2c765323abf500968e4
Vector
内積なら Vector
を使ったほうがいいでしょうか。
require 'matrix'
datetime_a.midnight
.since(
Vector[:hour, :min, :sec].map(&datetime_b.method(:send))
.dot(Vector[3600, 60, 1])
)
長さはどっこいどっこいですが、 Vector#dot
って書いてありゃ内積っていうことはわかるので、いいかもしれません。
ところで、 Vector
って map
しても Vector
なんですね。
内積だとしても式を書いちゃったほうがよい
instance_eval
使って計算すれば普通に短くなりました。数も固定長だし変数も要らないので、内積の方針でいくのであればこうしたほうがよさそうです。
datetime_a.midnight.since(datetime_b.instance_eval { 3600 * hour + 60 * min + sec })