facebook style "time ago" function

Coding Critique is the place to post source code for peer review by other members of DevNetwork. Any kind of code can be posted. Code posted does not have to be limited to PHP. All members are invited to contribute constructive criticism with the goal of improving the code. Posted code should include some background information about it and what areas you specifically would like help with.

Popular code excerpts may be moved to "Code Snippets" by the moderators.

Moderator: General Moderators

User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

facebook style "time ago" function

Post by s.dot »

Function:

Code: Select all

function time_passed($timestamp){
    //type cast, current time, difference in timestamps
    $timestamp      = (int) $timestamp;
    $current_time   = time();
    $diff           = $current_time - $timestamp;
    
    //intervals in seconds
    $intervals      = array (
        'year' => 31556926, 'month' => 2629744, 'week' => 604800, 'day' => 86400, 'hour' => 3600, 'minute'=> 60
    );
    
    //now we just find the difference
    if ($diff == 0)
    {
        return 'just now';
    }    

    if ($diff < 60)
    {
        return $diff == 1 ? $diff . ' second ago' : $diff . ' seconds ago';
    }        

    if ($diff >= 60 && $diff < $intervals['hour'])
    {
        $diff = floor($diff/$intervals['minute']);
        return $diff == 1 ? $diff . ' minute ago' : $diff . ' minutes ago';
    }        

    if ($diff >= $intervals['hour'] && $diff < $intervals['day'])
    {
        $diff = floor($diff/$intervals['hour']);
        return $diff == 1 ? $diff . ' hour ago' : $diff . ' hours ago';
    }    

    if ($diff >= $intervals['day'] && $diff < $intervals['week'])
    {
        $diff = floor($diff/$intervals['day']);
        return $diff == 1 ? $diff . ' day ago' : $diff . ' days ago';
    }    

    if ($diff >= $intervals['week'] && $diff < $intervals['month'])
    {
        $diff = floor($diff/$intervals['week']);
        return $diff == 1 ? $diff . ' week ago' : $diff . ' weeks ago';
    }    

    if ($diff >= $intervals['month'] && $diff < $intervals['year'])
    {
        $diff = floor($diff/$intervals['month']);
        return $diff == 1 ? $diff . ' month ago' : $diff . ' months ago';
    }    

    if ($diff >= $intervals['year'])
    {
        $diff = floor($diff/$intervals['year']);
        return $diff == 1 ? $diff . ' year ago' : $diff . ' years ago';
    }
}
Test:

Code: Select all

function test($ts){
    echo time_passed($ts) . '<br />';
} 
test(time());
test(time()-33);
test(time()-(60*17));
test(time()-((60*60*2) + (60*55)));
test(time()-(60*60*3+60));
test(strtotime('-1 day'));
test(strtotime('-13 days'));
test(strtotime('-25 days'));
test(strtotime('July 23 2009'));
test(strtotime('November 18 1999'));
test(strtotime('March 1 1937'));
Results:

Code: Select all

just now33 
seconds ago
17 minutes ago
2 hours ago
3 hours ago
1 day ago
1 week ago
3 weeks ago
7 months ago
10 years ago
72 years ago
It works well for my purposes, but it feels ugly with all the if{}s. But I can't think of a better way to do it. Approximate timing is OK with me, I don't need to factor in leap years to make the months count exactly correct. But I did use accurate seconds for seconds in a month and seconds in a year.
Last edited by Weirdan on Thu Dec 02, 2010 1:43 am, edited 1 time in total.
Reason: fixed formatting
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
Benjamin
Site Administrator
Posts: 6930
Joined: Sun May 19, 2002 10:24 pm

Re: facebook style "time ago" function

Post by Benjamin »

Nice!

You may want to look into using comparison operators in a switch. That would clean it up a little.

http://www.php.net/manual/en/control-st ... .php#93342

Also: use braces on all your if statements!
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Re: facebook style "time ago" function

Post by Luke »

I agree with astions. Always use braces with your if statements. And if you absolutely MUST leave out the braces, put it all on one line:

Code: Select all

if ($something) $foo = "bar";
Obviously this is a question of taste, so leave it if you want, but I think most developers prefer that you not do that.

Also, I wrote a view helper with similar functionality a while back. I thought you might be able to take some inspiration from it. Or maybe not. Either way, here it is:

Code: Select all

<?php
/**
 * Zend View Helper for "humanizing" dates and numbers.
 *
 * @package ImpSoft
 * @copyright Luke Visinoni (luke.visinoni@gmail.com)
 * @author Luke Visinoni (luke.visinoni@gmail.com)
 * @license GNU Lesser General Public License
 */
