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

やりたいこととしては、

* `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` で確認しています。


# 最終的に採用した方法
```ruby
datetime_a.midnight.since(datetime_b.seconds_since_midnight)
```

`midnight` は `beginning_of_day` でも同じです。[^1]

この記事 https://qiita.com/hirokishirai/items/93ca9b566dddc815063c を見て time of day を使うのも良さそうだと思いましたが、 `since` に入れることを考えると意味がなさそうです。

もっと簡潔な方法はあるのでしょうか。

[^1]: そもそもそれに気づいていなかったので、 `seconds_since_midnight` に気づくのも遅れました。

# 日付と時刻を別々に取得して `Time.new` を使う方針で作ってみる
`Time.new` を使おうと思いました。

```ruby
Time.new(*[:year, :month, :day].map(&datetime_a.method(:send)) + [:hour, :min, :sec].map(&datetime_b.method(:send)))
```

見やすくすると

```ruby
Time.new(
  *
    [:year, :month, :day].map(&datetime_a.method(:send))
    +
    [:hour,   :min, :sec].map(&datetime_b.method(:send))
)
```

こういうことです。個人的には気に入っています。

追記: `%i` を使えば少し短くなります。あと、splat 展開 `*` を二回使えば `+` の演算は不要になります ([コメント](https://qiita.com/negito6/items/493b8630e3937740ee7a#comment-856cc96f90588e6f1195) より)

## もう一段 map したけど
上記だと、 `.method(:send)` あたりが重複しているのでまとめてみようと思いました。

```rb
Time.new(*{[:year, :month, :day] => datetime_a, [:hour, :min, :sec] => datetime_b}.flat_map { |keys, time| keys.map(&time.method(:send)) })
```
却って長くなってしまいました。ハッシュの記号と `map` の仮引数(?)部分が結構長くつくようです [^2]

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

## 時間をの構成要素を `to_a` で取り出す

```ruby
> datetime_b.to_a
=> [56, 34, 12, 1, 1, 2018, 3, 101, false, "UTC"]
```
時刻に対する `to_a` は、配列で、ただし逆順で返ってきます。ここから一部を取り出したいのですが、 `Range` を使うと昇順しか指定できなさそうなので、 `values_at` を使います。

```ruby
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` の順) 。

```ruby
Time.new(*(datetime_b.to_a[0..2] + datetime_a.to_a[3..5]).reverse)
```
これは割と短めです。

# `seconds_since_midnight` を再発明する悲しい作業記録
当初、 `seconds_since_midnight` を知るまでは、これでいこうかと思っていました。

```ruby
datetime_a.midnight.since(datetime_b - datetime_b.midnight)
```

`datetime_b` が二回出てくるなあと思っていました。

## `instance_eval` 使っうちゃう
変数が複数回出てくるときは `instance_eval` を使うと一回で済むことがあります。

```ruby
datetime_a.midnight.since(datetime_b.instance_eval { self - midnight })
```

残念。記述量が増えました（そういう問題？

## 愚直に計算
`seconds_since_midnight` を愚直に計算してみました。

```ruby
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(:+))
```

見やすく (?) すると

```ruby
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(:+)
)
```
友達をなくしそうなコードです。

どういうことかというと、

```ruby
> [:hour, :min, :sec].map(&datetime_b.method(:send))
=> [12, 34, 56]
```
こうやって取れた　`[時・分・秒]` の、それぞれを秒単位に揃えるため `[3600, 60, 1]` とそれぞれの成分を掛け合わせます。

```ruby
> [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 への掛け算になってしまうと思ったのですが、そうではないようでした。
 => 追記: [コメント](https://qiita.com/negito6/items/493b8630e3937740ee7a#comment-856cc96f90588e6f1195) より、 `inject` の引数で初期値を渡さない場合はそもそも演算が行われないそうです


## `map` は配列を展開できる
`Array#sum` とか普通に実装されているし、二者の掛け算は `inject` よりも配列にバラして受け取り、計算式を書いたほうが短いことに気づきました。

```ruby
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` を使ったほうがいいでしょうか。

```ruby
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` 使って計算すれば普通に短くなりました。数も固定長だし変数も要らないので、内積の方針でいくのであればこうしたほうがよさそうです。

```ruby
datetime_a.midnight.since(datetime_b.instance_eval { 3600 * hour + 60 * min + sec })
```