views:

1484

answers:

11

I'm looking for a fuzzy date algorithm. I just started writing one and realised what a tedious taks it is. It quickly degenerated into a lot of horrid code to cope with special cases like the difference between "yesterday", "last week" and "late last month" all of which can (in some cases) refer to the same day but are individually correct based on today's date.

I feel sure there must be an open source fuzzy date formatter but I can't find it. Ideally I'd like something using NSDate (OSX/iPhone) and its formatters but that isn't the difficult bit. Does anyone know of a fuzzy date formatter taking any time period relative to now and returning a string like (but not limited to):

  • a few moments ago
  • in the last five minutes
  • earlier today
  • this morning
  • last night
  • last week
  • last wednesday
  • early last month
  • june last year
  • a couple of years ago

In an ideal world I'd like the string to be as rich as possible (i.e. returning random variants on "Just a moment ago" such as "just now").

Clarification. I'm looking for something more subtle than basic buckts and strings. I want something that knows "yesterday" and "last wednesday" can both refer to the same period but only one is correct when today is Thursday.

+6  A: 

This question should get you started. It has the code this very site uses to calculate its relative time. It may not have the specific ranges you want, but they are easy enough to add once you got it setup.

Paolo Bergantino
Thanks, I think to get what I want, I need to add some extra code to one of these solutions.
Roger Nolan
+1  A: 

In my experience these types of date generators are not "fuzzy" at all. In fact, they are just a bunch of if statements based bands of time. For example, any time less than 30 seconds is "moments ago", 360 to 390 days is "just a year ago", etc. Some of these will use the target date to calculate the special names (June, Wednesday, etc). Sorry to dash an illusions you had.

Craig
You're partially right but there is a lot more subtelty available. Like the way you describe 5am is different if you are describing it from 6am on the same day, 6am the next day or 6am siz months later.
Roger Nolan
A: 

This is almost always done using a giant switch statement and is trivial to implement.

Keep the following in mind:

  • Always test for the smallest time span first
  • Don't forget to keep your strings localizable.
rein
+2  A: 

I am not sure why you say it would be a horrid coding practice. Each of the return strings are actually a subset of the parent set, so you can quite elegantly do this in a if/elseif chain.

if timestamp < 5sec
    "A moment ago"
elseif timestamp < 5min 
    "Few minutes ago"
elseif timestamp < 12hr && timestamp < noon
    "Today Morning"
...
elseif timestamp < 1week 
    "Few days ago"
elseif timestamp < 1month
    "Few weeks ago"
elseif timestamp < 6month
    "Few Months ago" 
...
else
    "Really really long time ago"
The Unknown
Not quite, my point is that the periods are not subsets of each other."yesterday" and "last wednesday" can both refer to the same period but only one is correct when today is Thursday
Roger Nolan
Not sure that's right. Yesterday is always nearer in time to today than "last Eednesday." If today is Thursday, then the previous 24 hours is yesterday, the 24 hours before that is "the day before yesterday" and the 24 hours before that "last monday",
Jane Sales
OK, a better example: at 5am, "6 hours ago" is more accurately "last night".
Roger Nolan
A: 

You may find the source from timeago useful. The description of the plugin is "a jQuery plugin that makes it easy to support automatically updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago")."

It's essentially a JavaScript port of Rail's distance_of_time_in_words function crammed into a jQuery plugin.

Zack Mulgrew
+1  A: 

