110

I have two Ruby on Rails DateTime objects. How to find the number of months between them? (Keeping in mind they might belong to different years)

phoenixwizard
  • 5,079
  • 7
  • 27
  • 38

14 Answers14

198
(date2.year * 12 + date2.month) - (date1.year * 12 + date1.month)

more info at http://www.ruby-forum.com/topic/72120

Miscreant
  • 6,626
  • 3
  • 22
  • 21
Massimiliano Peluso
  • 26,379
  • 6
  • 61
  • 70
48

A more accurate answer would consider days in the distance.

For example, if you consider that the month-distance from 28/4/2000 and 1/5/2000 is 0 rather than 1, then you can use:

(date2.year - date1.year) * 12 + date2.month - date1.month - (date2.day >= date1.day ? 0 : 1)
dgilperez
  • 10,716
  • 8
  • 68
  • 96
  • 2
    e.g. for calculating the number of times someone has received his salary always being at the 22nd of the months – Matthias Jul 22 '16 at 13:31
16

Assuming both are dates: ((date2 - date1).to_f / 365 * 12).round simple.

Andreykul
  • 303
  • 3
  • 9
  • 1
    Very nice solution! Clean, simple, and even works across years – i.e. the range of months between February 2018 and December 2017. – jeffdill2 May 03 '18 at 18:57
  • 1
    Needs to take hours, minutes, and seconds into account `((date2 - date1).to_f / 60 / 60 / 24 / 365 * 12).round` – scarver2 Aug 02 '18 at 00:41
  • 2
    @scarver2 The number you get from your calculation is way off. All we are doing is taking days and converting them to months, it is not super accurate because it assumes 30.4167 days per month, but it very close and then it rounds anyways. – Andreykul Aug 02 '18 at 13:21
15

Give a try to

