Help us understand the problem. What is going on with this article?

Ruby で現在日付に最も近い日付を求める方法を色々考えてみた

More than 1 year has passed since last update.

はじめに

最近、日付の配列から現在日付に最も近い日付を求める方法を考える機会がありました。

いざやってみると案外一筋縄ではいかない題材でした。また、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を使う

次に mapmin を使ってできるか試しました。以下のような感じです。

# 初期設定
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 さん、コメントありがとうございました!

終わりに

複数の実装パターンが想定できる場合は、何が良いかを実際実装してみて比較すると面白いですね。

もしもっとスマートな実装がありましたら教えていただけるとありがたいです!

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away