class ImpSoft_View_Helper_Humanize { 
    const DAYFORMAT = 'M d, Y';
    const TIMEFORMAT = 'g:ia';
    const DATEFORMAT = 'm/d/Y h:i:sa';    

    public function humanize() {
            return $this;
    }
    /**
     * Converts an integer to its ordinal as a string. 1 is '1st', 2 is '2nd', etc.
     * @todo Optionally humanize number as well and default to true. 1000 is 1,000th, etc.
     */
    public function ordinal($value) {
        $value = (integer) $value;
        $ord = array('th','st','nd','rd','th','th','th','th','th','th');
        // special cases
        if (in_array($value % 100, array(11, 12, 13))) return sprintf("%d%s", $value, $ord[0]);
           return sprintf('%d%s', $value, $ord[$value % 10]);
    }
    /**
     * Converts a large integer to a friendly text representation. Works best for
     * numbers over 1 million. For example, 1000000 becomes '1.0 million', 1200000
     * becomes '1.2 million' and '1200000000' becomes '1.2 billion'.
     *
     * For now, don't send numbers larger than 2 billion to it.
     */
    public function intword($value) {
        $value = (integer) $value;
        if ($value < 1000000) {
            return $value;
        }
        if ($value < 1000000000) {
            $new_value = $value / 1000000.0;
            return sprintf("%.1f million", $new_value);
        }
        if ($value < 1000000000000) {
            $new_value = $value / 1000000000.0;
            return sprintf("%.1f billon", $new_value);
        }
        /** PHP doesn't support numbers this large without an extension.
        if ($value < 1000000000000000) { 
           $new_value = $value / 1000000000000.0;
           return sprintf("%.1f trillion", $new_value);
        }
        */
        return $value;
     }
    /**
     * Returns a "humanized" day - today, tomorrow, yesterday if relevant,
     * otherwise it returns the date in $format format
     * 
     * I am sure this is not the best way to do this, but it works
     */
    public function naturalDay($timestamp = null, $format = null) {
        if (is_null($timestamp)) $timestamp = time();
        if (is_null($format)) $format = self::DAYFORMAT;
        $oneday = 60*60*24;
        $today = strtotime('today');
        $tomorrow = $today + $oneday;
        $yesterday = $today - $oneday;
        // if time is 12:00 yesterday or more
        if ($timestamp >= $yesterday) {
            // if time is less than 12:00 the day after tomorrow
            if ($timestamp < $tomorrow + $oneday && $timestamp > $today) {
                // if time is less than 12:00 tomorrow
                if ($timestamp < $tomorrow) {
                    return 'today';
                }
                return 'tomorrow';
            }
            return 'yesterday';
        }
        
        return date($format, $timestamp);
    }
    /**
     * Returns a "humanized" time - so, if entry was today, it will say "about 16 minutes ago", "about 8 hours ago",
     * but if it isn't it will return the time formatted in $format format
     *
     * I am sure this is not the best way to do this, but it works
     */
    public function naturalTime($timestamp = null, $format = null) {
        if (is_null($timestamp)) $timestamp = time();
        if (is_null($format)) $format = self::TIMEFORMAT;
        $now = time();
        $hour = 60*60;
        if ($this->naturalDay($timestamp, $format) == 'today') {
            $hourago = $now - $hour;
            $hourfromnow = $now + $hour;
            // if timestamp passed in was after an hour ago...
            if ($timestamp > $hourago) {
                // if timestamp passed in is in the future...
                if ($timestamp > $now) {
                    // return how many minutes from now
                    $seconds = $timestamp - $now;
                    $minutes = (integer) round($seconds/60);
                    // if more than 60 minutes ago, report in hours
                    if ($minutes > 60) {
                        $hours = round($minutes/60);
                        return "in about $hours hours";
                    }
                    // if it got rounded down to zero, or it was one, report one
                    if (!$minutes || $minutes === 1) return "just now";
                    return "in about $minutes minutes";
                }
                // return how many minutes from now
                $seconds = $now - $timestamp;
                $minutes = (integer) round($seconds/60);
                // if it got rounded down to zero, or it was one, report one
                if (!$minutes || $minutes === 1) return "just now";
                return "about $minutes minutes ago";
            }
        }

        return date($format, $timestamp);
    }
    /**
     * For now, this will only convert numbers 0-9
     * @todo Convert a number to it's spelled-out version. For instance, 1 becomes one, 307 becomes
     * three hundred seven, 28 becomes twenty-eight.
     */
    public function strNum($value) {
        $int = (integer) $value;
        $numbers = array('zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine');
        if (isset($numbers[$value])) return $numbers[$value];
        return $value;
    }
 }
It is used like this:

Code: Select all

