Displaying time as local to a user

At sign-up, I collect the user’s country and set the nearest timezone (they can change this if it is incorrect).

I want to display time local to a user. Every is stored in the database as UTC (in the code below, ‘Zulu’ = UTC), and that zone is set at the start of every run.

Some Googling didn’t really help (e.g. Dan Grossman’s question a few years ago had the same problem).

What I have: (you can re-use this if you want)



class TimezoneTable extends Singleton
{
    private $zones;


    protected function __construct()
    {
        $dir = sfConfig::get('sf_data_dir');
        $dataFile = $dir.'/timezones';
        $timerFile = $dir.'/timezones-timer';
        $logFile = $dir.'/timezones-log';


        // See if the cache is less than a day old.
        if (is_file($timerFile))
        {
            $lastTime = (integer) file_get_contents($timerFile);
            if ($lastTime > (time() - 89000))
            {
                // Cache is fresh.
                $this->zones = unserialize(file_get_contents($dataFile));
                return;
            }
        }


        $vals = json_decode(file_get_contents('http://api.coronatus.com/clock/read'), true);
        $zones = array();
        foreach ($vals as $val)
        {
            $zones[$val['timezone']] = $val['utc_offset'];
        }

        file_put_contents($dataFile, serialize($zones));
        file_put_contents($timerFile, time());
        file_put_contents($logFile, '['.date('Y-m-d H:i:i')."] Refreshed\
", FILE_APPEND);

        $this->zones = $zones;
    }


    public function getAllNames()
    {
        return array_keys($this->zones);
    }


    /**
     * Return the number of hours $timezone is off of UTC.
     *
     * @param string $timezone
     * @return integer  Number of hours.
     */
    public function getOffsetInHours($timezone)
    {
        if (!isset($this->zones[$timezone]))
        {
            throw new InvalidArgumentException('Timezone not known: '.$timezone);
        }

        return $this->zones[$timezone];
    }
}




/**
 * Try to change any string/unix-timestamp/DateTime-object to a unix timestamp.
 *
 * Timezones are ignored.
 *
 * @param mixed $time Any type of date-time, in Zulu
 * @return string Seconds since Epoch, in Zulu
 */
function convert_to_seconds($time)
{
    if ($time instanceof DateTime)
    {
        $seconds = $time->format('U');
    }
    // Already in seconds.
    elseif (ctype_digit($time) && (10 === strlen($time)))
    {
        $seconds = $time;
    }
    // YYYY-MM-DD HH:MM:SS
    elseif (1 === preg_match('/\\d\\d\\d\\d-\\d\\d-\\d\\d \\d\\d:\\d\\d:\\d\\d/', $time))
    {
        $seconds = strtotime($time);
    }
    else
    {
        throw new InvalidArgumentException(
            'Time couldn\\'t be interpreted: '.Coronatus_Util::getArgDescription($time)
        );
    }

    return $seconds;
}


/**
 * Describe the difference in time between $then and now.
 *
 * Timezones are irrelevant to this function.
 *
 * @param mixed $then A zulu time.
 * @return string
 */
function how_long_ago($then)
{
    $diff = time() - convert_to_seconds($then);

    if ($diff < 0)
    {
        throw new InvalidArgumentException('Time is in the future: '.$then);
    }

    return Coronatus_UtilDateTime::timespan($diff).' ago';
}


/**
 * Use this function for every date/time displayed.
 *
 * It alters the time to be in the actor's timezone.
 *
 * @param string $format How to format the output.
 * @param mixed $time The Zulu time to convert.
 * @return string
 */
function display_time($format, $time)
{
    switch (strtolower($format))
    {
        case 'mysql':
            $format = 'Y-m-d H:i:s';
            break;
        case 'date':
            $format = 'jS F Y';
            break;
        case 'datetime':
            $format = 'jS F Y, H:i';
            break;
    }

    if (!isset($userOffset))
    {
        static $userOffset;
        $userOffset = sfContext::getInstance()->getUser()->getTzSecondOffset();
    }

    return date($format, convert_to_seconds($time) + $userOffset);
}

display_time() does the showing.

Is there a better way to do this, that doesn’t involve changing PHP’s TZ multiple times?

Thanks

PHP 5.3 added new date and time zone classes that weren’t available when I asked that question in 2007. If you have access to PHP 5.3, you can do timezone conversion by name without doing any of the computations yourself, or changing the timezone setting for the whole interpreter.

Look at the first comment here for an example:
http://www.php.net/manual/en/datetime.settimezone.php

I did end up doing some ugly stuff with date_default_timezone_set() all over the place back then, but don’t have to anymore.

I like that! Sadly I don’t have PHP5.3 in production, only development… I guess it’s that time again to email my host and ask to move server ^^

It cannot be said as ‘ugly’ way Dan because those stuffs are the only ways to do with prior to PHP 5.3. I have also done in that way for several projects like;

  • let user to select their timezone while registering/signing up.
  • save that in the database
  • set that timezone string with put_env or date_default_timezone_set() at the top of the page/script after the user logs in

Thank you PHP team for adding new feature to achieve the goal. Hope all the hosting servers will upgrade PHP version to 5.3 very soon.

Unless I missed something, I believe you should be using gmdate() and manually adjusting for dst. date() will adjust for php’s timezone offset and dst flag. If php’s timezone is gmt, you get lucky and it “works anyway” because there’s no dst. Then you can put that is_dst flag to use from your json data.