Difference between #3 and #2 of Local time zones and locales

changed
Title
Local timestime zones and locales
unchanged
Category
How-tos
unchanged
Tags
i18n, date, time, timezone, locale
unchanged
Content
Scenario
------------------
Following on from [international
dates](http://www.yiiframework.com/wiki/183/using-international-dates/
"http://www.yiiframework.com/wiki/183/using-international-dates/"), I
also wanted times in the local timezone and format. This was a little more
tricky but I think I've got a solution. This is only really appropriate if
you have an international app.

Setup
------------------
### Column types
Change all date, time, datetime columns to timestamp. This is because timestamp
fields are converted and stored as UTC They can also accept values such as
' 2011-05-25T01:42:48+1000'. You can also then use 'set
time_zone' in phpmyadmin to display timestamp fields in your local time
zone.
### Config
In protected/config/main.php make the following changes
~~~
[php]
// application components
'components'=>array(
	...
	'localtime'=>array(
		'class'=>'LocalTime',
		),
	...
	'db'=>array(
		...
		'initSQLs'=>array("set time_zone='+00:00';"),
           
		...
	),
~~~
This makes the LocalTime class available as Yii::app()->localtime

The sql 'set time_zone' ensures that all dates retrieved are in UTC
time. If your MySql server has the [time zone
names](http://dev.mysql.com/doc/refman/5.1/en/time-zone-support.html
"http://dev.mysql.com/doc/refman/5.1/en/time-zone-support.html")
installed then use 'UTC' instead of '+00:00'.
### Components
Copy the code at the end of this wiki for LocalTime.php and
DefaultDateTimeParser.php into the folder protected/component.
User login
------------------
After the user logs in, set these 2 properties

~~~
[php]
Yii::app()->localtime->Locale = $user->locale->code; // eg
'en_gb'
			
Yii::app()->localtime->TimeZone = $user->timezone->name; // eg
'Europe/London'
~~~

I set these in the authenticate() function in
protected/components/useridentity.php. The localtime component saves the values
as global variables, so you only need to set them once.

For the locale and timezone data, rather than fill up this wiki, I've
posted 2 mysql scripts on my blog at 
[http://www.russellengland.com/2011/05/mysql-scripts-to-create-locale-and.html](http://www.russellengland.com/2011/05/mysql-scripts-to-create-locale-and.html
"http://www.russellengland.com/2011/05/mysql-scripts-to-create-locale-and.html")

Usage in models
------------------
For any date or time fields in your model, change the following
### Rules
Use either getLocalDateTimeFormat($datewidth,$timewidth) or
getLocalDateFormat($datewidth) to get the date/time format for the locale
~~~
[php]
public function rules()
{
	return array(
		...
		array('date_of_birth',
		'date',
		'format'=>
		Yii::app()->localtime->getLocalDateFormat('short')),
		...
		array('start_time',
		'date',
		'format'=>
		Yii::app()->localtime->getLocalDateTimeFormat('short','short')),
		...

~~~
### Before save
Use fromLocalDateTime for both date and date+time values. This will convert the
local time string into an ISO 8601 format eg:
'2011-05-25T01:42:48+1000'
~~~
[php]
protected function beforeSave()
{
	if(parent::beforeSave())
	{
		...
		$this->date_of_birth = 
			Yii::app()->localtime->fromLocalDateTime(
				$this->date_of_birth,'short');
		...
		$this->start_time = 
			Yii::app()->localtime->fromLocalDateTime(
				$this->start_time,'short','short');
		...
		return true;
	}
	else
		return false;
}
~~~
### After find
Finally use either toLocalDate() or toLocalDateTime() to convert the time to the
local format
~~~
[php]
protected function afterFind()
{
	...
	$this->date_of_birth = 
		Yii::app()->localtime->toLocalDate(
			$this->date_of_birth,'short');
	...
	$this->start_time = 
		Yii::app()->localtime->toLocalDateTime(
			$this->start_time,'short','short');
	...		
	return (parent::afterFind());
}	
~~~
### Now()
Because the sql time zone is UTC, DBExpression('NOW()') should return
a UTC time. But for a belt and braces approach I use
~~~
[php]
$this->updated = Yii::app()->localtime->UTCNow;
~~~
There's also a localNow if you need to display the current time
~~~
[php]
echo Yii::app()->localtime->localNow;
~~~

LocalTime component
------------------
Copy this code to protected/component/LocalTime.php
~~~
[php]
<?php
class LocalTime extends CApplicationComponent
{
	/*
	*		Notes
	* 1.	Column field types must be timestamp not date, time, datetime
	*		This is because timestamp columns are stored as UTC then converted to the
specified timezone
	*		date, time and datetime columns don't save the timezone
	* 2.	Set the timezone to UTC in protected/config/main.php
	*		so that all retrieved times are in the UTC timezone for consistency
	* 		'db'=>array(
	*		...
    *		'initSQLs'=>array("set names utf8;set
time_zone='+00:00';"),
	*		),
	* 3.	When using phpMyAdmin, just use "set
time_zone='+00:00'"
	*		or whatever timezone you require to display timestamps in your zone
	* 4.	Yii::app()->setTimeZone() and setLanguage() is not saved globally
	*		So this class is used to save the users timezone and locale
	* 5.	After a user logs in call
Yii::app()->localtime->setLocale('en_gb');
	*		and Yii->app()->localtime->setTimeZone('Europe/London');
	* 6.	All date/time formats default to 'short' eg: dd/mm/yyyy hh:mm -
no seconds
	*/

	// Used for setting/getting the global variable - change this if there are
conflicts
	const _globalTimeZone = 'LocalTime_timezone';
	const _globalLocale = 'LocalTime_locale';
	// Default server time
	const _utc = 'UTC';

	// Set the timezone - usually after the user has logged in
	// Use one of the supported timezones, eg: Europe/London as this will calculate
daylight saving hours
	// http://php.net/manual/en/timezones.php
	public function setTimezone($timezone)
	{
		// Save the timezone for the session
		Yii::app()->setGlobalState(self::_globalTimeZone,$timezone);
	}

	// Return the current timezone
	public function getTimezone()
	{
		// Get the localDateTimeZone if its been set
		$timezone=Yii::app()->getGlobalState(self::_globalTimeZone);

		// Default to UTC if it hasn't
		if ($timezone===null)
			$timezone=self::_utc;

		return($timezone);
	}

	// Set the locale - usually after the user has logged in
	// Use one of the supported locales, eg: en_gb
	// http://php.net/manual/en/timezones.php
	public function setLocale($locale)
	{
		// Save the timezone for the session
		Yii::app()->setGlobalState(self::_globalLocale,$locale);
	}

	// Return the current locale
	public function getLocale()
	{
		// Get the localDateTimeZone if its been set
		$locale=Yii::app()->getGlobalState(self::_globalLocale);

		// Default to yii language if it isn't - note that
Yii::app()->setLanguage doesn't save globally
		if ($locale===null)
			$locale=Yii::app()->language;

		return($locale);
	}

	// Local now() function
	// Can use any of the php date() formats to return the local date/time value
	// http://php.net/manual/en/function.date.php
	public function getLocalNow($format=DATE_ISO8601)
	{
		$localnow=new DateTime(null,$this->localDateTimeZone);
		return $localnow->format($format);
	}

	// UTC Now() function
	// Can use any of the php date() formats to return the UTC date/time value
	// http://php.net/manual/en/function.date.php
	public function getUTCNow($format=DATE_ISO8601)
	{
		$utcnow=new DateTime(null,$this->serverDateTimeZone);
		return $utcnow->format($format);
	}

	// Return a datetimezone object for the local time
	public function getLocalDateTimeZone($timezone=null)
	{
		if ($timezone===null)
			$timezone = $this->timezone;

		// Create a local datetimezone object
		$datetimezone = new DateTimeZone($timezone);

		return($datetimezone);
	}

	// Return a datetimezone object for UTC
	public function getServerDateTimeZone()
	{
		// Create a local datetimezone object
		$datetimezone = new DateTimeZone(self::_utc);

		return($datetimezone);
	}

	// Converts a timestamp from UTC to a local time
	// Expects a date in Y-m-d H:i:s type format and assumes it is UTC
	// Returns a date in the local time zone
	public function fromUTC($servertimestamp)
	{
		// Its okay if an ISO8601 time is passed because the timezone in the string
will be used and the _serverDateTimeZone object is ignored
		$localtime = new DateTime($servertimestamp,$this->serverDateTimeZone);

		// Then set the timezone to local and it will be automagically updated, even
allowing for daylight saving
		$localtime->setTimeZone($this->localDateTimeZone);

		// Return as 2010-08-15 15:52:01 for use in the yii app
		return($localtime->format('Y-m-d H:i:s'));
	}

	// Converts a local timestamp to UTC
	// Expects a date in Y-m-d H:i:s format and assumes it is the local time zone
	// Returns an ISO date in the UTC zone
	public function toUTC($localtimestamp)
	{
		// Create an object using the local time zone - this will account for daylight
saving
		$servertime = new DateTime($localtimestamp,$this->localDateTimeZone);

		// Then set the timezone to UTC and it will be automagically updated
		// In theory this step isn't needed if using the ISO8601 format.
		$servertime->setTimeZone($this->serverDateTimeZone);

		// Return as 2010-08-15T15:52:01+0000 so the timestamp column is properly
updated
		return($servertime->format(DATE_ISO8601));
	}

	// Use in afterFind
	// Ensure that the SQL "set time_zone='+00:00'" has been
set
	// Returns a date/time combination based on the current locale
	// Expects a date/time in the yyyy-mm-dd hh:mm:ss type format
	public function
toLocalDateTime($servertimestamp,$datewidth='short',$timewidth='short')
	{
		// Create a server datetime object
		$localtime = new DateTime($servertimestamp,$this->serverDateTimeZone);

		// Then set the timezone to local and it will be automagically updated, even
allowing for daylight saving
		$localtime->setTimeZone($this->localDateTimeZone);

		// Get the local yii date+time format
		$localformat = $this->getLocalDateTimeFormat($datewidth,$timewidth);

		// Convert to php date format
		$localformat = $this->YiitoPHPDateFormat($localformat);

		// Return as a local datetime
		return($localtime->format($localformat));

	}

	// Use in afterFind
	// Ensure that the SQL "set time_zone='+00:00'" has been
set
	// Returns the date based on the current locale
	// Expects a date string in the yyyy-mm-dd hh:mm:ss type format
	public function toLocalDate($servertimestamp,$datewidth='short')
	{
		// Create a server datetime object
		$localtime = new DateTime($servertimestamp,$this->serverDateTimeZone);

		// Then set the timezone to local and it will be automagically updated, even
allowing for daylight saving
		$localtime->setTimeZone($this->localDateTimeZone);

		// Get the local yii date only format
		$localformat = $this->getLocalDateFormat($datewidth);

		// Convert to php date format
		$localformat = $this->YiitoPHPDateFormat($localformat);

		// Return as a local datetime
		return($localtime->format($localformat));
	}

	// Use in beforeSave
	// Converts a date/time string in the locale format to an ISO time for saving
to the server
	// eg 31/12/2011 will become 2011-12-31T00:00:00+0000
	public function
fromLocalDateTime($localtime,$datewidth='short',$timewidth='short')
	{
		// Local datetime format
		$localformat = $this->getLocalDateTimeFormat($datewidth,$timewidth);

		// Uses a modified CDateTimeParser that defaults the time values rather than
return false
		// Also returns a time string rather than a timestamp just in case the
timestamp is the wrong timezone
		$defaults = array('year'=>$this->getLocalNow('Y'),
'month'=>$this->getLocalNow('m'),
'day'=>$this->getLocalNow('d'),
'hour'=>0, 'minute'=>0, 'second'=>0 );
		$timevalues = DefaultDateTimeParser::parse($localtime, $localformat,
$defaults);

		// Create a new date time in the local timezone
		$servertime = new DateTime($timevalues,$this->localDateTimeZone);

		// Set the timezone to UTC
		$servertime = $servertime->setTimeZone($this->serverDateTimeZone);

		// Return it as an iso date ready for saving
		return($servertime->format(DATE_ISO8601));

	}

	// Returns the local date time format from yii
	public function
getLocalDateTimeFormat($datewidth='short',$timewidth='short')
	{
		// Set the language so the local date/time format is returned
		// This assumes the locale has already been set
		Yii::app()->setLanguage($this->locale);

		// This returns the order of the date time combination, eg {1} {0}
		$localformat = Yii::app()->locale->getDateTimeFormat();

		// Get the local date format - eg dd/MM/yyyy
		$localformat = str_replace('{1}',
Yii::app()->locale->getDateFormat($datewidth), $localformat);

		// Get the local time format with seconds - eg hh:mm:ss
		$localformat = str_replace('{0}',
Yii::app()->locale->getTimeFormat($timewidth), $localformat);

		return $localformat;
	}

	// Just a wrapper of the yii getDateFormat function
	public function getLocalDateFormat($width='short')
	{
		// Set the language so the local date format is returned
		// Assumes the locale has been set
		Yii::app()->setLanguage($this->locale);

		// Get the local date format - eg dd/MM/yyyy
		$localformat = Yii::app()->locale->getDateFormat($width);
		return $localformat;
	}

	// Converts a yii date format to a php date format
	// Eg dd/MM/yyyy HH:mm:ss will be converted to d/m/Y H:i:s
	public function YiitoPHPDateFormat($dateformat)
	{
		$patterns = array(
			//
'ampm'=>array('a'=>'a','A'=>'A'),
			'microseconds'=>array('zzzz'=>'u','z'=>'u'),
			'seconds'=>array('ss'=>'s','s'=>'s'),
			'minutes'=>array('mm'=>'i','m'=>'i'),
			'24hours'=>array('HH'=>'H','H'=>'G'),
			'12hours'=>array('hh'=>'h','h'=>'g'),
			'years'=>array('yyyy'=>'Y','yy'=>'y'),
			'months'=>array('MM'=>'m','M'=>'n'),
			'days'=>array('dd'=>'d','d'=>'j'),
		);


		// Run through each time pattern
		foreach($patterns as $pattern)
		{
			foreach($pattern as $search=>$replace)
			{
				if (strpos($dateformat,$search)!==false)
				{
					// We have a winner!
					// Replace the first pattern found and then get out of here
					$dateformat = str_replace($search, $replace, $dateformat);
					break;
				}
			}
		}
		return($dateformat);
	}

}
~~~
DefaultDateTimeParser
------------------
Used by LocalTime, this is a copy of the CDateTimeParser but uses defaults for
missing values rather then returning false. It also returns a string rather than
a unix timestamp in case the time zone is wrong.
~~~
[php]
<?php

/**
 * DefaultDateTimeParser converts a date/time string to an array
 *
 * The following pattern characters are recognized:
 * <pre>
 * Pattern |      Description
 * ----------------------------------------------------
 * d       | Day of month 1 to 31, no padding
 * dd      | Day of month 01 to 31, zero leading
 * M       | Month digit 1 to 12, no padding
 * MM      | Month digit 01 to 12, zero leading
 * yy      | 2 year digit, e.g., 96, 05
 * yyyy    | 4 year digit, e.g., 2005
 * h       | Hour in 0 to 23, no padding
 * hh      | Hour in 00 to 23, zero leading
 * H       | Hour in 0 to 23, no padding
 * HH      | Hour in 00 to 23, zero leading
 * m       | Minutes in 0 to 59, no padding
 * mm      | Minutes in 00 to 59, zero leading
 * s       | Seconds in 0 to 59, no padding
 * ss      | Seconds in 00 to 59, zero leading
 * a       | AM or PM, case-insensitive (since version 1.1.5)
 * ----------------------------------------------------
 * </pre>
 *
 *
 * Modified version of http://www.yiiframework.com/doc/api/1.1/CDateTimeParser
 *
 * This version will accept a pattern and default the time values for any
missing pattern
 * It returns a string rather than a timestamp in case its the wrong timezone
 * Also uses the LocalTime class to get the time for now() in the users
timezone
 * For example,
DefaultDateTimeParser::parse('31/12/2011','dd/MM/yyyy',array('hour'=>0,'minute'=>0,'day'=>0);
 * Will return '2011-12-2011 0:0:0'
 */
class DefaultDateTimeParser
{

	public static function
parse($value,$pattern='MM/dd/yyyy',$defaults=array())
	{
		$tokens=self::tokenize($pattern);
		$i=0;
		$n=strlen($value);
		foreach($tokens as $token)
		{
			switch($token)
			{
				case 'yyyy':
				{
					if(($year=self::parseInteger($value,$i,4,4))!==null)
						$i+=4;
					break;
				}
				case 'yy':
				{
					if(($year=self::parseInteger($value,$i,1,2))!==null)
						$i+=strlen($year);
					break;
				}
				case 'MM':
				{
					if(($month=self::parseInteger($value,$i,2,2))!==null)
						$i+=2;
					break;
				}
				case 'M':
				{
					if(($month=self::parseInteger($value,$i,1,2))!==null)
						$i+=strlen($month);
					break;
				}
				case 'dd':
				{
					if(($day=self::parseInteger($value,$i,2,2))!==null)
						$i+=2;
					break;
				}
				case 'd':
				{
					if(($day=self::parseInteger($value,$i,1,2))!==null)
						$i+=strlen($day);
					break;
				}
				case 'h':
				case 'H':
				{
					if(($hour=self::parseInteger($value,$i,1,2))!==null)
						$i+=strlen($hour);
					break;
				}
				case 'hh':
				case 'HH':
				{
					if(($hour=self::parseInteger($value,$i,2,2))!==null)
						$i+=2;
					break;
				}
				case 'm':
				{
					if(($minute=self::parseInteger($value,$i,1,2))!==null)
						$i+=strlen($minute);
					break;
				}
				case 'mm':
				{
					if(($minute=self::parseInteger($value,$i,2,2))!==null)
						$i+=2;
					break;
				}
				case 's':
				{
					if(($second=self::parseInteger($value,$i,1,2))!==null)
						$i+=strlen($second);
					break;
				}
				case 'ss':
				{
					if(($second=self::parseInteger($value,$i,2,2))!==null)
						$i+=2;
					break;
				}
				case 'a':
				{
				    // If this value isn't present then ignore it
					if(($ampm=self::parseAmPm($value,$i))===null)
				        break;

				    if(isset($hour))
				    {
				    	if($hour==12 && $ampm==='am')
				    		$hour=0;
				    	else if($hour<12 && $ampm==='pm')
				    		$hour+=12;
				    }
					$i+=2;
					break;
				}
				default:
				{
					// If the separator pattern doesn't exist in the value, then ignore
it
					// eg: a space
					if (strpos($value, $token)===false)
							break;
					
					$tn=strlen($token);
					if($i>=$n || substr($value,$i,$tn)!==$token)
						return false;
					$i+=$tn;
					break;
				}
			}
		}
		if($i<$n) // somethings gone wrong
			return false;

		// Defaults to the date/time for the local timezone
		// If you don't want to use Yii::app()-localtime->localNow() then
simply replace with the php date() function
		// Yii::app()->localtime-> = LocalTime::
		if(!isset($year))
			$year=isset($defaults['year']) ? $defaults['year'] :
Yii::app()->localtime->localNow('Y'); // date('Y');
		if(!isset($month))
			$month=isset($defaults['month']) ? $defaults['month'] :
Yii::app()->localtime->localNow('n'); // date('n');
		if(!isset($day))
			$day=isset($defaults['day']) ? $defaults['day'] :
Yii::app()->localtime->localNow('j'); // date('j');
		if(!isset($hour))
			$hour=isset($defaults['hour']) ? $defaults['hour'] :
Yii::app()->localtime->localNow('H'); // date('H');
		if(!isset($minute))
			$minute=isset($defaults['minute']) ? $defaults['minute']
: Yii::app()->localtime->localNow('i'); // date('i');
		if(!isset($second))
			$second=isset($defaults['second']) ? $defaults['second']
: Yii::app()->localtime->localNow('s'); // date('s');

		$year=(int)$year;
		$month=(int)$month;
		$day=(int)$day;
		$hour=(int)$hour;
		$minute=(int)$minute;
		$second=(int)$second;
		

		if(CTimestamp::isValidDate($year,$month,$day) &&
CTimestamp::isValidTime($hour,$minute,$second))
		{
			// Return a time string rather than a timestamp because the timestamp might
be the wrong timezone?
			return $year.'-'.$month.'-'.$day.'
'.$hour.':'.$minute.':'.$second;
		}
		else
			return false;
	}

	/*
	 * @param string $pattern the pattern that the date string is following
	 */
	private static function tokenize($pattern)
	{
		if(!($n=strlen($pattern)))
			return array();
		$tokens=array();
		for($c0=$pattern[0],$start=0,$i=1;$i<$n;++$i)
		{
			if(($c=$pattern[$i])!==$c0)
			{
				$tokens[]=substr($pattern,$start,$i-$start);
				$c0=$c;
				$start=$i;
			}
		}
		$tokens[]=substr($pattern,$start,$n-$start);
		return $tokens;
	}

	/*
	 * @param string $value the date string to be parsed
	 * @param integer $offset starting offset
	 * @param integer $minLength minimum length
	 * @param integer $maxLength maximum length
	 */
	protected static function parseInteger($value,$offset,$minLength,$maxLength)
	{
		for($len=$maxLength;$len>=$minLength;--$len)
		{
			$v=substr($value,$offset,$len);
			if(ctype_digit($v) && strlen($v)>=$minLength)
				return $v;
		}
		// Changed by Russell England to null rather than false
		return null;
	}

	/*
	 * @param string $value the date string to be parsed
	 * @param integer $offset starting offset
	 */
	protected static function parseAmPm($value, $offset)
	{
		$v=strtolower(substr($value,$offset,2));
		return $v==='am' || $v==='pm' ? $v : false;
	}
}

~~~