14

In a Rails project I want to find the difference between two dates and then display it in natural language. Something like

>> (date1 - date2).to_natural_language 
"3 years, 2 months, 1 week, 6 days"

Basically this for ruby.

Google and the Rails API haven't turned up anything. I've found some things that will give you the difference in one unit (ie, how many weeks between two dates) but not something that will accurately calculate years, months, weeks, days all together.

Community
  • 1
  • 1
Eric Wright
  • 825
  • 3
  • 8
  • 14

6 Answers6

55

The Rails' ActionView module includes two methods that may do what you require:

Shiva
  • 11,485
  • 2
  • 67
  • 84
John Topley
  • 113,588
  • 46
  • 195
  • 237
24

The other answers may not give the type of output that you're looking for, because instead of giving a string of years, months, etc., the Rails helpers just show the largest unit. If you're looking for something more broken down, here's another option. Stick this method into a helper:

def time_diff_in_natural_language(from_time, to_time)
  from_time = from_time.to_time if from_time.respond_to?(:to_time)
  to_time = to_time.to_time if to_time.respond_to?(:to_time)
  distance_in_seconds = ((to_time - from_time).abs).round
  components = []

  %w(year month week day).each do |interval|
    # For each interval type, if the amount of time remaining is greater than
    # one unit, calculate how many units fit into the remaining time.
    if distance_in_seconds >= 1.send(interval)
      delta = (distance_in_seconds / 1.send(interval)).floor
      distance_in_seconds -= delta.send(interval)
      components << pluralize(delta, interval)
      # if above line give pain. try below one  
      # components <<  interval.pluralize(delta)  
    end
  end

  components.join(", ")
end

And then in a view you can say something like:

<%= time_diff_in_natural_language(Time.now, 2.5.years.ago) %>
=> 2 years, 6 months, 2 days 

The given method only goes down to days, but can be easily extended to add in smaller units if desired.

uzaif
  • 3,511
  • 2
  • 21
  • 33
Daniel Vandersluis
  • 91,582
  • 23
  • 169
  • 153
  • This looks really promising. Unfortunately I'm an getting "undefined method `pluralize' for #>" error (in the console). – Eric Wright Jun 30 '09 at 20:06
  • That's because pluralize is an ActionView helper method: http://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html. If you say `include ActionView::Helpers::TextHelper` in script/console, pluralize will be defined. As mentioned in the answer, you should stick the method in a view helper (in app/helpers) and then refer to it from a view. – Daniel Vandersluis Jun 30 '09 at 20:11
9

I tried Daniel's solution and found some incorrect results for a few test cases, due to the fact that it doesn't correctly handle the variable number of days found in months:

> 30.days < 1.month
   => false

So, for example:

> d1 = DateTime.civil(2011,4,4)
> d2 = d1 + 1.year + 5.months
> time_diff_in_natural_language(d1,d2)
=> "1 year, 5 months, 3 days" 

The following will give you the correct number of {years,months,days,hours,minutes,seconds}:

def time_diff(from_time, to_time)
  %w(year month day hour minute second).map do |interval|
    distance_in_seconds = (to_time.to_i - from_time.to_i).round(1)
    delta = (distance_in_seconds / 1.send(interval)).floor
    delta -= 1 if from_time + delta.send(interval) > to_time
    from_time += delta.send(interval)
    delta
  end
end
> time_diff(d1,d2)
 => [1, 5, 0, 0, 0, 0] 

Community
  • 1
  • 1
klochner
  • 8,077
  • 1
  • 33
  • 45
  • Long ago, but I tried this now, and it works good. A little comment though, considering DST. Having `Jun 1` and `Dec 1` both at `12:00`, is the difference between them `6 months` or `6 months and 1 hour`? I decided for the latter, calculating with times in UTC. – 244an Feb 08 '21 at 20:11
3

distance_of_time_in_words is the most accurate here. Daniel's answer is actully wrong: 2.5 years ago should produce exactly 2 years, 6 months. The issue is that months contain 28-31 day, and years might be leap.

I wish I knew how to fix this :(

Anton
  • 2,483
  • 2
  • 23
  • 35
2

DateHelper#distance_of_time_in_words

Pesto
  • 23,810
  • 2
  • 71
  • 76
  • 1
    This is really good (and I'm annoyed I didn't find it before), but it still only gives me a rough approximation. I.e., it will give me "3 months", not "3 months, 4 days". – Eric Wright Jun 30 '09 at 20:00
0
def date_diff_in_natural_language(date_from, date_to)
  components = []
  %w(years months days).each do |interval_name|
    interval = 1.send(interval_name)
    count_intervals = 0
    while date_from + interval <= date_to
      date_from += interval
      count_intervals += 1
    end
    components << pluralize(count_intervals, interval_name) if count_intervals > 0
  end
  components.join(', ')
end