Fuzzy Time
Recently I discovered a need (read: desire that was relatively easily fulfilled, thus necessary) for a web app to have “fuzzy time” display capabilities. You’ve got the ironclad, all-superior, well-known, never-fail Unix timestamp (e.g. 1126935037). Backbone of a LOT software (and for good reason), but useless for display. You’ve got the MySQL datetime, which is really nifty, but even for seasoned developers, can be a bit harsh on the eyes. PHP is the winner by far. With it’s function called date(), you can print strings like “Monday, November 17th, 2008 at 10:21am” which is much nicer. Isn’t abstraction wonderful?
Let’s unpack this. In each instance you’re moving away from a single, absolute integer, to an easier-on-the-eyes formatted string. But why stop there? That’s where “fuzzy time” functions come into play. They take a given time value (timestamp, datetime, or something to that effect) and, with some very simple logic… lay it out in a much easier-to-swallow way, such as “today at 8:16am,” “yesterday around 1:30pm,” and “about two weeks ago.” A buddy of mine used to have a fuzzy time widget for his (Fedora) desktop that ran the gambit, abstracting time as specifically as “early morning” to as generally as “early winter.” Perhaps that’s going too far, perhaps not. You be the judge - I thought it was pretty funny.
Back to the topic at hand. I’ve always admired Facebook’s use of fuzzy time for status updates.
At certain times of day and night (and certain levels of non-sobriety) your users can’t even parse some of the nicest date() output. So, I went ahead and wrote a very simple PHP function to take things just one level higher up the ole’ abstraction ladder. (Note that line 2 only existed in my example script to save me typing.)
function fuzzy_time( $time ) { echo "\"$time\" is: "; if ( ( $time = strtotime( $time ) ) == false ) { return 'an unknown time'; } define( 'NOW', time() ); define( 'ONE_MINUTE', 60 ); define( 'ONE_HOUR', 3600 ); define( 'ONE_DAY', 86400 ); define( 'ONE_WEEK', ONE_DAY*7 ); define( 'ONE_MONTH', ONE_WEEK*4 ); define( 'ONE_YEAR', ONE_MONTH*12 ); // sod = start of day :) $sod = mktime( 0, 0, 0, date( 'm', $time ), date( 'd', $time ), date( 'Y', $time ) ); $sod_now = mktime( 0, 0, 0, date( 'm', NOW ), date( 'd', NOW ), date( 'Y', NOW ) ); // used to convert numbers to strings $convert = array( 1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four', 5 => 'five', 6 => 'six', 7 => 'seven', 8 => 'eight', 9 => 'nine', 10 => 'ten', 11 => 'eleven' ); // today if ( $sod_now == $sod ) { if ( $time > NOW-(ONE_MINUTE*3) ) { return 'just a moment ago'; } else if ( $time > NOW-(ONE_MINUTE*7) ) { return 'a few minutes ago'; } else if ( $time > NOW-(ONE_HOUR) ) { return 'less than an hour ago'; } return 'today at ' . date( 'g:ia', $time ); } // yesterday if ( ($sod_now-$sod) < = ONE_DAY ) { if ( date( 'i', $time ) > (ONE_MINUTE+30) ) { $time += ONE_HOUR/2; } return 'yesterday around ' . date( 'ga', $time ); } // within the last 5 days if ( ($sod_now-$sod) < = (ONE_DAY*5) ) { $str = date( 'l', $time ); $hour = date( 'G', $time ); if ( $hour < 12 ) { $str .= ' morning'; } else if ( $hour < 17 ) { $str .= ' afternoon'; } else if ( $hour < 20 ) { $str .= ' evening'; } else { $str .= ' night'; } return $str; } // number of weeks (between 1 and 3)... if ( ($sod_now-$sod) < (ONE_WEEK*3.5) ) { if ( ($sod_now-$sod) < (ONE_WEEK*1.5) ) { return 'about a week ago'; } else if ( ($sod_now-$sod) < (ONE_DAY*2.5) ) { return 'about two weeks ago'; } else { return 'about three weeks ago'; } } // number of months (between 1 and 11)... if ( ($sod_now-$sod) < (ONE_MONTH*11.5) ) { for ( $i = (ONE_WEEK*3.5), $m=0; $i < ONE_YEAR; $i += ONE_MONTH, $m++ ) { if ( ($sod_now-$sod) <= $i ) { return 'about ' . $convert[$m] . ' month' . (($m>1)?'s':'') . ' ago'; } } } // number of years... for ( $i = (ONE_MONTH*11.5), $y=0; $i < (ONE_YEAR*10); $i += ONE_YEAR, $y++ ) { if ( ($sod_now-$sod) <= $i ) { return 'about ' . $convert[$y] . ' year' . (($y>1)?'s':'') . ' ago'; } } // more than ten years... return 'more than ten years ago'; }
The original base-function I used was one that I found on PHPClasses by a fellow named Andrew Collington. The code was in a class method for getting fuzzy file times. I took that code, broke it out into a function, and massaged the heck out of it. The function takes as it’s first parameter any value accepted by the PHP function strtotime(). I chose that because of it’s impressive flexibility. The function I ended up with only goes back to a ten-year period. I figure, if I ever write anything that has records older than that, I can update it pretty easily. Here’s sample output for all of the different intervals the function currently supports:
"now" is: just a moment ago
"-5 minutes" is: a few minutes ago
"-30 minutes" is: less than an hour ago
"-24 hours" is: yesterday around 10am
"-48 hours" is: Saturday morning
"-90 hours" is: Thursday afternoon
"-1 week" is: about a week ago
"-2 weeks" is: about three weeks ago
"-3 weeks" is: about three weeks ago
"-4 weeks" is: about one month ago
"-5 weeks" is: about one month ago
"-3 months" is: about three months ago
"-9 months" is: about nine months ago
"-13 months" is: about one year ago
"-76 months" is: about six years ago
"-9 years" is: about nine years ago
"-26 years" is: more than ten years ago
I was suprised at how much character and youth it gave the site. Thanks Andrew and Facebook!
November 17 2008 | Communication and Development | No Comments »