はじめに
最近、日付の配列から現在日付に最も近い日付を求める方法を考える機会がありました。
いざやってみると案外一筋縄ではいかない題材でした。また、Ruby では色々な書き方ができそうだったので、試した実装をまとめてみました。
実装の前提
日付の配列リスト
ランダムに格納された2018年の日付リストを1 ~ 12月分作成しました。ただし、日付が重複していないこととします。
require 'date'
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
dates
[Wed, 01 Aug 2018,
Mon, 01 Oct 2018,
Thu, 01 Feb 2018,
Thu, 01 Nov 2018,
Sun, 01 Apr 2018,
Thu, 01 Mar 2018,
Tue, 01 May 2018,
Mon, 01 Jan 2018,
Sun, 01 Jul 2018,
Sat, 01 Dec 2018,
Fri, 01 Jun 2018,
Sat, 01 Sep 2018]
現在日付の扱い方について
現在日付は Date.today
で取得できますが、今回は場合を分けて結果を再現しやすいように、Date.new
で日付を切り替えて試してみます。
require 'date'
today = Date.new(2018, 11, 15)
today
Thu, 15 Nov 2018
① 日付リストに過去日付のみ含まれる場合は max
メソッドで OK
過去日付のみの場合、最も新しい日付が現在日付に最も近い日付となります。
この場合は、Array#max
メソッドが使えます。配列の中の Date オブジェクトの中から最も新しい日付を返却してくれます。
現在日付を日付リストの日付より未来(2019/11/15)として試してみると、想定通り 2018/12/01
の日付が取得できます。
# 初期設定
require 'date'
today = Date.new(2019, 11, 15)
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
# 実装
dates.max
# 結果
=> Sat, 01 Dec 2018
② 日付リストに未来日付のみ含まれる場合は min
メソッドで OK
未来日付のみの場合、過去日付の場合とは逆に、最も古い日付が現在日付に最も近い日付となります。
こちらは Array#min
メソッドが使えます。配列の中の Date オブジェクトの中から最も古い日付を返却してくれます。
現在日付を日付リストの日付より過去(2017/11/15)として試してみると、想定通り 2018/01/01
の日付が取得できます。
# 初期設定
require 'date'
today = Date.new(2017, 11, 15)
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
# 実装
dates.min
# 結果
=> Mon, 01 Jan 2018
③ 日付リストに過去・未来いずれの日付も含まれる場合は…?
過去日付のみ、未来日付のみの場合は割とシンプルに扱えたのですが、過去日付・未来日付いずれも含まれる場合になるとちょっと複雑になります。
この場合は、(現在日付 - 比較対象の日付) の日数が最小になる日付 を配列の中からピックアップする必要があります。
こちらを実現する方法をいくつか考えました。
なお、現在日付は 2018/11/15
として扱います。結果の想定は 2018/11/01
となります。
パターン1. each
を使う
まず直感的に思いつきやすい、each
で単純に配列をループさせて評価していく方法から試しました。
# 初期設定
require 'date'
today = Date.new(2018, 11, 15)
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
# 実装
most_recent_date = dates.first
dates.each do |date|
if (today - most_recent_date).abs > (today - date).abs
most_recent_date = date
end
end
most_recent_date
# 結果
=> Thu, 01 Nov 2018
過去・未来いずれの日付もあるため、日付の計算結果が負になったりすることがあります。そのため、abs
で絶対値にしています。
その絶対値が最終的に最も少ない日付を返却しています。
直感的な実装でできましたが、一時的な変数も使っているため、処理が閉じていない印象があります。
パターン2. map
, min
を使う
次に map
、min
を使ってできるか試しました。以下のような感じです。
# 初期設定
require 'date'
today = Date.new(2018, 11, 15)
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
# 実装
dates.map{|date| {"#{(today - date).abs.to_i}" => date}}
.min{|a, b| a.keys.first.to_i <=> b.keys.first.to_i }
.values.first
# 結果
=> Thu, 01 Nov 2018
最初の map
で、現在日付から引いた日数の絶対値をキーとした日付のハッシュを作ります。
データイメージはこんな感じです。
[{"137"=>Sun, 01 Jul 2018},
{"106"=>Wed, 01 Aug 2018},
...
]
また日付の計算の戻り値は (2/1)
のような Rational
クラスとなるので、比較しやすいように to_i
メソッドで整数にしています。
次の min
で、ハッシュのキーのうち最小の日数のデータを抽出しています。
{"14"=>Thu, 01 Nov 2018}
あとは values.first
でハッシュの値として入れていた日付を取得します。
一時的な変数の利用がなくなり1つの処理にまとまりましたが、メソッドを複数使っていたり、途中でハッシュに変換したりと、複雑な実装になっています。
パターン3. inject
を使う
次は畳み込み計算の inject
メソッドを使ってできるかやってみました。以下のような感じです。
# 初期設定
require 'date'
today = Date.new(2018, 11, 15)
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
# 実装
dates.inject do |most_recent_date, date|
if (today - most_recent_date).abs > (today - date).abs
date
else
most_recent_date
end
end
# 結果
=> Thu, 01 Nov 2018
畳み込み計算のイメージは以下のような感じです。それぞれの日数比較を畳み込み計算で進める形にしました。
回数 | most_recent_date | date | ループの計算結果 |
---|---|---|---|
1 | 最初の要素 | 最初の要素 | 最初の要素 |
2 | 1回目の結果 | 2番目要素 | 1回目の結果と2番目で日数小さい方の日付 |
3 | 2回目の結果 | 3番目要素 | 2回目の結果と3番目で日数小さい方の日付 |
4 | ... | ... | ... |
一時変数も無く、ハッシュの変換なども不要になりましたが、プログラムの行数をもう少し削りたいところです。
パターン4. sort_by
を使う
最後に、sort_by
を使った方法です。以下のような感じです。
# 初期設定
require 'date'
today = Date.new(2018, 11, 15)
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
# 実装
dates.sort_by{|date| (today - date).abs}.first
=>Thu, 01 Nov 2018
sort_by
で要素それぞれで現在日付との差分を取り、その差分の小さい順にソートしています。その後、first
で最初の要素を取得しています。
上記のパターンの中で最もシンプルで理解しやすい実装になりました。
パターン5. min_by
を使う(2018/12/01 追記)
パターン4をより簡潔にするために、min_by
メソッドを利用すると以下のような感じになります。
# 初期設定
require 'date'
today = Date.new(2018, 11, 15)
dates = 12.times.map{|n| Date.new(2018, n + 1)}.shuffle
# 実装
dates.min_by{|date| (today - date).abs}
=>Thu, 01 Nov 2018
min_by
メソッドでは、ブロックに要素を順番に渡してブロック内の式でそれぞれを評価し、最小であった値に対応する元の要素を返します。
こちらを使うと、sort_by
メソッドを使う場合では必要だった first
メソッドが不要になり、更に簡潔になります。
@yusuke23 さん、コメントありがとうございました!
終わりに
複数の実装パターンが想定できる場合は、何が良いかを実際実装してみて比較すると面白いですね。
もしもっとスマートな実装がありましたら教えていただけるとありがたいです!
参考
- ruby - rails helper function to return the most recent date - Stack Overflow
- instance method Array#max (Ruby 2.5.0)
- instance method Array#min (Ruby 2.5.0)
- instance method Enumerable#collect (Ruby 2.5.0)
- instance method Enumerable#inject (Ruby 2.5.0)
- instance method Enumerable#sort_by (Ruby 2.5.0)
- instance method Array#each (Ruby 2.5.0)
- instance method Enumerable#min_by (Ruby 2.5.0)