Posted on <?= $this->humanize()->naturalDay($this->post->datetime) ?> at <?= $this->humanize()->naturalTime($this->post->datetime) ?>
Last edited by Weirdan on Thu Dec 02, 2010 1:58 am, edited 1 time in total.
Reason: fixed formatting
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: facebook style "time ago" function

Post by VladSun »

I'd use a lookup table instead of using control statements:

Code: Select all

function time_passed($timestamp)
{
    $diff = time() - (int)$timestamp;

    if ($diff == 0) 
         return 'just now';

    $intervals = array
    (
        1                   => array('year',    31556926),
        $diff < 31556926    => array('month',   2628000),
        $diff < 2629744     => array('week',    604800),
        $diff < 604800      => array('day',     86400),
        $diff < 86400       => array('hour',    3600),
        $diff < 3600        => array('minute',  60),
        $diff < 60          => array('second',  1)
    );

     $value = floor($diff/$intervals[1][1]);
     return $value.' '.$intervals[1][0].($value > 1 ? 's' : '').' ago';
}
It's somehow more "config" like :)
Last edited by VladSun on Tue Mar 30, 2010 2:27 am, edited 1 time in total.
There are 10 types of people in this world, those who understand binary and those who don't
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Re: facebook style "time ago" function

Post by s.dot »

You know what's funny, I ALWAYS use braces on everything, I just didn't for this post so the function didn't look so long LOL! Talk about irony.

@luke, that function is wayyyy cool! I can see me doing something like that, using yours as inspiration.

@vladsun, that is badass!! I was thinking along the lines of a lookup, but all my mind could come up with was using in_array() and range() and that would generate huuuuuuge arrays

I'm not even sure how your lookup table works from looking at the code, but dang that is cool. I'm going to play with it and see if I can figure it out.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: facebook style "time ago" function

Post by VladSun »

We had a "solving-intervals-match" topic recently - viewtopic.php?f=1&t=112042
Take a look at it.
There are 10 types of people in this world, those who understand binary and those who don't
User avatar
Benjamin
Site Administrator
Posts: 6930
Joined: Sun May 19, 2002 10:24 pm

Re: facebook style "time ago" function

Post by Benjamin »

Yeah the lookup table is awesome. I didn't realize you could do that. I will definitely start using that in my codebases.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: facebook style "time ago" function

Post by VladSun »

Thanks for the flowers guys :)
There are 10 types of people in this world, those who understand binary and those who don't
User avatar
Darhazer
DevNet Resident
Posts: 1011
Joined: Thu May 14, 2009 3:00 pm
Location: HellCity, Bulgaria

Re: facebook style "time ago" function

Post by Darhazer »

VladSun wrote:Thanks for the flowers guys :)
Well, I bet you don't drink flowers, so you got one whiskey from me for the interesting solutions you are posting.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: facebook style "time ago" function

Post by VladSun »

Heh, an offer one can not refuse :)
Thanks, buddy :drunk:
There are 10 types of people in this world, those who understand binary and those who don't
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: facebook style "time ago" function

Post by josh »

So much better then storing all the columns if saving state to a database too (bitwise lookups). Generally would be done after the initial implementation with if statements, as a refactoring. Pretty interesting application of it vlad
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: facebook style "time ago" function

Post by VladSun »

There are 10 types of people in this world, those who understand binary and those who don't
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Re: facebook style "time ago" function

Post by s.dot »

I would alter my function to include the lookup table instead of the if's ;) But then it would be Vladsun's function and not mine, and I'm not about stealing someone's thunder :P

Readers can choose which they want to use.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Re: facebook style "time ago" function

Post by s.dot »

I did a little speed test to see if using a lookup table would be faster or slower than using conditional if{}'s. On a heavy trafficked place like a forum it may be the deciding factor for which function to use.

I realize the following test does not determine the true speed of each function, but it should provide a somewhat accurate correlation of speed comparing the two functions.

code

Code: Select all

<?php 
class timer{
    private $start;
    private $end;
    
    public function start()
    {
        $this->start = microtime(true);
    }
    
    public function end()
    {
        $this->end = microtime(true);
    }
    
