Time Ago Method for Ruby on Rails

Posted by acts_as_flinn Tue, 10 Apr 2007 22:27:00 GMT

Updated 2008-09-03 – This article is super old but as it comes up pretty high on in search pages for "rails time" and "rails time ago" I should probably note rails has this code in it as a helper: time_ago_in_words

I recently searched for a Ruby or Ruby on Rails method to display date and time ago ala Typo, ex: “Posted 10 minutes ago”. As it turns out Typo uses Javascript to handle this.

Typo’s js_distance_of_time_in_words_to_now

Typo uses the method js_distance_of_time_in_words_to_now which results in something like this:

Posted by <cite>acts_as_flinn</cite>
<abbr class="published" title="2007-04-05T12:37:00-04:00"><span class="typo_date" title="Thu, 05 Apr 2007 16:37:00 GMT">Thu, 05 Apr 2007 16:37:00 GMT</span></abbr>

This is then parsed out to the users local time using javascript.

Rails Time Conversion API

I was surprised that Rails didn’t include something like this out of the box. The API docs refer to a number of convenience methods for handling time like since, ago, months_ago, years_ago, etc. but these don’t do what I want, instead the may it easy to use Numeric time conversions, for example: 5.month_ago returns a Time object set to 5 months ago.

Time as an adjective

What I really needed was time described in human readable terms with an adjective. Like 5 days ago, 10 minutes ago, 3 years ago, etc. So I googled around and found a reference to a timeago module written for Drupal http://zertox.com/topic/drupal/drupal_module_time_ago. I ported it to Ruby and placed it in my application helper, and it seems to work as intended.

# options
# :start_date, sets the time to measure against, defaults to now
# :later, changes the adjective and measures time forward
# :round, sets the unit of measure 1 = seconds, 2 = minutes, 3 hours, 4 days, 5 weeks, 6 months, 7 years (yuck!)
# :max_seconds, sets the maximimum practical number of seconds before just referring to the actual time
# :date_format, used with <tt>to_formatted_s<tt>
def timeago(original, options = {})
  start_date = options.delete(:start_date) || Time.now
  later = options.delete(:later) || false
  round = options.delete(:round) || 7
  max_seconds = options.delete(:max_seconds) || 32556926
  date_format = options.delete(:date_format) || :default

  # array of time period chunks
  chunks = [
    [60 * 60 * 24 * 365 , "year"],

    [60 * 60 * 24 * 30 , "month"],
    [60 * 60 * 24 * 7, "week"],
    [60 * 60 * 24 , "day"],
    [60 * 60 , "hour"],
    [60 , "minute"],
    [1 , "second"]
  ]

  if later
    since = original.to_i – start_date.to_i
  else
    since = start_date.to_i – original.to_i
  end
  time = []

  if since < max_seconds
    # Loop trough all the chunks
    totaltime = 0

    for chunk in chunks[0..round]
      seconds    = chunk[0]
      name       = chunk[1]

      count = ((since – totaltime) / seconds).floor
      time << pluralize(count, name) unless count == 0

      totaltime += count * seconds
    end

    if time.empty?
      "less than a #{chunks[round-1][1]} ago"
    else
      "#{time.join(’, ‘)} #{later ? ‘later’ : ‘ago’}"
    end
  else
    original.to_formatted_s(date_format)
  end
end

This yields results like 1 week, 18 hours ago. I think it works pretty well but I wanted it a little more vague like how Typo does it, so I ported the Typo methods from Javascript to Ruby.

Ruby on Rails Time Ago

  # options
  # :start_date, sets the time to measure against, defaults to now
  # :date_format, used with <tt>to_formatted_s<tt>, default to :default
  def timeago(time, options = {})
    start_date = options.delete(:start_date) || Time.new
    date_format = options.delete(:date_format) || :default
    delta_minutes = (start_date.to_i – time.to_i).floor / 60
    if delta_minutes.abs <= (8724*60) # eight weeks… I’m lazy to count days for longer than that
      distance = distance_of_time_in_words(delta_minutes);
      if delta_minutes < 0
        "#{distance} from now"
      else
        "#{distance} ago"
      end
    else
      return "on #{system_date.to_formatted_s(date_format)}"
    end
  end

  def distance_of_time_in_words(minutes)
    case
      when minutes < 1
        "less than a minute"
      when minutes < 50
        pluralize(minutes, "minute")
      when minutes < 90
        "about one hour"
      when minutes < 1080
        "#{(minutes / 60).round} hours"
      when minutes < 1440
        "one day"
      when minutes < 2880
        "about one day"
      else
        "#{(minutes / 1440).round} days"
    end
  end

The result is a nice vague string that rounds to the nearest unit of measure without being overly specific.

Enjoy!

Comments

Leave a response

Comments


ss_blog_claim=746d258dc975cb7923cc57154dbf1d71