LoginSignup
3
0

More than 5 years have passed since last update.

Rails で、二つの DateTime の日付と時刻とを組み合わせようと思った無駄な時間の記録

Last updated at Posted at 2018-04-11

やりたいこととしては、

  • 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)

midnightbeginning_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 })

  1. そもそもそれに気づいていなかったので、 seconds_since_midnight に気づくのも遅れました。 

  2. 試すと分かるのですが、変数記号を短くしたりスペースを削っても、上回ることはなさそうです。 

3
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0