needless to say (but i'll say it anyway) don't use a where loop that decrements 365 days per year even on 366 day leap years (or you'll find yourself in the ranks of the Zune developers)

here is a c# version:

http://tiredblogger.wordpress.com/2008/08/21/creating-twitter-esque-relative-dates-in-c/

rizzle
+6  A: 

You might want to look at Rail's distance_of_time_in_words function in date_helper.rb, which I've pasted below.

# File vendor/rails/actionpack/lib/action_view/helpers/date_helper.rb, line 59
def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {})
 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_minutes = (((to_time - from_time).abs)/60).round
 distance_in_seconds = ((to_time - from_time).abs).round

 I18n.with_options :locale => options[:locale], :scope => 'datetime.distance_in_words''datetime.distance_in_words' do |locale|
   case distance_in_minutes
     when 0..1
       return distance_in_minutes == 0 ?
              locale.t(:less_than_x_minutes, :count => 1) :
              locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds

       case distance_in_seconds
         when 0..4   then locale.t :less_than_x_seconds, :count => 5
         when 5..9   then locale.t :less_than_x_seconds, :count => 10
         when 10..19 then locale.t :less_than_x_seconds, :count => 20
         when 20..39 then locale.t :half_a_minute
         when 40..59 then locale.t :less_than_x_minutes, :count => 1
         else             locale.t :x_minutes,           :count => 1
       end

     when 2..44           then locale.t :x_minutes,      :count => distance_in_minutes
     when 45..89          then locale.t :about_x_hours,  :count => 1
     when 90..1439        then locale.t :about_x_hours,  :count => (distance_in_minutes.to_f / 60.0).round
     when 1440..2879      then locale.t :x_days,         :count => 1
     when 2880..43199     then locale.t :x_days,         :count => (distance_in_minutes / 1440).round
     when 43200..86399    then locale.t :about_x_months, :count => 1
     when 86400..525599   then locale.t :x_months,       :count => (distance_in_minutes / 43200).round
     when 525600..1051199 then locale.t :about_x_years,  :count => 1
     else                      locale.t :over_x_years,   :count => (distance_in_minutes / 525600).round
   end
 end
end
Andrew Cholakian
A: 

My company has this .NET library that does some of what you want in that it does very flexible date time parsing (including some relative formats) but it only does non-relative outputs.

BCS
A: 

I know expressing times like this has become quite popular lately, but please considering making it an option to switch been relative 'fuzzy' dates and normal absolute dates.

For example, it's useful to know that a comment was made 5 minutes ago, but it's less useful to tell me comment A was 4 hours ago and comment B was 9 hours ago when it's 11 AM and I'd rather know that comment A was written when someone woke up this morning and comment B was written by someone staying up late (assuming I know they are in my timezone).

-- EDIT: looking closer at your question you seem to have avoided this to some degree by referring to time of day instead of "X ago", but on the other hand, you may be giving a false impression if users are in different time zone, since your "this morning" may be in the middle of the night for the relevant user.

It might be cool to augment the times with relative time of day depending on the other user's timezone, but that assumes that users are willing to supply it and that it's correct.

pimlottc
That is what I'm trying to do. I don't want to just say "n hours ago" I want to be able to write the date in a human friendly format: "late afternoon on wednesday last week".
Roger Nolan
A: 

Are you looking for just a formatter, or also a parser? It's not clear from your question.

pimlottc
Just a formatter. a parser is a much more difficult natural languag eprocessing problem.
Roger Nolan
+2  A: 

There is a property in NSDateFormatter - "doesRelativeDateFormatting". It appears only in 10.6/iOS4.0 and later but it will format a date into a relative date in the correct locale.

From Apple's Documentation:

If a date formatter uses relative date formatting, where possible it replaces the date component of its output with a phrase—such as “today” or “tomorrow”—that indicates a relative date. The available phrases depend on the locale for the date formatter; whereas, for dates in the future, English may only allow “tomorrow,” French may allow “the day after the day after tomorrow,” as illustrated in the following example.

Code

The following is code that will print out a good number of the relative strings for a given locale.

NSLocale *locale = [NSLocale currentLocale];
//    NSLocale *locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"fr_FR"] autorelease];

NSDateFormatter *relativeDateFormatter = [[NSDateFormatter alloc] init];
[relativeDateFormatter setTimeStyle:NSDateFormatterNoStyle];
[relativeDateFormatter setDateStyle:NSDateFormatterMediumStyle];
[relativeDateFormatter setDoesRelativeDateFormatting:YES];
[relativeDateFormatter setLocale:locale];

NSDateFormatter *normalDateFormatter = [[NSDateFormatter alloc] init];
[normalDateFormatter setTimeStyle:NSDateFormatterNoStyle];
[normalDateFormatter setDateStyle:NSDateFormatterMediumStyle];
[normalDateFormatter setDoesRelativeDateFormatting:NO];
[normalDateFormatter setLocale:locale];

NSString * lastUniqueString = nil;

for ( NSTimeInterval timeInterval = -60*60*24*400; timeInterval < 60*60*24*400; timeInterval += 60.0*60.0*24.0 )
{
    NSDate * date = [NSDate dateWithTimeIntervalSinceNow:timeInterval];

    NSString * relativeFormattedString = [relativeDateFormatter stringForObjectValue:date];
    NSString * formattedString = [normalDateFormatter stringForObjectValue:date];

    if ( [relativeFormattedString isEqualToString:lastUniqueString] || [relativeFormattedString isEqualToString:formattedString] )
        continue;

    NSLog( @"%@", relativeFormattedString );
    lastUniqueString = relativeFormattedString;
}

Notes:

  • A locale is not required
  • There are not that many substitutions for English. At the time of writing there are: "Yesterday, Today, Tomorrow". Apple may include more in the future.
  • It's fun to change the locale and see what is available in other languages (French has a few more than English, for example)
  • If on iOS, you might want to subscribe to UIApplicationSignificantTimeChangeNotification

Interface Builder

You can set the "doesRelativeDateFormatting" property in Interface Builder:

  • Select your NSDateFormatter and choose the "Identity Inspector" tab of the Inspector Palette (the last one [command-6]).
  • Under the sub-section named "User Defined Runtime Attributes", you can add your own value for a key on the selected object (in this case, your NSDateFormatter instance). Add "doesRelativeDateFormatting", choose a "Boolean" type, and make sure it's checked.
  • Remember: It may look like it didn't work at all, but that might because there are only a few substituted values for your locale. Try at least a date for Yesterday, Today, and Tomorrow before you decide if it's not set up right.
Michael Bishop