Page 1 of 2

facebook style "time ago" function

Posted: Tue Feb 23, 2010 12:03 am
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.

Re: facebook style "time ago" function

Posted: Tue Feb 23, 2010 1:17 am
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!

Re: facebook style "time ago" function

Posted: Tue Feb 23, 2010 1:39 am
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) ?>

Re: facebook style "time ago" function

Posted: Tue Feb 23, 2010 5:06 am
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 :)

Re: facebook style "time ago" function

Posted: Tue Feb 23, 2010 2:00 pm
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.

Re: facebook style "time ago" function

Posted: Tue Feb 23, 2010 4:00 pm
by VladSun
We had a "solving-intervals-match" topic recently - viewtopic.php?f=1&t=112042
Take a look at it.

Re: facebook style "time ago" function

Posted: Wed Feb 24, 2010 4:16 pm
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.

Re: facebook style "time ago" function

Posted: Wed Feb 24, 2010 5:08 pm
by VladSun
Thanks for the flowers guys :)

Re: facebook style "time ago" function

Posted: Wed Feb 24, 2010 6:14 pm
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.

Re: facebook style "time ago" function

Posted: Thu Feb 25, 2010 1:23 am
by VladSun
Heh, an offer one can not refuse :)
Thanks, buddy :drunk:

Re: facebook style "time ago" function

Posted: Sat Mar 06, 2010 11:37 am
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

Re: facebook style "time ago" function

Posted: Sun Mar 07, 2010 4:22 pm
by VladSun

Re: facebook style "time ago" function

Posted: Mon Mar 08, 2010 2:55 am
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.

Re: facebook style "time ago" function

Posted: Mon Mar 08, 2010 3:27 am
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?

Re: facebook style "time ago" function

Posted: Mon Mar 08, 2010 3:37 am
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.