8539


現在、RubyのDate型を利用して2つのDate型インスタンスから、  
その差分の日数を「□年◯ヶ月△日」で取得する方法を考えいているのですが上手く行きません。

例) d1 = Date.new(2014,3,3), d2 = Date.new(2014,5,10)  
望んでいる出力  
'0年2ヶ月7日' = date_difference(d1,d2)

結果はStringオブジェクトでなくても構いません。

現状はIntegerで日数の差分を取得を取得しています。（上の例の場合：68日）  
ですが、そこから上手く年、月、日に変換する事ができていません。  
主に日数の差が1月以上ある際に、月数を出すために、その月の日数（30,31,28など）によって除数が変わるためアルゴリズムが複雑になってしまうという問題があります。

上記ような理由があり、期待する値をシンプルに求めるためにDate型の機能を使用できればと思っています。  
なにか思い当たる節がある方がいらしゃったら教えていただけると幸いです。  
また,Date型を使用しなくてもスマートに書ける方法が思いつく方がいましたら、そちらでも構いません。


CC BY-SA 3.0
2015-03-29T10:53:26.377
8530
2015-03-29T21:27:54.800
7944
question
ruby ruby-on-rails
RubyのDate型で日付の差分を「□年◯ヶ月△日」で取得する方法


d1 <= d2 限定ですが、こんな感じでどうでしょう？

[code]

    require 'date'
    
    d1 = Date.new( 2014, 3, 3 )
    d2 = Date.new( 2014, 5, 10 )
    
    # d1の1日を作ってd2を補正
    d2 -= d1 - Date.new( d1.year, d1.month, 1 )
    
    # 月数で計算
    diff_months = d2.year * 12 + d2.month - d1.year * 12 - d1.month
    
    # 年月日に戻す
    diff_years = diff_months / 12
    diff_months -= diff_years * 12
    diff_days = d2.day - 1
    
    puts "#{diff_years}年#{diff_months}ヶ月#{diff_days}日"
    
[/code]


CC BY-SA 3.0
2015-03-29T14:38:01.733
8539
2015-03-29T14:38:01.733
7900
8530
answer


activesupportというgemを使うと、特定の日付からのxx日後、xx日前、xxヶ月後、xxヶ月前、といった日付を取得することができます。

[code]

    require 'active_support/all'
    
    '2015-01-01'.to_date.since(1.day).to_date
    # => Fri, 02 Jan 2015
    '2015-01-01'.to_date.since(1.month).to_date
    # => Sun, 01 Feb 2015
    '2015-01-01'.to_date.since(1.year).to_date
    # => Fri, 01 Jan 2016
    
[/code]

全部自前で計算しようとするとかなり苦労すると思うので、まずはこういったgemの力を借りるのがよいかと思います。

ただし、具体的な計算ロジックはここで書くことは出来ません。  
なぜなら仕様が明確でないからです。

同じ年月内（2015-01-01から2015-01-15等）や、xx月1日同士のdiff（2015-01-01から2015-02-01等）であればわかりやすいですが、どちらかの日付に28日～31日が含まれる場合や、閏年が絡んでくる場合にどういう結果を得たいのかが不明です。

たとえば、activesupportを使うと、2015-01-28の1ヶ月後も、2015-01-31の1ヶ月後も、どちらも2015-02-28が返ってきます。

[code]

    '2015-01-28'.to_date.since(1.month).to_date
    # => Sat, 28 Feb 2015
    '2015-01-31'.to_date.since(1.month).to_date
    # => Sat, 28 Feb 2015
    
[/code]

なので、考え方によっては'2015-01-28'から'2015-02-28'も、'2015-01-31'から'2015-02-28'も、どちらも「0年1ヶ月0日」だ、という見方もできます。

一方で、「'2015-01-31'から'2015-02-27' => 0年0ヶ月27日」であれば「'2015-01-31'から'2015-02-28' =>
0年0ヶ月28日」だ、と考える人も出てきそうです。

このあたりはかなり多くの組み合わせが考えられそうです。

もし仕様が明確に決まっているのであればそのあたりも追記してください。

### 追記

参考までに動作確認用に書いた計算ロジックとテストコードを載せておきます。  
あくまで動作確認用なので、あまりきれいなコードじゃないです。