    public function get_time()
    {
        return ($this->end - $this->start;
    }
    
    public function clear()
    {
        $this->start = null;
        $this->end = null;
    }
} 

function a($timestamp){
     //type cast, current time, difference in timestamps
     $timestamp      = (int) $timestamp;
     $current_time   = time();
     $diff           = $current_time - $timestamp;
     
     //intervals in seconds
     $intervals      = array (
         'year' => 31556926, 'month' => 2629744, 'week' => 604800, 'day' => 86400, 'hour' => 3600, 'minute'=> 60
     );
     
    //now we just find the difference
     if ($diff == 0)
     {
         return 'just now';
     }
     
     if ($diff < 60)
     {
         return $diff == 1 ? $diff . ' second ago' : $diff . ' seconds ago';
     }
     
     
     if ($diff >= 60 && $diff < $intervals['hour'])
     {
         $diff = floor($diff/$intervals['minute']);
         return $diff == 1 ? $diff . ' minute ago' : $diff . ' minutes ago';
     }
     
     
     if ($diff >= $intervals['hour'] && $diff < $intervals['day'])
     {
         $diff = floor($diff/$intervals['hour']);
         return $diff == 1 ? $diff . ' hour ago' : $diff . ' hours ago';
     }
    
     if ($diff >= $intervals['day'] && $diff < $intervals['week'])
     {
         $diff = floor($diff/$intervals['day']);
         return $diff == 1 ? $diff . ' day ago' : $diff . ' days ago';
     }
     
     if ($diff >= $intervals['week'] && $diff < $intervals['month'])
     {
         $diff = floor($diff/$intervals['week']);
         return $diff == 1 ? $diff . ' week ago' : $diff . ' weeks ago';
     }
    
     if ($diff >= $intervals['month'] && $diff < $intervals['year'])
     {
         $diff = floor($diff/$intervals['month']);
         return $diff == 1 ? $diff . ' month ago' : $diff . ' months ago';
     }
   
     if ($diff >= $intervals['year'])
     {
         $diff = floor($diff/$intervals['year']);
         return $diff == 1 ? $diff . ' year ago' : $diff . ' years ago';
     }
} 

function b($timestamp){
     $diff = time() - (int)$timestamp;
       if ($diff == 0)
         return 'just now';
       $intervals = array     (
         1                   => array('year',    31556926),
         $diff < 31556926    => array('month',   2628000),
         $diff < 2629744     => array('week',    604800),
         $diff < 604800      => array('day',     86400),
         $diff < 86400       => array('hour',    3600),
         $diff < 3600        => array('minute',  60),
         $diff < 60          => array('second',  1)
     );
     $value = floor($diff/$intervals[1][1]);
     return $value.' '.$intervals[1][0].($value > 1 ? 's' : '').' ago';
} //time values for comparison

$s = time()-10;
$m = time()-70;
$h = time()-60*60*2;
$d = strtotime('-2 days');
$w = strtotime('-3 weeks');
$n = strtotime('-4 months');
$y = strtotime('-5 years');  

//function a$timer = new timer();
$timer->start();
for ($i=0; $i<10000; $i++){
    a($s); //seconds
    a($m); //minutes
    a($h); //hours
    a($d); //day
    a($w); //week
    a($n); //month
    a($y); //year
} 

$timer->end();

echo 'Function a(), 10,000 iterations using one date in each time range: ' . $timer->get_time() . ' seconds' . "\n";
$timer->clear();

//function b
$timer->start();
for ($i=0; $i<10000; $i++){
    b($s); //seconds
    b($m); //minutes
    b($h); //hours
    b($d); //day
    b($w); //week
    b($n); //month
    b($y); //year
} 

$timer->end();
echo 'Function b(), 10,000 iterations using one date in each time range: ' . $timer->get_time() . ' seconds' . "\n";
result

Code: Select all

Function a(), 10,000 iterations using one date in each time range: 0.595626831055 seconds
Function b(), 10,000 iterations using one date in each time range: 0.972527980804 seconds
So if we want to break that down on a per call function

function a

Code: Select all

0.595626831055 / (10,000 iterations * 7 function calls per iteration) = 0.00000850895473
function b

Code: Select all

0.972527980804 / (10,000 iterations * 7 function calls per iteration) = 0.00001389325687
The lookup table does appear to be a tiiiiiiiiny bit slower, nothing big though.

Can you explain why vladsun? Does the lookup table have to evaluate every possible circumstance before it can return, rather than the if{}'s returning immediately once the value is found?
Last edited by Weirdan on Thu Dec 02, 2010 2:07 am, edited 1 time in total.
Reason: fixed formatting
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
VladSun
DevNet Master
Posts: 4313
Joined: Wed Jun 27, 2007 9:44 am
Location: Sofia, Bulgaria

Re: facebook style "time ago" function

Post by VladSun »

s.dot wrote:Can you explain why vladsun? Does the lookup table have to evaluate every possible circumstance before it can return, rather than the if{}'s returning immediately once the value is found?
Exactly.

And while in this case we have relatively simple (not compute expensive) "circumstances" - address/value pairs, it won't be so in many others.
There are 10 types of people in this world, those who understand binary and those who don't
Post Reply