0

How to calculate relative_time for seconds,minutes, days, months and year in ruby?

Given a DateTime object, implement relative time
d = DateTime.now
d.relative_time # few seconds ago

Possible outputs
# few seconds ago
# few minutes ago - till 3 minutes
# x minutes ago (here x starts from 3 till 59)
# x hours ago (x starts from 1 - 24) and so on for the below outputs
# x days ago
# x weeks ago
# x months ago
# x years ago

how to implement this in ruby? please help

nim
  • 1
  • 3
  • 3
    You might want to have a look at the [source of `distance_of_time_in_words` in Ruby on Rails](https://github.com/rails/rails/blob/fbe2433be6e052a1acac63c7faf287c52ed3c5ba/actionview/lib/action_view/helpers/date_helper.rb#L95) – spickermann Jul 26 '20 at 12:21
  • Please consider [How do I ask and answer homework questions?](https://meta.stackoverflow.com/a/334823/421705) when asking homework questions. – Holger Just Jul 26 '20 at 13:55

4 Answers4

2

Use GNU Date

In Ruby's core and standard library, there are no convenience methods for the type of output you want, so you'd have to implement your own logic. Without ActiveSupport extensions, you'd be better off calling out to the GNU (not BSD) date utility. For example:

now       = Time.now
last_week = %x(date -d "#{now} - 1 week")

If you pass a format flag to GNU date such as +"%s", the date command will provide the date as seconds since epoch. This allows you to create case statements for whatever units of time you want to compare, such whether it's more than 360 seconds ago or less than 86,400.

Please see the date input formats defined by GNU coreutils for a more comprehensive look at how to use the various date string options.

Using Rails Extensions

If you're willing to install and require a couple of Rails extensions in your code, you can use ActionView::Helpers::DateHelper#distance_of_time_in_words. For example:

require "active_support/core_ext/integer/time"
require "active_support/core_ext/numeric/time"
require "action_view"

include ActionView::Helpers::DateHelper

2.weeks.ago
#=> 2020-07-12 09:34:20.178526 -0400

distance_of_time_in_words Date.current, 1.month.ago
#=> "30 days"

sprintf "%s ago", distance_of_time_in_words(Time.now, 1.hour.ago)
#=> "about 1 hour ago"

Some combination of the various #ago methods coupled with date/time calculations, and calls to the distance helper will do what you want, but you'll have to implement some of your own logic if you want to cast the results differently (e.g. specifying that you want the output of large distances as weeks or months instead of in years).

Other Gems

There are certainly other Ruby gems that could help. The Ruby Toolbox shows a number of gems for determining time differences, but none seem to be well-maintained. Caveat emptor, and your mileage may vary.

See Also

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
1

I will construct a method relative_time that has two arguments:

  • date_time an instance of DateTime that specifies a time in the past; and
  • time_unit, one of :SECONDS, :MINUTES, :HOURS, :DAYS, :WEEKS, :MONTHS or :YEARS, specifying the unit of time for which the difference in time between the current time and date_time is to be expressed.

Code

require 'date'

TIME_UNIT_TO_SECS = { SECONDS:1, MINUTES:60, HOURS:3600, DAYS:24*3600,
                      WEEKS: 7*24*3600 }
TIME_UNIT_LBLS    = { SECONDS:"seconds", MINUTES:"minutes", HOURS:"hours",
                      DAYS:"days", WEEKS: "weeks", MONTHS:"months",
                      YEARS: "years" }

def relative_time(date_time, time_unit)
  now = DateTime.now
    raise ArgumentError, "'date_time' cannot be in the future" if
      date_time > now

  v = case time_unit
      when :SECONDS, :MINUTES, :HOURS, :DAYS, :WEEKS
        (now.to_time.to_i-date_time.to_time.to_i)/
          TIME_UNIT_TO_SECS[time_unit]
      when :MONTHS
        0.step.find { |n| (date_time >> n) > now } -1
      when :YEARS
        0.step.find { |n| (date_time >> 12*n) > now } -1
      else
        raise ArgumentError, "Invalid value for 'time_unit'"
      end
  puts "#{v} #{TIME_UNIT_LBLS[time_unit]} ago"
end

Examples

date_time = DateTime.parse("2020-5-20")

relative_time(date_time, :SECONDS)
5870901 seconds ago

relative_time(date_time, :MINUTES)
97848 minutes ago

relative_time(date_time, :HOURS)
1630 hours ago

relative_time(date_time, :DAYS)
67 days ago

relative_time(date_time, :WEEKS)
9 weeks ago

relative_time(date_time, :MONTHS)
2 months ago

relative_time(date_time, :YEARS)
0 years ago

Explanation

If time_unit equals :SECONDS, :MINUTES, :HOURS, :DAYS or :WEEKS I simply compute the number of seconds elapsed between date_time and the current time, and divide that by the number of seconds per the given unit of time. For example, if time_unit equals :DAYS the elapsed time in seconds is divided by 24*3600, as there are that many seconds per day.

If time_unit equals :MONTHS, I use the method Date#>> (which is inherited by DateTime) to determine the number of months that elapse from date_time until a time is reached that is after the current time, then subtract 1.

The calculation is similar if time_unit equals :YEARS: determine the number of years that elapse from date_time until a time is reached that is after the current time, then subtract 1.

One could require the user to enter a Time instance (rather than a DateTime instance) as the first argument. That would not simplify the method, however, as the Time instance would have to be converted to a DateTime instance when time_unit equals :MONTH or :YEAR, to use the method Date#>>.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • This is basically a code-only answer. Maybe you could add some explanation given that the OP seems to be a newbie. – Stefan Jul 27 '20 at 10:07
1

First of all, DateTime is intended for historical dates, whereas Time is used for current dates, so you probably want the latter. (see When should you use DateTime and when should you use Time?)

Given a time instance 45 minutes ago: (45 × 60 = 2,700)

t = Time.now - 2700

You can get the difference in seconds to the current time via Time#-:

Time.now - t
#=> 2701.360482

# wait 10 seconds

Time.now - t
#=> 2711.148882

This difference can be used in a case expression along with ranges to generate the corresponding duration string:

diff = Time.now - t

case diff
when 0...60       then "few seconds ago"
when 60...180     then "few minutes ago"
when 180...3600   then "#{diff.div(60)} minutes ago"
when 3600...7200  then "1 hour ago"
when 7200...86400 then "#{diff.div(3600)} hours ago"
# ...
end

You might have to adjust the numbers, but that should get you going.

Stefan
  • 109,145
  • 14
  • 143
  • 218
0

The following function uses the power of Date#<< to represent past dates relative to today.

def natural_past_date(date)
  date_today = Date.today
  date_then = Date.parse(date)

  if (date_today << 12) >= date_then
    dy = (1..100).detect { (date_today << (_1.next * 12)) < date_then }
    "#{dy} year#{dy > 1 ? 's' : ''} ago"
  elsif (date_today << 1) >= date_then
    dm = (1..12).detect { (date_today << _1.next) < date_then }
    "#{dm} month#{dm > 1 ? 's' : ''} ago"
  elsif (dw = (dd = (date_today - date_then).to_i)/7) > 0
    "#{dw} week#{dw > 1 ? 's' : ''} ago"
  else
    if (2...7) === dd
      "#{dd} days ago"
    elsif (1...2) === dd
      'yesterday'
    else
      'today'
    end
  end
end

Some sample usages of the function:

Date.today
=> #<Date: 2022-03-16 ...

natural_past_date '2020/01/09'
=> "2 years ago"

natural_past_date '2021/09/09'
=> "6 months ago"

natural_past_date '2022/03/08'
=> "1 week ago"

natural_past_date '2022/03/14'
=> "2 days ago"

natural_past_date '2022/03/15'
=> "yesterday"

natural_past_date '2022/03/16'
=> "today"