[code]

    require 'active_support/all'
    
    def date_diff(date_from, date_to)
      month_diff = 0
      while date_from.since((month_diff + 1).months).to_date <= date_to
        month_diff += 1
      end
      day_diff = (date_to - date_from.since((month_diff).months).to_date).to_i
      year_diff, month_diff = month_diff.divmod(12)
      [year_diff, month_diff, day_diff]
    end
    
    describe '#date_diff' do
      subject { date_diff(date_from.to_date, date_to.to_date) }
      context '同じ日' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2015-01-01' }
        it { is_expected.to eq [0, 0, 0] }
      end
      context '1日違い' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2015-01-02' }
        it { is_expected.to eq [0, 0, 1] }
      end
      context '1日-月末' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2015-01-31' }
        it { is_expected.to eq [0, 0, 30] }
      end
      context '1ヶ月違い' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2015-02-01' }
        it { is_expected.to eq [0, 1, 0] }
      end
      context '1ヶ月と1日違い' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2015-02-02' }
        it { is_expected.to eq [0, 1, 1] }
      end
      context '1ヶ月後に同じdayが存在しない場合' do
        context '1ヶ月未満' do
          let(:date_from) { '2015-01-31' }
          let(:date_to) { '2015-02-01' }
          it { is_expected.to eq [0, 0, 1] }
        end
        context '翌月の月末まで' do
          let(:date_from) { '2015-01-31' }
          let(:date_to) { '2015-02-28' }
          it { is_expected.to eq [0, 1, 0] }
        end
        context '1/30から翌月の月末まで' do
          let(:date_from) { '2015-01-30' }
          let(:date_to) { '2015-02-28' }
          it { is_expected.to eq [0, 1, 0] }
        end
        context '1/29から翌月の月末まで' do
          let(:date_from) { '2015-01-29' }
          let(:date_to) { '2015-02-28' }
          it { is_expected.to eq [0, 1, 0] }
        end
        context '1/28から翌月の月末まで' do
          let(:date_from) { '2015-01-28' }
          let(:date_to) { '2015-02-28' }
          it { is_expected.to eq [0, 1, 0] }
        end
        context '1/27から翌月の月末まで' do
          let(:date_from) { '2015-01-27' }
          let(:date_to) { '2015-02-28' }
          it { is_expected.to eq [0, 1, 1] }
        end
        context '1/28から翌月の月末-1まで' do
          let(:date_from) { '2015-01-28' }
          let(:date_to) { '2015-02-27' }
          it { is_expected.to eq [0, 0, 30] }
        end
        context '1/29から翌月の月末-1まで' do
          let(:date_from) { '2015-01-29' }
          let(:date_to) { '2015-02-27' }
          it { is_expected.to eq [0, 0, 29] }
        end
        context '31-27' do
          let(:date_from) { '2015-01-31' }
          let(:date_to) { '2015-02-27' }
          it { is_expected.to eq [0, 0, 27] }
        end
      end
      context 'スタックオーバーフローの例' do
        let(:date_from) { '2014-03-03' }
        let(:date_to) { '2014-05-10' }
        it { is_expected.to eq [0, 2, 7] }
      end
      context '1年違い' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2016-01-01' }
        it { is_expected.to eq [1, 0, 0] }
      end
      context '1年と1日違い' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2016-01-02' }
        it { is_expected.to eq [1, 0, 1] }
      end
      context '1年と-1日違い' do
        let(:date_from) { '2015-01-01' }
        let(:date_to) { '2015-12-31' }
        it { is_expected.to eq [0, 11, 30] }
      end
      context '閏年が関連する場合' do
        context '28-28' do
          let(:date_from) { '2015-02-28' }
          let(:date_to) { '2016-02-28' }
          it { is_expected.to eq [1, 0, 0] }
        end
        context '28-29' do
          let(:date_from) { '2015-02-28' }
          let(:date_to) { '2016-02-29' }
          it { is_expected.to eq [1, 0, 1] }
        end
        context '閏年の28-28' do
          let(:date_from) { '2016-02-28' }
          let(:date_to) { '2017-02-28' }
          it { is_expected.to eq [1, 0, 0] }
        end
        context '29-28' do
          let(:date_from) { '2016-02-29' }
          let(:date_to) { '2017-02-28' }
          it { is_expected.to eq [1, 0, 0] }
        end
        context '閏年の29-3月1日' do
          let(:date_from) { '2016-02-29' }
          let(:date_to) { '2017-03-01' }
          it { is_expected.to eq [1, 0, 1] }
        end
        context '閏年の28-3月1日' do
          let(:date_from) { '2016-02-28' }
          let(:date_to) { '2017-03-01' }
          it { is_expected.to eq [1, 0, 1] }
        end
      end
    end
    
[/code]

このロジックを使うと、例として挙がっている日付のdiffも「0年2ヶ月7日」になります。

[code]

      context 'スタックオーバーフローの例' do
        let(:date_from) { '2014-03-03' }
        let(:date_to) { '2014-05-10' }
        it { is_expected.to eq [0, 2, 7] }
      end
    
[/code]

一方で、日付は異なるのにdiffは同じ、というケースも出てきます。

[code]

    context '翌月の月末まで' do
      let(:date_from) { '2015-01-31' }
      let(:date_to) { '2015-02-28' }
      it { is_expected.to eq [0, 1, 0] }
    end
    context '1/30から翌月の月末まで' do
      let(:date_from) { '2015-01-30' }
      let(:date_to) { '2015-02-28' }
      it { is_expected.to eq [0, 1, 0] }
    end
    context '1/29から翌月の月末まで' do
      let(:date_from) { '2015-01-29' }
      let(:date_to) { '2015-02-28' }
      it { is_expected.to eq [0, 1, 0] }
    end
    context '1/28から翌月の月末まで' do
      let(:date_from) { '2015-01-28' }
      let(:date_to) { '2015-02-28' }
      it { is_expected.to eq [0, 1, 0] }
    end
    
    context '閏年の28-28' do
      let(:date_from) { '2016-02-28' }
      let(:date_to) { '2017-02-28' }
      it { is_expected.to eq [1, 0, 0] }
    end
    context '29-28' do
      let(:date_from) { '2016-02-29' }
      let(:date_to) { '2017-02-28' }
      it { is_expected.to eq [1, 0, 0] }
    end
    
[/code]

なお、テストコード（RSpec）がよくわからないという場合はこちらの記事を参考にしてみてください。

[使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」](http://qiita.com/jnchito/items/42193d066bd61c740612)


CC BY-SA 3.0
2015-03-29T21:05:14.103
8543
2015-03-29T21:27:54.800
2015-03-29T21:27:54.800
85
85
8530
answer