Fuzzy date algorithm - algorithm

I'm looking for a fuzzy date algorithm. I just started writing one and realised what a tedious task 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.

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.

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.

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

So, here is the category I wrote on NSDate for those who are still interested. The problem is one of those that becomes a little quixotic. It is basically a huge switch statment (although I implemented it in a series of cascading if()s to keep it more readable.
For each time period I then select from a random set of ways of telling the time.
All in all, this delighted a few of our users but I'm not sure it was worth the effort.
NSTimeInterval const kTenSeconds = (10.0f );
NSTimeInterval const kOneMinute = (60.0f);
NSTimeInterval const kFiveMinutes = (5.0f*60.0f);
NSTimeInterval const kFifteenMinutes = (15.0f*60.0f) ;
NSTimeInterval const kHalfAnHour = (30.0f*60.0f) ;
NSTimeInterval const kOneHour = 3600.0f; // (60.0f * 60.0f);
NSTimeInterval const kHalfADay = (3600.0f * 12.0f);
NSTimeInterval const kOneDay = (3600.0f * 24.0f);
NSTimeInterval const kOneWeek = (3600.0f * 24.0f * 7.0f);
#implementation NSDate (Fuzzy)
-(NSString*)fuzzyStringRelativeToNow;
{
static NSArray* secondsStrings;
static NSArray* minuteStrings;
static NSArray* fiveMinuteStrings;
static NSArray* halfHourStrings;
static NSArray* earlyMonthStrings;
NSTimeInterval timeFromNow = [self timeIntervalSinceNow];
if((timeFromNow < 0)) // In the past
{
timeFromNow = - timeFromNow;
if ( (timeFromNow < kTenSeconds))
{
if(!secondsStrings)
{
secondsStrings = [[NSArray arrayWithObjects:#"just now",
//#"a few seconds ago",
//#"right this instant",
#"moments ago",
nil] retain];
}
unsigned int index = random() % ([secondsStrings count] - 1);
return [secondsStrings objectAtIndex:index];
}
if ( (timeFromNow < kOneMinute))
{
if(!minuteStrings)
{
minuteStrings = [[NSArray arrayWithObjects:#"just now",
#"very recently",
#"in the last minute",
nil] retain];
}
unsigned int index = random() % ([minuteStrings count] - 1);
return [minuteStrings objectAtIndex:index];
}
if (timeFromNow < kFiveMinutes)
{
if(!fiveMinuteStrings)
{
fiveMinuteStrings = [[NSArray arrayWithObjects:#"just now",
#"very recently",
//#"in the last minute",
#"a few minutes ago",
//#"in the last five minutes",
nil] retain];
}
unsigned int index = random() % ([fiveMinuteStrings count] - 1);
return [fiveMinuteStrings objectAtIndex:index];
}
if (timeFromNow < kFifteenMinutes)
{
return #"in the last 15 minutes";
}
if (timeFromNow < kHalfAnHour)
{
if(!halfHourStrings)
{
halfHourStrings = [[NSArray arrayWithObjects:#"in the last half hour",
//#"in the last half an hour",
#"in the last 30 minutes",
//#"about half an hour ago",
#"fairly recently",
nil] retain];
}
unsigned int index = random() % ([halfHourStrings count] - 1);
return [halfHourStrings objectAtIndex:index];
}
if (timeFromNow < kOneHour)
{
return #"in the last hour";
}
if ((timeFromNow < (kOneHour + kFiveMinutes)) && (timeFromNow > (kOneHour - kFiveMinutes)))
{
return #"about an hour ago";
}
if((timeFromNow < ((kOneHour*2.0f) + kFiveMinutes ))&& (timeFromNow > ((kOneHour*2.0f) - kFiveMinutes)))
{
return #"a couple of hours ago";
}
// Now we're over an hour, we need to calculate a few specific dates to compare against
NSDate *today = [NSDate date];
NSCalendar *gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
NSUInteger unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit;
NSDateComponents* todayComponents = [gregorian components:unitFlags fromDate:today];
todayComponents.hour = 12;
NSDate* noonToday = [gregorian dateFromComponents:todayComponents];
NSTimeInterval timeSinceNoonToday = [self timeIntervalSinceDate:noonToday];
if (timeSinceNoonToday > 0) // sometime since noon
{
if (timeSinceNoonToday > kOneHour * 9) // i.e. after 9pm today
return #"earlier tonight";
if (timeSinceNoonToday > kOneHour * 7) // i.e. after 7pm today
return #"earlier this evening";
if (timeSinceNoonToday < kOneHour * 1) // between noon and 1pm
return #"early this afternoon";
return #"this afternoon";
}
NSTimeInterval timeSinceMidnight = kHalfADay -timeSinceNoonToday; // Note sign is reversed.
if ((timeSinceNoonToday < 0) & (timeSinceNoonToday > -kHalfADay)) // between midnight and noon today
{
if (timeSinceMidnight < kFiveMinutes)
return #"around midnight";
if (timeSinceMidnight < kOneHour * 2) // up to 2am
return #"very early this morning";
if (timeSinceMidnight < kOneHour * 5) // up to 5am
return #"early this morning";
else if (timeSinceMidnight < kOneHour * 11)
return #"late this morning";
else
return #"this morning";
}
// NSTimeInterval timeSinceNoonYesterday = timeSinceNoonToday - kOneDay;
// timeSinceMidnight = -timeSinceMidnight;
if (timeSinceMidnight < kOneHour * 24) // not the day before...
{
if (timeSinceMidnight < kFiveMinutes)
return #"around midnight";
if (timeSinceMidnight < kFifteenMinutes)
return #"just before midnight";
if (timeSinceMidnight < kOneHour * 2) // after 10pm
return #"late last night";
if (timeSinceMidnight < kOneHour * 5) // After 7
return #"yesterday evening";
else if (timeSinceMidnight < kOneHour * 7)
return #"yesterday evening"; // after 5pm
else if (timeSinceMidnight < kOneHour * 7)
return #"yesterday evening"; // after 5pm
else if (timeSinceMidnight < kOneHour * 10)
return #"yesterday afternoon"; // after 5pm
else if (timeSinceMidnight < kOneHour * 12)
return #"early yesterday afternoon"; // before 1pm
else if (timeSinceMidnight < kOneHour * 13)
return #"late yesterday morning"; // after 11m
else if (timeSinceMidnight < kOneHour * 17)
return #"yesterday morning";
else
return #"early yesterday morning";
}
NSDateFormatter* formatter = [[[NSDateFormatter alloc] init] autorelease];
int integerSeconds = timeSinceMidnight;
int integerDay = kOneDay;
int secondsIntoDay = integerSeconds % integerDay;
NSString* formatString = #"last %#";
if (timeFromNow < kOneWeek)
{
if (secondsIntoDay < kFifteenMinutes)
formatString = #"around midnight on %#";
//else if (secondsIntoDay < kFifteenMinutes)
// formatString = #"just before midnight on %#";
else if (secondsIntoDay < kOneHour * 2) // after 10pm
formatString = #"late on %# night";
else if (secondsIntoDay < kOneHour * 5) // After 7
formatString = #"on %# evening";
else if (secondsIntoDay < kOneHour * 10)
formatString = #"on %# afternoon"; // after 5pm
else if (secondsIntoDay < kOneHour * 12)
formatString = #"early on %# afternoon"; // before 1pm
else if (secondsIntoDay < kOneHour * 13)
formatString = #"late on %# morning"; // after 11am
else if (secondsIntoDay < kOneHour * 17)
formatString = #"on %# morning";
else if (secondsIntoDay < kOneHour * 24) // not the day before...
formatString = #"early on %# morning";
[formatter setDateFormat:#"EEEE"]; /// EEEE is long format of day of the week see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
}
//formatString = #"on %# the week before last";
/*if (secondsIntoDay < kOneHour * 2) // after 10pm
formatString = #"early on %# the week before last";
else if (timeSinceMidnight > kOneHour * 13)
formatString = #"late on %# the week before last"; // after 11m*/
//if (timeFromNow < kOneWeek * 2)
//{
// [formatter setDateFormat:#"EEE"]; /// EEE is short format of day of the week see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
// return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
//}
if (timeFromNow < kOneWeek * 2)
{
return #"the week before last";
}
NSDateComponents* myComponents = [gregorian components:unitFlags fromDate:self];
int monthsAgo = myComponents.month - todayComponents.month;
int yearsAgo = myComponents.year - todayComponents.year;
if (yearsAgo == 0)
{
if (monthsAgo == 0)
{
if(myComponents.day > 22)
return #"late this month";
if(myComponents.day < 7)
{
if(!earlyMonthStrings)
{
earlyMonthStrings = [[NSArray arrayWithObjects:#"earlier this month",
//#"at the beginning of the month",
#"early this month",
nil] retain];
}
unsigned int index = random() % ([earlyMonthStrings count] - 1);
return [earlyMonthStrings objectAtIndex:index];
}
return #"earlier this month";
}
if (monthsAgo == 1)
{
if(myComponents.day > 22)
return #"late last month";
if(myComponents.day < 7)
return #"early last month";
return #"last month";
}
formatString = #"in %# this year";
/*if(myComponents.day > 22)
formatString = #"late in %# this year";
if(myComponents.day < 7)
formatString = #"early in %# this year";*/
[formatter setDateFormat:#"MMMM"]; /// MMM is longformat of month see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
}
if (yearsAgo == 1)
{
formatString = #"in %# last year";
/*if(myComponents.day > 22)
formatString = #"late in %# last year";
if(myComponents.day < 7)
formatString = #"late in %# last year";*/
[formatter setDateFormat:#"MMM"]; /// MMM is longformat of month see: http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns
return [NSString stringWithFormat:formatString, [formatter stringFromDate: self]];
}
// int daysAgo = integerSeconds / integerDay;
// Nothing yet...
[formatter setDateStyle:kCFDateFormatterMediumStyle];
//[formatter setTimeStyle:kCFDateFormatterShortStyle];
return [NSString stringWithFormat:#"on %#",[formatter stringFromDate: self]];
}
else
if(timeFromNow > 0) // The future
{
AICLog(kErrorLogEntry, #"FuzzyDates: Time marked as in the future: referenced date is %#, local time is %#", self, [NSDate date]);
return #"moments ago";
}
else
return #"right now"; // this seems unlikely.
return [self description]; // should never get here.
}
sorry it took so long to post this...

This is based on code in the Pretty and Humane date & time threads. I added handling for "last Monday, 5pm", because I like that more than x days ago. This handles past and future up to centuries. I am keen on the internationalization aspect so this needs a lot more work eventually. Calculations are in the local time zone.
public static class DateTimePretty
{
private const int SECOND = 1;
private const int MINUTE = 60 * SECOND;
private const int HOUR = 60 * MINUTE;
private const int DAY = 24 * HOUR;
private const int WEEK = 7 * DAY;
private const int MONTH = 30 * DAY;
private const int YEAR = 365;
const string now = "just now";
const string secondsFuture = "in {0} seconds", secondsPast = "{0} seconds ago";
const string minuteFuture = "in about a minute", minutePast = "about a minute ago";
const string minutesFuture = "in about {0} minutes", minutesPast = "about {0} minutes ago";
const string hourFuture = "in about an hour", hourPast = "about an hour ago";
const string hoursFuture = "in about {0} hours", hoursPast = "about {0} hours ago";
const string tomorrow = "tomorrow, {0}", yesterday = "yesterday, {0}";
const string nextDay = "{0}", nextWeekDay = "next {0}", lastDay = "last {0}";
//const string daysFuture = "in about {0} days", daysPast = "about {0} days ago";
const string weekFuture = "in about a week", weekPast = "about a week ago";
const string weeksFuture = "in about {0} weeks", weeksPast = "about {0} weeks ago";
const string monthFuture = "in about a month", monthPast = "about a month ago";
const string monthsFuture = "in about {0} months", monthsPast = "about {0} months ago";
const string yearFuture = "in about a year", yearPast = "about a year ago";
const string yearsFuture = "in about {0} years", yearsPast = "about {0} years ago";
const string centuryFuture = "in about a century", centuryPast = "about a century ago";
const string centuriesFuture = "in about {0} centuries", centuriesPast = "about {0} centuries ago";
/// <summary>
/// Returns a pretty version of the provided DateTime: "42 years ago", or "in 9 months".
/// </summary>
/// <param name="dateTime">DateTime in local time format, not Utc</param>
/// <returns>A pretty string</returns>
public static string GetPrettyDate(DateTime dateTime)
{
DateTime dateTimeNow = DateTime.Now;
bool isFuture = (dateTimeNow.Ticks < dateTime.Ticks);
var ts = isFuture ? new TimeSpan(dateTime.Ticks - dateTimeNow.Ticks) : new TimeSpan(dateTimeNow.Ticks - dateTime.Ticks);
double delta = ts.TotalSeconds;
if (delta < 10)
return now;
if (delta < 1 * MINUTE)
return isFuture ? string.Format(secondsFuture, ts.Seconds) : string.Format(secondsPast, ts.Seconds);
if (delta < 2 * MINUTE)
return isFuture ? minuteFuture : minutePast;
if (delta < 45 * MINUTE)
return isFuture ? string.Format(minutesFuture, ts.Minutes) : string.Format(minutesPast, ts.Minutes);
if (delta < 2 * HOUR)
return isFuture ? hourFuture : hourPast;
if (delta < 7 * DAY)
{
string shortTime = DateTimeFormatInfo.CurrentInfo.ShortTimePattern;
string shortWeekdayTime = "dddd, " + shortTime;
int dtDay = (int) dateTime.DayOfWeek;
int nowDay = (int) dateTimeNow.DayOfWeek;
if (isFuture)
{
if (dtDay == nowDay)
{
if (delta < DAY)
return string.Format(hoursFuture, ts.Hours);
else
return string.Format(nextWeekDay, dateTime.ToString(shortWeekdayTime));
}
else if (dtDay - nowDay == 1 || dtDay - nowDay == -6)
return string.Format(tomorrow, dateTime.ToString(shortTime));
else
return string.Format(nextDay, dateTime.ToString(shortWeekdayTime));
}
else
{
if (dtDay == nowDay)
{
if (delta < DAY)
return string.Format(hoursPast, ts.Hours);
else
return string.Format(lastDay, dateTime.ToString(shortWeekdayTime));
}
else if (nowDay - dtDay == 1 || nowDay - dtDay == -6)
return string.Format(yesterday, dateTime.ToString(shortTime));
else
return string.Format(lastDay, dateTime.ToString(shortWeekdayTime));
}
}
//if (delta < 7 * DAY)
// return isFuture ? string.Format(daysFuture, ts.Days) : string.Format(daysPast, ts.Days);
if (delta < 4 * WEEK)
{
int weeks = Convert.ToInt32(Math.Floor((double) ts.Days / 30));
if (weeks <= 1)
return isFuture ? weekFuture : weekPast;
else
return isFuture ? string.Format(weeksFuture, weeks) : string.Format(weeksPast, weeks);
}
if (delta < 12 * MONTH)
{
int months = Convert.ToInt32(Math.Floor((double) ts.Days / 30));
if (months <= 1)
return isFuture ? monthFuture : monthPast;
else
return isFuture ? string.Format(monthsFuture, months) : string.Format(monthsPast, months);
}
// Switch to days to avoid overflow
delta = ts.TotalDays;
if (delta < 100 * YEAR)
{
int years = Convert.ToInt32(Math.Floor((double) ts.TotalDays / 365.25));
if (years <= 1)
return isFuture ? yearFuture : yearPast;
else
return isFuture ? string.Format(yearsFuture, years) : string.Format(yearsPast, years);
}
else
{
int centuries = Convert.ToInt32(Math.Floor((double) ts.TotalDays / 365.2425));
if (centuries <= 1)
return isFuture ? centuryFuture : centuryPast;
else
return isFuture ? string.Format(centuriesFuture, centuries) : string.Format(centuriesPast, centuries);
}
}
}

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"

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.

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/

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.

I was not happy with the solution in the other question. So made my own using the Date time class. IMO, its cleaner. In my tests it worked like as I wanted. Hope this helps someone.
DateTime now = DateTime.Now;
long nowticks = now.Ticks;
long thenticks = dt.Ticks;
long diff = nowticks - thenticks;
DateTime n = new DateTime(diff);
if (n.Year > 1)
{
return n.Year.ToString() + " years ago";
}
else if (n.Month > 1)
{
return n.Month.ToString() + " months ago";
}
else if (n.Day > 1)
{
return n.Day.ToString() + " days ago";
}
else if (n.Hour > 1)
{
return n.Hour.ToString() + " hours ago";
}
else if (n.Minute > 1)
{
return n.Minute.ToString() + " minutes ago";
}
else
{
return n.Second.ToString() + " seconds ago";
}

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.

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.

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.

Check out Chrono for a Javascript heuristic date parser.
Chrono supports most date and time formats, such as:
Today, Tomorrow, Yesterday, Last Friday, etc
17 August 2013 - 19 August 2013
This Friday from 13:00 - 16.00
5 days ago
Sat Aug 17 2013 18:40:39 GMT+0900 (JST)
2014-11-30T08:15:30-05:30
https://github.com/wanasit/chrono

Related

How to sort a list of timings like this [ 2:00 AM , 10:00 AM , 6:00 PM ] in dart-flutter?

I'm working on a flutter app, one of its features is to add your drug dose timings to get a reminder to take your drug. and I need to sort the timings to get the 'next_dose' to appear here :
https://drive.google.com/file/d/1j5KrRbDj0J28_FrMKy7dazk4m9dw202d/view?usp=sharing
this is an example of the list which I want to sort
[8:30 AM, 3:30 PM, 9:30 AM, 7:00 AM]
function I made to get the greater between 2 timings
int greater(String element,String element2){
var hour = element.toString().split(':')[0];
var hour2 = element2.toString().split(':')[0];
var minute = element.toString().split(':')[1].split(' ')[0];
var minute2 = element2.toString().split(':')[1].split(' ')[0];
var day = element.toString().split(':')[1].split(' ')[1];
var day2 = element2.toString().split(':')[1].split(' ')[1];
if(day == 'AM' && day2 == 'PM')
return -1;
else if(day2 == 'AM' && day == 'PM')
return 1;
else if(day == day2)
{
if(int.parse(hour) > int.parse(hour2)) return -1;
else if(int.parse(hour) < int.parse(hour2)) return 1;
else{
if(int.parse(minute) > int.parse(minute2))
return 1;
else
return -1;
}
}
here I tried to use the function to sort the list'Dose'
dose.sort((a,b)=>greater(a,b));
Instead of creating a sort callback with a lot of complicated logic, it'd be simpler if the callback parsed the time strings either into an int or into a DateTime object and compared those. For example:
/// Parses a time of the form 'hh:mm AM' or 'hh:mm PM' to a 24-hour
/// time represented as an int.
///
/// For example, parses '3:30 PM' as 1530.
int parseTime(String time) {
var components = time.split(RegExp('[: ]'));
if (components.length != 3) {
throw FormatException('Time not in the expected format: $time');
}
var hours = int.parse(components[0]);
var minutes = int.parse(components[1]);
var period = components[2].toUpperCase();
if (hours < 1 || hours > 12 || minutes < 0 || minutes > 59) {
throw FormatException('Time not in the expected format: $time');
}
if (hours == 12) {
hours = 0;
}
if (period == 'PM') {
hours += 12;
}
return hours * 100 + minutes;
}
void main() {
var list = ['8:30 AM', '3:30 PM', '9:30 AM', '7:00 AM'];
list.sort((a, b) => parseTime(a).compareTo(parseTime(b)));
print(list); // Prints: [7:00 AM, 8:30 AM, 9:30 AM, 3:30 PM]
}
Alternatively, you can use package:intl and DateFormat.parse to easily parse strings into DateTime objects.
Here we get the time slot by passing the start and end time duration.
List<String> createTimeSlot(
Duration startTime, Duration endTime, BuildContext context,
{Duration step = const Duration(minutes: 30)} // Gap between interval
) {
var timeSlot = <String>[];
var hourStartTime = startTime.inHours;
var minuteStartTime = startTime.inMinutes.remainder(60);
var hourEndTime = endTime.inHours;
var minuteEndTime = endTime.inMinutes.remainder(60);
do {
timeSlot.add(TimeOfDay(hour: hourStartTime, minute: minuteStartTime)
.format(context));
minuteStartTime += step.inMinutes;
while (minuteStartTime >= 60) {
minuteStartTime -= 60;
hourStartTime++;
}
} while (hourStartTime < hourEndTime ||
(hourStartTime == hourEndTime && minuteStartTime <= minuteEndTime));
debugPrint("Number of slot $timeSlot");
return timeSlot;
}
Function call
createTimeSlot(Duration(hours: 1, minutes: 30),
Duration(hours: 3, minutes: 30), context);
Output:
Number of slot [1:30 AM, 2:00 AM, 2:30 AM, 3:00 AM, 3:30 AM]

How to add number of days to a custom Date captured from Correlation function?

I am looking for logic which can add couple of days to a custom date(not current date)
Below is Correlation function:
web_reg_save_param("Recommended_Date",
"LB=\"start\":\"",
"RB/DIG=T##:##:##\",",
"Ord=1",
"Search=Body",
LAST);
I want to add +21 days to Recommended_Date parameter. I tried doing below thing but no luck
lr_save_datetime("%Y-%M-%D", lr_eval_string("{Recommended_Date}") + (ONE_DAY*21), "New_Date");
Can anyone please assist me.
One of our engineers prepared this example for you:
int diff_days(char * dateString, char * dateFormat) {
int year, month, day;
struct tm info;
double delta;
double days=0;
time_t today;
time(&today);
sscanf(dateString, dateFormat, &year, &month, &day);
info.tm_year = year - 1900;
info.tm_mon = month - 1;
info.tm_mday = day;
// info.tm_hour = 0;
// info.tm_min = 0;
// info.tm_sec = 0;
info.tm_isdst = -1;
mktime(&info);
delta = difftime(mktime(&info),today);
if (delta >= 0) {
days = difftime(mktime(&info),today)/ 86400.0 +1;
} else {
days = difftime(mktime(&info),today)/ 86400.0;
}
return (int)days;
}
Action()
{
int plus;
lr_save_string("2020-09-01","D2");
plus = diff_days(lr_eval_string("{D2}"),"%d-%d-%d");
lr_save_datetime("%Y-%m-%d", DATE_NOW + ONE_DAY*(21+plus), "New_Date");
lr_save_string("2020/04/05","D2");
plus = diff_days(lr_eval_string("{D2}"),"%d/%d/%d");
lr_save_datetime("%Y/%m/%d", DATE_NOW + ONE_DAY*(21+plus), "New_Date");
return 0;
}

Calculating Total Job Experience

I need to calculate the total job experience as year value. Users add experiences with starting and ending dates to their resumes, just like Linkedin. But there is no any certain pattern. For instance;
A user may have a resume like that;
Experience 2
08.2012 - 01.2015
Experience 1
01.2011 - 01.2013
The user started their second experience while the first hasn't finished yet. So resumes may have many overlapping experiences. Overlapping also may occur between more than 2. So I need to consider many cases.
I tried to visualise the experience and year relationship for you.
I just need to develop an algorithm covering all the cases for this issue.
Sort by start date
start at the beginning and accumulate overlapping experiences (i.e. treat as one)
e.g. (Jan 2012, Jan 2015), (Jan 2014, Dec 2016) overlap, so we treat it as a single experience
This "super experience" begins at the start of the first, and ends at the end of the last; (Jan 2012, Dec 2016)
This is assuming that there can be gaps in experience, so we don't want to treat the entire history as one long "super experience"
I just did this
Sort the experiences by Begin Date.
Use 2 variables, that represent the begin of work and the end called Begin
and End.
If end and begin are empty it means that is the first date, we filled with the first experience, and we calculate the month count between the begin and end of this experience and and then this months count add to our months accumulator.
if the end and begin date of next experience is not between our begin and end variables, we calculate the months count from the experience and then add it to our accumulator.
If the begin date of next experience is between our begin and end variables, we calculate the months count between our end date as begin and the experience end date as end, and then that difference add to our months accumulator.
as you can see every time we set our new end if the experience end is greater than ours.
at the end we get the months, if you divide by 12 you get the years exactly you can round it if you want
NOTE: I Mapped the experiences, if not have End it means that is equivalent to NOW
you can see all the code below
function monthDiff(d1, d2) {
var m = (d1.getFullYear() - d2.getFullYear()) * 12 +
(d1.getMonth() - d2.getMonth());
if (d1.getDate() < d2.getDate()) --m;
return m;
}
function dateCheck(from, to, check) {
var fDate, lDate, cDate;
fDate = Date.parse(from);
lDate = Date.parse(to);
cDate = Date.parse(check);
if (cDate <= lDate && cDate >= fDate) {
return true;
}
return false;
}
function calculateYearsOfExperience(experiences) {
if (!experiences) return 0;
let months = 0;
let now = new Date();
let sorted = experiences
.sort((a, b) => {
return new Date(a.begin) - new Date(b.begin);
})
.map(experience => {
if (!experience.end) experience.end = now;
return { begin: experience.begin, end: experience.end };
});
let begin;
let end;
for (var i in sorted) {
let dif = 0;
if (!end && !begin) {
dif = monthDiff(sorted[i].begin, sorted[i].end);
begin = sorted[i].begin;
end = sorted[i].end;
} else if (
!dateCheck(begin, end, sorted[i].begin) &&
!dateCheck(begin, end, sorted[i].end)
) {
dif = monthDiff(sorted[i].begin, sorted[i].end);
end = sorted[i].end;
} else if (dateCheck(begin, end, sorted[i].begin)) {
dif = monthDiff(end, sorted[i].end);
end = sorted[i].end;
}
months += dif;
}
return months / 12;
}
experiences = [
# (start date, end date),
(date(2012, 8, 1), date(2015, 1, 30)),
(date(2011, 1, 1), date(2013, 1, 30))
]
print(sum((end_date - start_date).days/365 for start_date, end_date in experiences))
Create an array full of 0. Element range: Start would be the very first hire date, last would be current date / last working date. For every year you have 12 elements that can be 0/1. After filling in the array count the elements that have the value 1. If you need to take into account the overlapping then ignore the 0/1 part and give +1 for every month worked.
This is c# code, but you'll get the steps in algorithm. It returns total number of months which you can convert into year and month format.
var sortedList = expList
.OrderBy(a => a.start_year)
.ThenBy(a => a.start_month)
.ThenBy(a => a.end_year)
.ThenBy(a => a.end_month)
.ToList();
int totalMonths = 0;
int totalDays = 0;
DateTime? prevEndDate = null;
foreach (var exp in sortedList)
{
if(exp.start_month != null && exp.start_year != null)
{
var startDate = new DateTime((int)exp.start_year, (int)exp.start_month, 1);
var endDate = (bool)exp.is_present ?
DateTime.Now :
new DateTime((int)exp.end_year, (int)exp.end_month, 1);
if (prevEndDate != null && prevEndDate > startDate)
{
startDate = (DateTime)prevEndDate;
}
var timespan = endDate.Subtract(startDate);
var tempdate = DateTime.MinValue + timespan;
totalMonths = totalMonths + (tempdate.Year - 1) * 12 + tempdate.Month - 1;
totalDays = totalDays + tempdate.Day - 1;
prevEndDate = endDate;
}
}
if (totalDays > 0)
{
totalMonths = totalMonths + 1;
}
public static function YearCalculation($id=null) {
if($id=="")
$employee_id=#Auth::guard('web_employee')->user()->id;
else
$employee_id=$id;
$employee_exp = Experience::where('employee_id',$employee_id)->orderBy("experience_from","ASC")->get();
//print_r($employee_exp ); exit;
$years = 0;
$months = 0;
$days = 0;
$sum=0;
$lastfromdate=0;
$last_endDate=0;
if(#$employee_exp!=null){
foreach(#$employee_exp as $experience){
$fdate = #$experience->experience_from;
if($experience->is_current==0){
$tdate = #$experience->experience_to;
}else{
$tdate = date("Y-m-d");
}
if($last_endDate != date("Y-m-d") ){
if( $fdate < $last_endDate ){
$start_date1 = strtotime($last_endDate);
$end_date1 = strtotime($tdate);
$interval1 = ($end_date1 - $start_date1)/60/60/24;
$sum +=$interval1;
}
else{
$start_date2 = strtotime($fdate);
$end_date2 = strtotime($tdate);
$interval2 = ($end_date2 - $start_date2)/60/60/24;
$sum +=$interval2;
}
if($last_endDate < $tdate){
$last_endDate=$tdate;
}
}
}
$years = ($sum / 365) ;
$years = floor($years);
$month = ($sum % 365) / 30.4166;
$month = floor($month);
$days = ($sum % 365) % 30.4166;
//$total=date_diff(0,$tmstamp);
//$total_year=$years.' years, ' .$month.' months and '.$days.($days==1?' day':' days');
$total_year=($years==0?'':($years==1?$years.' year':$years.' years and ')) .($month==0?'':($month==1?$month.($days>1?'+':'').' month':$month.($days>1?'+':'').' months'));
return $total_year;
}
}

Getting NSDate from JSON generated by WCF REST service

The following code works, but it feels dirty. Is there a more standard way to convert epoch date with offset into an NSDate?
- (NSDate *) dateFromJSONString: (NSString *) JSONString{
//expects JSON from .NET WCF Service in epoch ticks, ex:
//"timeScheduled":"\/Date(1348600140000+0100)\/"
NSString *date = [[JSONString stringByReplacingOccurrencesOfString:#"/Date(" withString:#""] stringByReplacingOccurrencesOfString:#")/" withString:#""];
NSString *offsetString = [date substringFromIndex:(date.length - 5)];
//convert to seconds
NSTimeInterval dateInterval = [date doubleValue] /1000;
//gets offset value in seconds - +0100 -> 100 -> 1 -> 3600
double offsetValue = ([offsetString doubleValue] / 100) * 60 * 60;
if ([[offsetString substringToIndex:1] isEqualToString:#"+"]) {
dateInterval = dateInterval + offsetValue;
}
else{
dateInterval = dateInterval - offsetValue;
}
NSDate *retVal = [[NSDate alloc]initWithTimeIntervalSince1970:dateInterval];
return retVal;
}
Try this
the original gist
#implementation NSDate (DotNetDates)
+(NSDate*) dateFromDotNet:(NSString*)stringDate{
NSDate *returnValue;
if ([stringDate isMemberOfClass:[NSNull class]]) {
returnValue=nil;
}
else {
NSInteger offset = [[NSTimeZone defaultTimeZone] secondsFromGMT];
returnValue= [[NSDate dateWithTimeIntervalSince1970:
[[stringDate substringWithRange:NSMakeRange(6, 10)] intValue]]
dateByAddingTimeInterval:offset];
}
return returnValue;
}
-(NSString*) dateToDotNet{
double timeSince1970=[self timeIntervalSince1970];
NSInteger offset = [[NSTimeZone defaultTimeZone] secondsFromGMT];
offset=offset/3600;
double nowMillis = 1000.0 * (timeSince1970);
NSString *dotNetDate=[NSString stringWithFormat:#"/Date(%.0f%+03d00)/",nowMillis,offset] ;
return dotNetDate;
}
#end
// /Date(-422928000000+0100)/
Docs:
DateTime values appear as JSON strings in the form of "/Date(700000+0500)/", where the first number (700000 in the example provided) is the number of milliseconds in the GMT time zone, regular (non-daylight savings) time since midnight, January 1, 1970. The number may be negative to represent earlier times. The part that consists of "+0500" in the example is optional and indicates that the time is of the Local kind - that is, should be converted to the local time zone on deserialization. If it is absent, the time is deserialized as Utc. The actual number ("0500" in this example) and its sign (+ or -) are ignored.
NSTimeInterval is always specified in seconds; it yields sub-millisecond precision over a range of 10,000 years.
+ (NSDate*) dateFromDotNet:(NSString *)stringDate{
if(stringDate==(id)[NSNull null])
return nil;
NSInteger ix0= [stringDate rangeOfString:#"("].location;
NSInteger ix1= [stringDate rangeOfString:#")"].location;
if(ix0==NSNotFound || ix1==NSNotFound)
#throw [NSException exceptionWithName:#"ExceptionName" reason:#"Invalid JSON data" userInfo:#{#"json":stringDate}];
NSRange range= NSMakeRange(ix0+1, ix1-ix0);
NSString *dateString= [stringDate substringWithRange:range];
// dateString: -422928000000+0100
NSCharacterSet *signs= [NSCharacterSet characterSetWithCharactersInString:#"+-"];
range= [dateString rangeOfCharacterFromSet:signs option:NSBackwardSearch];
// WCF will send 13 digit-long value for the time interval since 1970 (millisecond precision)
// whereas iOS works with 10 digit-long values (second precision), hence the divide by 1000
NSTimeInterval unixTime = [dateString doubleValue] / 1000;
if(range.location!=NSNotFound){
NSString *sign = [dateString substringWithRange:range];
NSString *off = [dateString substringFromIndex:range.location+1];
// gets offset value in seconds -+0100 -> 100 -> 1 -> 3600
double offset = ([off doubleValue] / 100) * 60 * 60;
if ([sign isEqualToString:#"+"])
unixTime+= offset;
else
unixTime-= offset;
}
NSDate *date= [NSDate dateWithTimeIntervalSince1970:unixTime];
return date;
}

How can I check that NSDate is between two times?

I am making a Cocoa application and I want it to be able to conduct a daily update. (this app is for personal use) I want it to be doing its update at a specific time everyday so I set my computer to wake up at a that time. I set a notification observer thing in my app and it will conduct this function if the app gets a computer did awake notification:
- (void) receiveWakeNote: (NSNotification*) note
{
[self conductBeginning];
}
What should I add to make sure that the wake up notice occurred between a specific time, say between 16:00 and 16:15, and then only execute the
[self conductBeginning];
line.
NSDate *now = [NSDate date];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit;
NSDateComponents *components = [[NSCalendar currentCalendar] components:units
fromDate:now];
if (components.hour == 16 && components.minute <= 15)
NSLog(#"it's time");
something like this:
NSDate * aDate = ...;
NSDate * bDate = ...;
NSTimeInterval a = [aDate timeIntervalSinceNow];
NSTimeInterval b = [bDate timeIntervalSinceNow];
NSTimeInterval min = a > b ? b : a;
NSTimeInterval max = a < b ? b : a;
NSTimeInterval now = CFAbsoluteTimeGetCurrent();
bool result = now >= min && now <= max;

Resources