((date2.to_time - date1.to_time)/1.month.second).to_i
knotito
  • 1,382
  • 1
  • 12
  • 18
  • `irb>Time.at("2014-10-01".to_time - "2014-12-01".to_time).month => 10` (I give up on formatting... can't figure it out) – Toby Joiner Dec 23 '14 at 16:11
  • Even if you use the Date object, @toby-joiner 's comment will still be correct. The reason is that - is -2 months, but Time.at(-2months).month gives the month equivalent of -2 (i.e. -2 % 12 #=> 10). Your suggested method will work if the two months follow one another and are less than a year apart, but it will fail if the two months are in reverse order (e.g. October - December) or if the two months are more than a year apart. – elreimundo Jan 09 '15 at 18:37
  • Time differences gives you a timestamp corresponding – knotito Jan 09 '15 at 20:33
  • if you use `date1 = '2016-02-01'.to_date` and `date2 = '2016-03-01'.to_date` the number of months is `0`. :( – William Weckl Sep 20 '16 at 13:22
  • Using `round(1).to_i` is more accurate. – William Weckl Sep 20 '16 at 13:30
  • 4
    I like the idea behind it, but it is incorrect if the datespan is becoming really long. In my example of `Date.new(2014, 10, 31), Date.new(1997, 4, 30)` I received 213 instead of 210 months. The reason for that is that `1.month.seconds` is the number of seconds in 30 days and not the average seconds in a month. Downvoting so that others stay bug-free. – Joe Eifert Sep 26 '16 at 12:43
  • 2
    this wont work if you choose a month that isn't 30 days – dima Mar 19 '17 at 13:31
  • 3
    this method is not very accurate . – zztczcx Apr 06 '18 at 04:49
  • How has this gotten so many upvotes? It would be merely happenstance for it to produce an accurate number. – jeffdill2 May 03 '18 at 18:55
11

You can rephrase the question as "how many 1st days is there between the beginnings of months of the dates", and then use functional-style data transformations:

(date1.beginning_of_month...date2.beginning_of_month).select { |date| date.day == 1 }.size
shock_one
  • 5,845
  • 3
  • 28
  • 39
  • And to get back to the original question, you could sum the days (like you are doing with the first) and take the average of the sums. – Joe Eifert Sep 26 '16 at 12:47
4
start_date = Date.today
end_date   = Date.today+90
months = (end_date.month+end_date.year*12) - (start_date.month+start_date.year*12)

//months = 3
Giuseppe
  • 5,188
  • 4
  • 40
  • 37
3

I needed the exact number of months (including decimals) between two dates and wrote the following method for it.

def months_difference(period_start, period_end)
  period_end = period_end + 1.day
  months = (period_end.year - period_start.year) * 12 + period_end.month - period_start.month - (period_end.day >= period_start.day ? 0 : 1)
  remains = period_end - (period_start + months.month)

  (months + remains/period_end.end_of_month.day).to_f.round(2)
end

If comparing let's say September 26th to September 26th (same day) I calculate it as 1 day. If you don't need that you can remove the first line in the method: period_end = period_end + 1.day

It passes the following specs:

expect(months_difference(Date.new(2017, 8, 1), Date.new(2017, 8, 31))).to eq 1.0
expect(months_difference(Date.new(2017, 8, 1), Date.new(2017, 8, 30))).to eq 0.97
expect(months_difference(Date.new(2017, 8, 1), Date.new(2017, 10, 31))).to eq 3.0
# Overlapping february (28 days) still counts Feb as a full month
expect(months_difference(Date.new(2017, 1, 1), Date.new(2017, 3, 31))).to eq 3.0
expect(months_difference(Date.new(2017, 2, 10), Date.new(2017, 3, 9))).to eq 1.0
# Leap year
expect(months_difference(Date.new(2016, 2, 1), Date.new(2016, 2, 29))).to eq 1.0
mtrolle
  • 2,234
  • 20
  • 19
2

Another solution that I found (built off a solution posted here already) is for if you want the result to include fractions of a month. For example the distance is 1.2 months.

((date2.to_time - date1.to_time)/1.month.second).round(1) #Tenth of a month Ex: 1.2
((date2.to_time - date1.to_time)/1.month.second).round(2) #Hundreth, ex: 1.23 months
etc...
Zack
  • 2,377
  • 4
  • 25
  • 51
2
def difference_in_months(date1, date2)
  month_count = (date2.year == date1.year) ? (date2.month - date1.month) : (12 - date1.month + date2.month)
  month_count = (date2.year == date1.year) ? (month_count + 1) : (((date2.year - date1.year - 1 ) * 12) + (month_count + 1))
  month_count
end
  • 1
    It would be useful if you could elaborate on this? Generally on SO answers include an explanation on what the code does and why it works as a solution – Single Entity Apr 05 '17 at 09:33
2

Here's a brute forcing variant:

date1 = '2016-01-05'.to_date
date2 = '2017-02-27'.to_date
months = 0

months += 1 while (date2 << (count+1)) >= date1
puts months # => 13

date2 must be always greater than date1

ole
  • 5,166
  • 5
  • 29
  • 57
2

Solution for any cases

(date1..date2).map { |date| date.strftime('%m.%Y') }.uniq.size
1

How about this? I've found it pretty clean

((date2 - date1) / 1.month).round
0

Here is another method. This will help to calculate number of whole months between two dates

def months_difference(date_time_start, date_time_end)
  curr_months = 0
  while (date_time_start + curr_months.months) < date_time_end
    curr_months += 1
  end
  curr_months -= 1 if (date_time_start + curr_months.months) > date_time_end
  curr_months.negative? ? 0 : curr_months
end
0

If you want the real months,then you must consider the days, the next code take in count this.

# get the real years between date, it consider the months and days
def years_between_dates(since_date, until_date)
  years = until_date.year - since_date.year
  if (until_date.month < since_date.month) ||
     (until_date.month == since_date.month && since_date.day > until_date.day)
    years -= 1
  end
  years
end

# Get the months between dates, it consider the days
def difference_between_dates_in_months(since_date, until_date)
  months = (years_between_dates(since_date, until_date) * 12)
  until_month = until_date.month
  since_month = since_date.month

  if until_month > since_month
    months += since_month - until_month
  elsif until_month < since_month
    months += 12 - since_month + until_month
  end

  months -= 1 if(since_date.day > until_date.day)

  months
end
andres.ara
  • 206
  • 2
  • 8