diff --git a/lib/kolab_sync_timezone_converter.php b/lib/kolab_sync_timezone_converter.php
index ec83b56..5d8d5c7 100644
--- a/lib/kolab_sync_timezone_converter.php
+++ b/lib/kolab_sync_timezone_converter.php
@@ -1,610 +1,610 @@
|
| Copyright (C) 2008-2012, Metaways Infosystems GmbH |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak |
| Author: Jonas Fischer |
+--------------------------------------------------------------------------+
*/
/**
* Activesync timezone converter
*/
class kolab_sync_timezone_converter
{
/**
* holds the instance of the singleton
*
* @var kolab_sync_timezone_onverter
*/
private static $_instance = NULL;
protected $_startDate = array();
/**
* If set then the timezone guessing results will be cached.
* This is strongly recommended for performance reasons.
*
* @var rcube_cache
*/
protected $cache = null;
/**
* array of offsets known by ActiceSync clients, but unknown by php
* @var array
*/
protected $_knownTimezones = array(
'0AIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==' => array(
'Pacific/Kwajalein' => 'MHT'
)
);
/**
* don't use the constructor. Use the singleton.
*
* @param $_logger
*/
private function __construct()
{
}
/**
* don't clone. Use the singleton.
*/
private function __clone()
{
}
/**
* the singleton pattern
*
* @return kolab_sync_timezone_converter
*/
public static function getInstance()
{
if (self::$_instance === NULL) {
self::$_instance = new kolab_sync_timezone_converter();
}
return self::$_instance;
}
/**
* Returns a list of timezones that match to the {@param $_offsets}
*
* If {@see $_expectedTimezone} is set then the method will terminate as soon
* as the expected timezone has matched and the expected timezone will be the
* first entry to the returned array.
*
* @param string|array $_offsets
*
* @return array
*/
public function getListOfTimezones($_offsets)
{
if (is_string($_offsets) && isset($this->_knownTimezones[$_offsets])) {
$timezones = $this->_knownTimezones[$_offsets];
}
else {
if (is_string($_offsets)) {
// unpack timezone info to array
$_offsets = $this->_unpackTimezoneInfo($_offsets);
}
if (!$this->_validateOffsets($_offsets)) {
return array();
}
$this->_setDefaultStartDateIfEmpty($_offsets);
$cacheId = $this->_getCacheId('timezones', $_offsets);
$timezones = $this->_loadFromCache($cacheId);
if (!is_array($timezones)) {
$timezones = array();
foreach (DateTimeZone::listIdentifiers() as $timezoneIdentifier) {
$timezone = new DateTimeZone($timezoneIdentifier);
if (false !== ($matchingTransition = $this->_checkTimezone($timezone, $_offsets))) {
$timezones[$timezoneIdentifier] = $matchingTransition['abbr'];
}
}
$this->_saveInCache($timezones, $cacheId);
}
}
return $timezones;
}
/**
* Returns PHP timezone that matches to the {@param $_offsets}
*
* If {@see $_expectedTimezone} is set then the method will return this timezone if it matches.
*
* @param string|array $_offsets Activesync timezone definition
* @param string $_expectedTomezone Expected timezone name
*
* @return string Expected timezone name
*/
public function getTimezone($_offsets, $_expectedTimezone = null)
{
$timezones = $this->getListOfTimezones($_offsets);
if ($_expectedTimezone && isset($timezones[$_expectedTimezone])) {
return $_expectedTimezone;
}
else {
return key($timezones);
}
}
/**
* Return packed string for given {@param $_timezone}
*
* @param string $_timezone Timezone identifier
* @param string|int $_startDate Start date
*
* @return string Packed timezone offsets
*/
public function encodeTimezone($_timezone, $_startDate = null)
{
foreach ($this->_knownTimezones as $packedString => $knownTimezone) {
if (array_key_exists($_timezone, $knownTimezone)) {
return $packedString;
}
}
$offsets = $this->getOffsetsForTimezone($_timezone, $_startDate);
return $this->_packTimezoneInfo($offsets);
}
/**
* Get offsets for given timezone
*
* @param string $_timezone Timezone identifier
* @param string|int $_startDate Start date
*
* @return array Timezone offsets
*/
public function getOffsetsForTimezone($_timezone, $_startDate = null)
{
$this->_setStartDate($_startDate);
$cacheId = $this->_getCacheId('offsets', array($_timezone));
if (false === ($offsets = $this->_loadFromCache($cacheId))) {
$offsets = $this->_getOffsetsTemplate();
try {
$timezone = new DateTimeZone($_timezone);
}
catch (Exception $e) {
return null;
}
list($standardTransition, $daylightTransition) = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']);
if ($standardTransition) {
$offsets['bias'] = $standardTransition['offset']/60*-1;
if ($daylightTransition) {
- $offsets = $this->_generateOffsetsForTransition($offsets, $standardTransition, 'standard');
- $offsets = $this->_generateOffsetsForTransition($offsets, $daylightTransition, 'daylight');
+ $offsets = $this->_generateOffsetsForTransition($offsets, $standardTransition, 'standard', $timezone);
+ $offsets = $this->_generateOffsetsForTransition($offsets, $daylightTransition, 'daylight', $timezone);
//@todo how do we get the standardBias (is usually 0)?
//$offsets['standardBias'] = ...
$offsets['daylightBias'] = ($daylightTransition['offset'] - $standardTransition['offset'])/60*-1;
$offsets['standardHour'] -= $offsets['daylightBias'] / 60;
$offsets['daylightHour'] += $offsets['daylightBias'] / 60;
}
}
$this->_saveInCache($offsets, $cacheId);
}
return $offsets;
}
/**
* Get offsets for timezone transition
*
- * @param array $_offsets Timezone offsets
- * @param array $_transition Timezone transition information
- * @param string $_type 'standard' or 'daylight'
+ * @param array $_offsets Timezone offsets
+ * @param array $_transition Timezone transition information
+ * @param string $_type Transition type: 'standard' or 'daylight'
+ * @param DateTimeZone $_timezone Timezone of the transition
*
* @return array
*/
- protected function _generateOffsetsForTransition(array $_offsets, array $_transition, $_type)
+ protected function _generateOffsetsForTransition(array $_offsets, array $_transition, $_type, $_timezone)
{
- $transitionDateParsed = new DateTime($_transition['time']);
+ $transitionDate = new DateTime($_transition['time'], $_timezone);
if ($_transition['offset']) {
- $transitionDateParsed->modify($_transition['offset'] . ' seconds');
+ $transitionDate->modify($_transition['offset'] . ' seconds');
}
- $_offsets[$_type . 'Month'] = (int) $transitionDateParsed->format('n');
- $_offsets[$_type . 'DayOfWeek'] = (int) $transitionDateParsed->format('w');
- $_offsets[$_type . 'Minute'] = (int) $transitionDateParsed->format('i');
- $_offsets[$_type . 'Hour'] = (int) $transitionDateParsed->format('G');
+ $_offsets[$_type . 'Month'] = (int) $transitionDate->format('n');
+ $_offsets[$_type . 'DayOfWeek'] = (int) $transitionDate->format('w');
+ $_offsets[$_type . 'Minute'] = (int) $transitionDate->format('i');
+ $_offsets[$_type . 'Hour'] = (int) $transitionDate->format('G');
for ($i=5; $i>0; $i--) {
- if ($this->_isNthOcurrenceOfWeekdayInMonth($_transition['ts'], $i)) {
+ if ($this->_isNthOcurrenceOfWeekdayInMonth($transitionDate, $i)) {
$_offsets[$_type . 'Day'] = $i;
break;
};
}
return $_offsets;
}
/**
* Test if the weekday of the given {@param $_timestamp} is the {@param $_occurence}th occurence of this weekday within its month.
*
- * @param int $_timestamp
+ * @param DateTime $_datetime
* @param int $_occurence [1 to 5, where 5 indicates the final occurrence during the month if that day of the week does not occur 5 times]
*
* @return bool
*/
- protected function _isNthOcurrenceOfWeekdayInMonth($_timestamp, $_occurence)
+ protected function _isNthOcurrenceOfWeekdayInMonth($_datetime, $_occurence)
{
if ($_occurence <= 1) {
return true;
}
- $original = new DateTime('@'.$_timestamp);
- $orig = $original->format('n');
+ $orig = $_datetime->format('n');
if ($_occurence == 5) {
- $modified = clone($original);
+ $modified = clone($_datetime);
$modified->modify('1 week');
$mod = $modified->format('n');
// modified date is a next month
return $mod > $orig || ($mod == 1 && $orig == 12);
}
- $modified = clone($original);
+ $modified = clone($_datetime);
$modified->modify(sprintf('-%d weeks', $_occurence - 1));
$mod = $modified->format('n');
if ($mod != $orig) {
return false;
}
- $modified = clone($original);
+ $modified = clone($_datetime);
$modified->modify(sprintf('-%d weeks', $_occurence));
$mod = $modified->format('n');
// modified month is earlier than original
return $mod < $orig || ($mod == 12 && $orig == 1);
}
/**
* Check if the given {@param $_standardTransition} and {@param $_daylightTransition}
* match to the object property {@see $_offsets}
*
* @param array $standardTransition
* @param array $daylightTransition
*
* @return bool
*/
- protected function _checkTransition($_standardTransition, $_daylightTransition, $_offsets)
+ protected function _checkTransition($_standardTransition, $_daylightTransition, $_offsets, $tz)
{
if (empty($_standardTransition) || empty($_offsets)) {
return false;
}
$standardOffset = ($_offsets['bias'] + $_offsets['standardBias']) * 60 * -1;
// check each condition in a single if statement and break the chain when one condition is not met - for performance reasons
if ($standardOffset == $_standardTransition['offset'] ) {
if (empty($_offsets['daylightMonth']) && (empty($_daylightTransition) || empty($_daylightTransition['isdst']))) {
// No DST
return true;
}
$daylightOffset = ($_offsets['bias'] + $_offsets['daylightBias']) * 60 * -1;
// the milestone is sending a positive value for daylightBias while it should send a negative value
$daylightOffsetMilestone = ($_offsets['bias'] + ($_offsets['daylightBias'] * -1) ) * 60 * -1;
if ($daylightOffset == $_daylightTransition['offset'] || $daylightOffsetMilestone == $_daylightTransition['offset']) {
- $standardParsed = getdate($_standardTransition['ts']);
- $daylightParsed = getdate($_daylightTransition['ts']);
+ $standardDate = new DateTime($_standardTransition['time'], $tz);
+ $daylightDate = new DateTime($_daylightTransition['time'], $tz);
- if ($standardParsed['mon'] == $_offsets['standardMonth'] &&
- $daylightParsed['mon'] == $_offsets['daylightMonth'] &&
- $standardParsed['wday'] == $_offsets['standardDayOfWeek'] &&
- $daylightParsed['wday'] == $_offsets['daylightDayOfWeek']
+ if ($standardDate->format('n') == $_offsets['standardMonth'] &&
+ $daylightDate->format('n') == $_offsets['daylightMonth'] &&
+ $standardDate->format('w') == $_offsets['standardDayOfWeek'] &&
+ $daylightDate->format('w') == $_offsets['daylightDayOfWeek']
) {
- return $this->_isNthOcurrenceOfWeekdayInMonth($_daylightTransition['ts'], $_offsets['daylightDay']) &&
- $this->_isNthOcurrenceOfWeekdayInMonth($_standardTransition['ts'], $_offsets['standardDay']);
+ return $this->_isNthOcurrenceOfWeekdayInMonth($daylightDate, $_offsets['daylightDay']) &&
+ $this->_isNthOcurrenceOfWeekdayInMonth($standardDate, $_offsets['standardDay']);
}
}
}
return false;
}
/**
* decode timezone info from activesync
*
* @param string $_packedTimezoneInfo the packed timezone info
* @return array
*/
protected function _unpackTimezoneInfo($_packedTimezoneInfo)
{
$timezoneUnpackString = 'lbias/a64standardName/vstandardYear/vstandardMonth/vstandardDayOfWeek/vstandardDay/vstandardHour/vstandardMinute/vstandardSecond/vstandardMilliseconds/lstandardBias/a64daylightName/vdaylightYear/vdaylightMonth/vdaylightDayOfWeek/vdaylightDay/vdaylightHour/vdaylightMinute/vdaylightSecond/vdaylightMilliseconds/ldaylightBias';
$timezoneInfo = unpack($timezoneUnpackString, base64_decode($_packedTimezoneInfo));
return $timezoneInfo;
}
/**
* encode timezone info to activesync
*
* @param array $_timezoneInfo
* @return string
*/
protected function _packTimezoneInfo($_timezoneInfo)
{
if (!is_array($_timezoneInfo)) {
return null;
}
$packed = pack(
"la64vvvvvvvvla64vvvvvvvvl",
$_timezoneInfo['bias'],
$_timezoneInfo['standardName'],
$_timezoneInfo['standardYear'],
$_timezoneInfo['standardMonth'],
$_timezoneInfo['standardDayOfWeek'],
$_timezoneInfo['standardDay'],
$_timezoneInfo['standardHour'],
$_timezoneInfo['standardMinute'],
$_timezoneInfo['standardSecond'],
$_timezoneInfo['standardMilliseconds'],
$_timezoneInfo['standardBias'],
$_timezoneInfo['daylightName'],
$_timezoneInfo['daylightYear'],
$_timezoneInfo['daylightMonth'],
$_timezoneInfo['daylightDayOfWeek'],
$_timezoneInfo['daylightDay'],
$_timezoneInfo['daylightHour'],
$_timezoneInfo['daylightMinute'],
$_timezoneInfo['daylightSecond'],
$_timezoneInfo['daylightMilliseconds'],
$_timezoneInfo['daylightBias']
);
return base64_encode($packed);
}
/**
* Returns complete offsets array with all fields empty
*
* Used e.g. when reverse-generating ActiveSync Timezone Offset Information
* based on a given Timezone, {@see getOffsetsForTimezone}
*
* @return unknown_type
*/
protected function _getOffsetsTemplate()
{
return array(
'bias' => 0,
'standardName' => '',
'standardYear' => 0,
'standardMonth' => 0,
'standardDayOfWeek' => 0,
'standardDay' => 0,
'standardHour' => 0,
'standardMinute' => 0,
'standardSecond' => 0,
'standardMilliseconds' => 0,
'standardBias' => 0,
'daylightName' => '',
'daylightYear' => 0,
'daylightMonth' => 0,
'daylightDayOfWeek' => 0,
'daylightDay' => 0,
'daylightHour' => 0,
'daylightMinute' => 0,
'daylightSecond' => 0,
'daylightMilliseconds' => 0,
'daylightBias' => 0
);
}
/**
* Validate and set offsets
*
* @param array $value
*
* @return bool Validation result
*/
protected function _validateOffsets($value)
{
// validate $value
if ((!empty($value['standardMonth']) || !empty($value['standardDay']) || !empty($value['daylightMonth']) || !empty($value['daylightDay'])) &&
(empty($value['standardMonth']) || empty($value['standardDay']) || empty($value['daylightMonth']) || empty($value['daylightDay']))
) {
// It is not possible not set standard offsets without setting daylight offsets and vice versa
return false;
}
return true;
}
/**
* Parse and set object property {@see $_startDate}
*
* @param string|int $_startDate
* @return void
*/
protected function _setStartDate($_startDate)
{
if (empty($_startDate)) {
$this->_setDefaultStartDateIfEmpty();
return;
}
$startDateParsed = array();
if (is_string($_startDate)) {
$startDateParsed['string'] = $_startDate;
$startDateParsed['ts'] = strtotime($_startDate);
}
else if (is_int($_startDate)) {
$startDateParsed['ts'] = $_startDate;
$startDateParsed['string'] = strftime('%F', $_startDate);
}
else {
$this->_setDefaultStartDateIfEmpty();
return;
}
$startDateParsed['object'] = new DateTime($startDateParsed['string']);
$startDateParsed = array_merge($startDateParsed, getdate($startDateParsed['ts']));
$this->_startDate = $startDateParsed;
}
/**
* Set default value for object property {@see $_startdate} if it is not set yet.
* Tries to guess the correct startDate depending on object property {@see $_offsets} and
* falls back to current date.
*
* @param array $_offsets [offsets may be avaluated for a given start year]
* @return void
*/
protected function _setDefaultStartDateIfEmpty($_offsets = null)
{
if (!empty($this->_startDate)) {
return;
}
if (!empty($_offsets['standardYear'])) {
$this->_setStartDate($_offsets['standardYear'].'-01-01');
}
else {
$this->_setStartDate(time());
}
}
/**
* Check if the given {@param $_timezone} matches the {@see $_offsets}
* and also evaluate the daylight saving time transitions for this timezone if necessary.
*
* @param DateTimeZone $timezone
* @param array $offsets
*
* @return array|bool
*/
protected function _checkTimezone(DateTimeZone $timezone, $offsets)
{
list($standardTransition, $daylightTransition) = $this->_getTransitionsForTimezoneAndYear($timezone, $this->_startDate['year']);
- if ($this->_checkTransition($standardTransition, $daylightTransition, $offsets)) {
+ if ($this->_checkTransition($standardTransition, $daylightTransition, $offsets, $timezone)) {
return $standardTransition;
}
return false;
}
/**
* Returns the standard and daylight transitions for the given {@param $_timezone}
* and {@param $_year}.
*
* @param DateTimeZone $_timezone
* @param int $_year
*
* @return array
*/
protected function _getTransitionsForTimezoneAndYear(DateTimeZone $_timezone, $_year)
{
$standardTransition = null;
$daylightTransition = null;
$start = mktime(0, 0, 0, 12, 1, $_year - 1);
$end = mktime(24, 0, 0, 12, 31, $_year);
$transitions = $_timezone->getTransitions($start, $end);
if ($transitions === false) {
return array();
}
foreach ($transitions as $index => $transition) {
if (strftime('%Y', $transition['ts']) == $_year) {
if (isset($transitions[$index+1]) && strftime('%Y', $transitions[$index]['ts']) == strftime('%Y', $transitions[$index+1]['ts'])) {
$daylightTransition = $transition['isdst'] ? $transition : $transitions[$index+1];
$standardTransition = $transition['isdst'] ? $transitions[$index+1] : $transition;
}
else {
$daylightTransition = $transition['isdst'] ? $transition : null;
$standardTransition = $transition['isdst'] ? null : $transition;
}
break;
}
else if ($index == count($transitions) -1) {
$standardTransition = $transition;
}
}
return array($standardTransition, $daylightTransition);
}
protected function _getCacheId($_prefix, $_offsets)
{
return $_prefix . md5(serialize($_offsets));
}
protected function _loadFromCache($key)
{
if ($cache = $this->getCache) {
return $cache->get($key);
}
return false;
}
protected function _saveInCache($value, $key)
{
if ($cache = $this->getCache) {
$cache->set($key, $value);
}
}
/**
* Getter for the cache engine object
*/
protected function getCache()
{
if ($this->cache === null) {
$rcube = rcube::get_instance();
$cache = $rcube->get_cache_shared('activesync');
$this->cache = $cache ? $cache : false;
}
return $this->cache;
}
}
diff --git a/tests/timezone_converter.php b/tests/timezone_converter.php
index 0cfcd3a..4dbd68e 100644
--- a/tests/timezone_converter.php
+++ b/tests/timezone_converter.php
@@ -1,98 +1,116 @@
getListOfTimezones($input);
$this->assertTrue(is_array($output));
+
+ $converter = timezone_converter_test::getInstance();
+ $output = $converter->getListOfTimezones('xP///0MAZQBuAHQAcgBhAGwAIABFAHUAcgBvAHAAZQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAoAAAAFAAMAAAAAAAAAAAAAAEMAZQBuAHQAcgBhAGwAIABFAHUAcgBvAHAAZQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUAAAAAAAAAAAAAAAMAAAAFAAIAAAAAAAAAxP///w==');
+
+ $this->assertTrue(is_array($output));
+ $this->assertTrue(isset($output['Europe/Warsaw']));
+
+ $converter = timezone_converter_test::getInstance();
+ $output = $converter->getListOfTimezones('4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==');
+
+ $this->assertTrue(is_array($output));
+ $this->assertTrue(isset($output['America/Los_Angeles']));
}
function test_get_timezone()
{
+ date_default_timezone_set('America/Los_Angeles');
+
$converter = timezone_converter_test::getInstance();
$datetime = new DateTime('2017-01-01T12:00:00Z');
$offsets = $converter->getOffsetsForTimezone('UTC', $datetime);
$output = $converter->getTimezone($offsets, 'UTC');
$this->assertSame('UTC', $output);
$offsets = $converter->getOffsetsForTimezone('Europe/Warsaw', $datetime);
$output = $converter->getTimezone($offsets, 'Europe/Warsaw');
$this->assertSame('Europe/Warsaw', $output);
- $offsets = $converter->getOffsetsForTimezone('America/Los_angeles', $datetime);
+ $offsets = $converter->getOffsetsForTimezone('America/Los_Angeles', $datetime);
$output = $converter->getTimezone($offsets, 'America/Los_Angeles');
$this->assertSame('America/Los_Angeles', $output);
}
function test_get_offsets_for_timezone()
{
+ date_default_timezone_set('America/Los_Angeles');
+
$converter = timezone_converter_test::getInstance();
$datetime = new DateTime('2017-01-01T12:00:00Z');
$output = $converter->getOffsetsForTimezone('UTC', $datetime);
$this->assertSame($output['bias'], 0);
$this->assertSame($output['standardBias'], 0);
$this->assertSame($output['daylightBias'], 0);
$this->assertSame($output['standardMonth'], 0);
$this->assertSame($output['daylightMonth'], 0);
$output = $converter->getOffsetsForTimezone('Europe/Warsaw', $datetime);
$this->assertSame($output['standardBias'], 0);
$this->assertSame($output['standardMonth'], 10);
$this->assertSame($output['standardDay'], 5);
$this->assertSame($output['standardHour'], 3);
$this->assertSame($output['daylightBias'], -60);
$this->assertSame($output['daylightMonth'], 3);
$this->assertSame($output['daylightDay'], 5);
$this->assertSame($output['daylightHour'], 2);
$output = $converter->getOffsetsForTimezone('America/Los_Angeles', $datetime);
$this->assertSame($output['bias'], 480);
$this->assertSame($output['standardBias'], 0);
$this->assertSame($output['standardMonth'], 11);
$this->assertSame($output['standardDay'], 1);
$this->assertSame($output['standardHour'], 2);
$this->assertSame($output['daylightBias'], -60);
$this->assertSame($output['daylightMonth'], 3);
$this->assertSame($output['daylightDay'], 2);
$this->assertSame($output['daylightHour'], 2);
$output = $converter->getOffsetsForTimezone('Atlantic/Azores', $datetime);
$this->assertSame($output['bias'], 60);
$this->assertSame($output['standardBias'], 0);
$this->assertSame($output['standardMonth'], 10);
$this->assertSame($output['standardDay'], 5);
$this->assertSame($output['standardHour'], 1);
$this->assertSame($output['daylightBias'], -60);
$this->assertSame($output['daylightMonth'], 3);
$this->assertSame($output['daylightDay'], 5);
$this->assertSame($output['daylightHour'], 0);
}
}
class timezone_converter_test extends kolab_sync_timezone_converter
{
// disable cache
function getCache()
{
return null;
}
}