OlsonTimeZone::getRawOffset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace Agavi\Date;
3
4
// +---------------------------------------------------------------------------+
5
// | This file is part of the Agavi package.                                   |
6
// | Copyright (c) 2005-2011 the Agavi Project.                                |
7
// |                                                                           |
8
// | For the full copyright and license information, please view the LICENSE   |
9
// | file that was distributed with this source code. You can also view the    |
10
// | LICENSE file online at http://www.agavi.org/LICENSE.txt                   |
11
// |   vi: set noexpandtab:                                                    |
12
// |   Local Variables:                                                        |
13
// |   indent-tabs-mode: t                                                     |
14
// |   End:                                                                    |
15
// +---------------------------------------------------------------------------+
16
use Agavi\Exception\AgaviException;
17
use Agavi\Translation\TranslationManager;
18
use Agavi\Util\Toolkit;
19
20
/**
21
 * A time zone based on the Olson database. Olson time zones change behavior
22
 * over time. The raw offset, rules, presence or absence of daylight savings
23
 * time, and even the daylight savings amount can all vary.
24
 *
25
 * Ported from ICU:
26
 *  icu/trunk/source/i18n/olsontz.cpp         r19133
27
 *  icu/trunk/source/i18n/olsontz.h           r18762
28
 *
29
 * @package    agavi
30
 * @subpackage date
31
 *
32
 * @author     Dominik del Bondio <[email protected]>
33
 * @author     The ICU Project
34
 * @copyright  Authors
35
 * @copyright  The Agavi Project
36
 *
37
 * @since      0.11.0
38
 *
39
 * @version    $Id$
40
 */
41
class OlsonTimeZone extends TimeZone
42
{
43
    /**
44
     * The transitions
45
     *
46
     * @var        array
47
     * @since      0.11.0
48
     */
49
    protected $transitions;
50
51
    /**
52
     * The types, 1..255
53
     *
54
     * @var        array
55
     * @since      0.11.0
56
     */
57
    protected $types;
58
59
    /**
60
     * The last year for which the transitions data are to be used
61
     * rather than the finalZone.  If there is no finalZone, then this
62
     * is set to INT32_MAX.  NOTE: This corresponds to the year _before_
63
     * the one indicated by finalMillis.
64
     *
65
     * @var        int
66
     * @since      0.11.0
67
     */
68
    protected $finalYear;
69
70
    /**
71
     * The millis for the start of the first year for which finalZone
72
     * is to be used, or DBL_MAX if finalZone is 0.  NOTE: This is
73
     * 0:00 GMT Jan 1, <finalYear + 1> (not <finalMillis>).
74
     *
75
     * @var        float
76
     * @since      0.11.0
77
     */
78
    protected $finalMillis;
79
80
    /**
81
     * A SimpleTimeZone that governs the behavior for years > finalYear.
82
     * If and only if finalYear == INT32_MAX then finalZone == 0.
83
     *
84
     * @var        SimpleTimeZone
85
     * @since      0.11.0
86
     */
87
    protected $finalZone; // owned, may be NULL
88
89
    const MAX_INT = 2147483647;
90
    const MAX_DBL = Calendar::MAX_MILLIS;
91
92
    /**
93
     * Constructor
94
     *
95
     * @see        AgaviOlsonTimeZone::constructor()
96
     * @see        AgaviOlsonTimeZone::constructorOSA()
97
     *
98
     * @author     Dominik del Bondio <[email protected]>
99
     * @author     The ICU Project
100
     * @since      0.11.0
101
     */
102
    public function __construct()
103
    {
104
        $arguments = func_get_args();
105
        if (count($arguments) == 1) {
106
            parent::__construct($arguments[0]);
107
            return;
108
        }
109
        $fName = Toolkit::overloadHelper(array(
110
            array('name' => 'constructorOSA',
111
                        'parameters' => array('object', 'string', 'array')),
112
            ),
113
            $arguments
114
        );
115
        call_user_func_array(array($this, $fName), $arguments);
116
    }
117
118
    /**
119
     * Default constructor. Creates a time zone with an empty ID and
120
     * a fixed GMT offset of zero.
121
     *
122
     * @author     Dominik del Bondio <[email protected]>
123
     * @author     The ICU Project
124
     * @since      0.11.0
125
     */
126
    protected function constructor()
127
    {
128
        $this->finalYear = self::MAX_INT;
129
        $this->finalMillis = self::MAX_DBL;
130
        $this->finalZone = null;
131
132
        $this->constructEmpty();
133
    }
134
135
    /**
136
     * Construct a GMT+0 zone with no transitions.  This is done when a
137
     * constructor fails so the resultant object is well-behaved.
138
     *
139
     * @author     Dominik del Bondio <[email protected]>
140
     * @author     The ICU Project
141
     * @since      0.11.0
142
     */
143
    protected function constructEmpty()
144
    {
145
        $this->transitionCount = 0;
0 ignored issues
show
Bug introduced by
The property transitionCount does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
146
        $this->transitions = array();
147
        // TODO: this should probably contain at least one item
148
        $this->types = array();
149
    }
150
151
    /**
152
     * Construct with info from an array.
153
     *
154
     * @param      TranslationManager $tm The translation manager.
155
     * @param      string $id The id.
156
     * @param      array  $zoneInfo The zone info data.
157
     *
158
     * @author     Dominik del Bondio <[email protected]>
159
     * @author     The ICU Project
160
     * @since      0.11.0
161
     */
162
    protected function constructorOSA(TranslationManager $tm, $id, array $zoneInfo)
163
    {
164
        parent::__construct($tm, $id);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (__construct() instead of constructorOSA()). Are you sure this is correct? If so, you might want to change this to $this->__construct().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
165
166
        $this->finalYear = self::MAX_INT;
167
        $this->finalMillis = self::MAX_DBL;
168
        $this->finalZone = null;
169
170
        foreach ($zoneInfo['rules'] as $rule) {
171
            $this->transitions[] = $rule;
172
        }
173
174
        $this->types = $zoneInfo['types'];
175
176
        if (!isset($zoneInfo['finalRule'])) {
177
            throw new AgaviException($id);
178
        }
179
180
        // Subtract one from the actual final year; we actually store final year - 1,
181
        // and compare using > rather than >=.  This allows us to use INT32_MAX as
182
        // an exclusive upper limit for all years, including INT32_MAX.
183
        $rawOffset = $zoneInfo['finalRule']['offset'] * DateDefinitions::MILLIS_PER_SECOND;
184
        $this->finalYear = $zoneInfo['finalRule']['startYear'] - 1;
0 ignored issues
show
Documentation Bug introduced by
It seems like $zoneInfo['finalRule']['startYear'] - 1 can also be of type double. However, the property $finalYear is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
185
        // Also compute the millis for Jan 1, 0:00 GMT of the finalYear.  This reduces runtime computations.
186
        $this->finalMillis = CalendarGrego::fieldsToDay($zoneInfo['finalRule']['startYear'], 0, 1) * DateDefinitions::MILLIS_PER_DAY;
187
188
        if ($zoneInfo['finalRule']['type'] == 'dynamic') {
189
            $fr = $zoneInfo['finalRule'];
190
            $this->finalZone = new SimpleTimeZone(
191
                $tm, $rawOffset, $id,
192
                $fr['start']['month'], $fr['start']['date'], $fr['start']['day_of_week'], $fr['start']['time'], $fr['start']['type'],
193
                $fr['end']['month'], $fr['end']['date'], $fr['end']['day_of_week'], $fr['end']['time'], $fr['end']['type'],
194
                $fr['save'] * DateDefinitions::MILLIS_PER_SECOND
195
                );
196
        } else {
197
            $this->finalZone = new SimpleTimeZone($tm, $rawOffset, $id);
198
        }
199
    }
200
201
    /**
202
     * Returns true if the two TimeZone objects are equal.
203
     *
204
     * @param      TimeZone $that The timezone to compare against.
205
     *
206
     * @author     Dominik del Bondio <[email protected]>
207
     * @author     The ICU Project
208
     * @since      0.11.0
209
     */
210 View Code Duplication
    function __is_equal(TimeZone $that)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
211
    {
212
        // TODO: we need to compare finalyear and the transitions and finalzone
213
        return ($this === $that ||
214
                        (get_class($this) == get_class($that) &&
215
                            TimeZone::__is_equal($that)
216
                        ));
217
    }
218
219
    /**
220
     * TimeZone API.
221
     *
222
     * @see        TimeZone::getOffsetIIIIII()
223
     *
224
     * @author     Dominik del Bondio <[email protected]>
225
     * @author     The ICU Project
226
     * @since      0.11.0
227
     */
228 View Code Duplication
    protected function getOffsetIIIIII($era, $year, $month, $dom, $dow, $millis)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
229
    {
230
        if ($month < DateDefinitions::JANUARY || $month > DateDefinitions::DECEMBER) {
231
            throw new \InvalidArgumentException('Month out of range');
232
        } else {
233
            return $this->getOffsetIIIIIII($era, $year, $month, $dom, $dow, $millis, CalendarGrego::monthLength($year, $month));
234
        }
235
    }
236
237
    /**
238
     * TimeZone API.
239
     *
240
     * @see        TimeZone::getOffsetIIIIIII()
241
     *
242
     * @author     Dominik del Bondio <[email protected]>
243
     * @author     The ICU Project
244
     * @since      0.11.0
245
     */
246
    protected function getOffsetIIIIIII($era, $year, $month, $dom, $dow, $millis, $monthLength)
247
    {
248
        if (($era != GregorianCalendar::AD && $era != GregorianCalendar::BC)
249
                || $month < DateDefinitions::JANUARY
250
                || $month > DateDefinitions::DECEMBER
251
                || $dom < 1
252
                || $dom > $monthLength
253
                || $dow < DateDefinitions::SUNDAY
254
                || $dow > DateDefinitions::SATURDAY
255
                || $millis < 0
256
                || $millis >= DateDefinitions::MILLIS_PER_DAY
257
                || $monthLength < 28
258
                || $monthLength > 31) {
259
            throw new \InvalidArgumentException('One of the supplied parameters is out of range');
260
        }
261
262
        if ($era == GregorianCalendar::BC) {
263
            $year = -$year;
264
        }
265
266
        if ($year > $this->finalYear) { // [sic] >, not >=; see above
267
            return $this->finalZone->getOffset($era, $year, $month, $dom, $dow, $millis, $monthLength);
268
        }
269
270
        // Compute local epoch seconds from input fields
271
        $time = CalendarGrego::fieldsToDay($year, $month, $dom) * DateDefinitions::SECONDS_PER_DAY + floor($millis / DateDefinitions::MILLIS_PER_SECOND);
272
273
        $transition = $this->findTransition($time, true);
274
        return ($this->types[$transition['type']]['dstOffset'] + $this->types[$transition['type']]['rawOffset']) * DateDefinitions::MILLIS_PER_SECOND;
275
    }
276
277
    /**
278
     * TimeZone API.
279
     *
280
     * @see        TimeZone::getOffsetRef()
281
     *
282
     * @author     Dominik del Bondio <[email protected]>
283
     * @author     The ICU Project
284
     * @since      0.11.0
285
     */
286
    public function getOffsetRef($date, $local, &$rawoff, &$dstoff)
287
    {
288
        // The check against finalMillis will suffice most of the time, except
289
        // for the case in which finalMillis == DBL_MAX, date == DBL_MAX,
290
        // and finalZone == 0.  For this case we add "&& finalZone != 0".
291
        if ($date >= $this->finalMillis && $this->finalZone !== null) {
292
            $millis = 0;
293
            $days = Toolkit::floorDivide($date, DateDefinitions::MILLIS_PER_DAY, $millis);
294
295
            $year = 0;
296
            $month = 0;
297
            $dom = 0;
298
            $dow = 0;
299
300
            CalendarGrego::dayToFields($days, $year, $month, $dom, $dow);
301
302
            $rawoff = $this->finalZone->getRawOffset();
303
304
            if (!$local) {
305
                // Adjust from GMT to local
306
                $date += $rawoff;
307
                $days2 = Toolkit::floorDivide($date, DateDefinitions::MILLIS_PER_DAY, $millis);
308
                if ($days2 != $days) {
309
                    CalendarGrego::dayToFields($days2, $year, $month, $dom, $dow);
310
                }
311
            }
312
313
            $dstoff = $this->finalZone->getOffset(GregorianCalendar::AD, $year, $month, $dom, $dow, $millis) - $rawoff;
314
            return;
315
        }
316
317
        $secs = floor($date / DateDefinitions::MILLIS_PER_SECOND);
318
        $transition = $this->findTransition($secs, $local);
319
        $rawoff = $this->types[$transition['type']]['rawOffset'] * DateDefinitions::MILLIS_PER_SECOND;
320
        $dstoff = $this->types[$transition['type']]['dstOffset'] * DateDefinitions::MILLIS_PER_SECOND;
321
    }
322
323
    /**
324
     * TimeZone API.
325
     *
326
     * @see        TimeZone::setRawOffset()
327
     *
328
     * @author     Dominik del Bondio <[email protected]>
329
     * @author     The ICU Project
330
     * @since      0.11.0
331
     */
332
    public function setRawOffset($offsetMillis)
333
    {
334
        // We don't support this operation, since OlsonTimeZones are
335
        // immutable (except for the ID, which is in the base class).
336
337
        // Nothing to do!
338
    }
339
340
    /**
341
     * TimeZone API.
342
     *
343
     * @see        TimeZone::getRawOffset()
344
     *
345
     * @author     Dominik del Bondio <[email protected]>
346
     * @author     The ICU Project
347
     * @since      0.11.0
348
     */
349
    public function getRawOffset()
350
    {
351
        $raw = 0;
352
        $dst = 0;
353
        $this->getOffsetRef(Calendar::getNow(), false, $raw, $dst);
354
        return $raw;
355
    }
356
357
    /**
358
     * Find the smallest i (in 0..transitionCount-1) such that time >=
359
     * transition(i), where transition(i) is either the GMT or the local
360
     * transition time, as specified by `local'.
361
     *
362
     * @param      float $time epoch seconds, either GMT or local wall
363
     * @param      bool  $local if TRUE, `time' is in local wall units, otherwise it
364
     *                   is GMT
365
     *
366
     * @return     int   an index i, where 0 <= i < transitionCount, and
367
     *                   transition(i) <= time < transition(i+1), or i == 0 if
368
     *                   transitionCount == 0 or time < transition(0).
369
     *
370
     * @author     Dominik del Bondio <[email protected]>
371
     * @author     The ICU Project
372
     * @since      0.11.0
373
     */
374
    protected function findTransition($time, $local)
375
    {
376
        $i = 0;
377
378
        if (count($this->transitions) > 0) {
379
            // Linear search from the end is the fastest approach, since
380
            // most lookups will happen at/near the end.
381
            for ($i = count($this->transitions) - 1; $i > 0; --$i) {
382
                $transition = $this->transitions[$i];
383
                if ($local) {
384
                    $prevType = $this->transitions[$i - 1]['type'];
385
                    $zoneOffsetPrev = $this->types[$prevType]['dstOffset'] + $this->types[$prevType]['rawOffset'];
386
                    $currType = $transition['type'];
387
                    $zoneOffsetCurr = $this->types[$currType]['dstOffset'] + $this->types[$currType]['rawOffset'];
388
                    
389
                    // use the lowest offset ( == standard time ). as per tzregts.cpp which says:
390
391
                            /**
392
                             * @bug 4084933
393
                             * The expected behavior of TimeZone around the boundaries is:
394
                             * (Assume transition time of 2:00 AM)
395
                             *    day of onset 1:59 AM STD  = display name 1:59 AM ST
396
                             *                 2:00 AM STD  = display name 3:00 AM DT
397
                             *    day of end   0:59 AM STD  = display name 1:59 AM DT
398
                             *                 1:00 AM STD  = display name 1:00 AM ST
399
                             */
400
                    if ($zoneOffsetPrev < $zoneOffsetCurr) {
401
                        $transition['time'] += $zoneOffsetPrev;
402
                    } else {
403
                        $transition['time'] += $zoneOffsetCurr;
404
                    }
405
                }
406
407
                if ($time >= $transition['time']) {
408
                    break;
409
                }
410
            }
411
        }
412
413
        return $this->transitions[$i];
414
    }
415
416
    /**
417
     * TimeZone API.
418
     *
419
     * @see        TimeZone::useDaylightTime()
420
     *
421
     * @author     Dominik del Bondio <[email protected]>
422
     * @author     The ICU Project
423
     * @since      0.11.0
424
     */
425
    public function useDaylightTime()
426
    {
427
        // If DST was observed in 1942 (for example) but has never been
428
        // observed from 1943 to the present, most clients will expect
429
        // this method to return FALSE.  This method determines whether
430
        // DST is in use in the current year (at any point in the year)
431
        // and returns TRUE if so.
432
433
        $days = floor(Calendar::getNow() / DateDefinitions::MILLIS_PER_DAY); // epoch days
434
435
        $year = 0;
436
        $month = 0;
437
        $dom = 0;
438
        $dow = 0;
439
440
        CalendarGrego::dayToFields($days, $year, $month, $dom, $dow);
441
442
        if ($year > $this->finalYear) { // [sic] >, not >=; see above
443
            if ($this->finalZone) {
444
                return $this->finalZone->useDaylightTime();
445
            } else {
446
                return true;
447
            }
448
        }
449
450
        // Find start of this year, and start of next year
451
        $start = (int) CalendarGrego::fieldsToDay($year, 0, 1) * DateDefinitions::SECONDS_PER_DAY;
452
        $limit = (int) CalendarGrego::fieldsToDay($year + 1, 0, 1) * DateDefinitions::SECONDS_PER_DAY;
453
454
        // Return TRUE if DST is observed at any time during the current year.
455
        for ($i = 0, $transitionCount = count($this->transitions); $i < $transitionCount; ++$i) {
456
            if ($this->transitions[$i]['time'] >= $limit) {
457
                break;
458
            }
459
            if (($this->transitions[$i]['time'] >= $start && $this->types[$this->transitions[$i]['type']]['dstOffset'] != 0) || ($this->transitions[$i]['time'] > $start && $i > 0 && $this->types[$this->transitions[$i-1]['type']]['dstOffset'] != 0)) {
460
                return true;
461
            }
462
        }
463
464
        return false;
465
    }
466
467
    /**
468
     * TimeZone API.
469
     *
470
     * @see        TimeZone::getDSTSavings()
471
     *
472
     * @author     Dominik del Bondio <[email protected]>
473
     * @author     The ICU Project
474
     * @since      0.11.0
475
     */
476
    public function getDSTSavings()
477
    {
478
        if ($this->finalZone !== null) {
479
            return $this->finalZone->getDSTSavings();
480
        }
481
        return parent::getDSTSavings();
482
    }
483
484
    /**
485
     * TimeZone API.
486
     *
487
     * @see        TimeZone::inDaylightTime()
488
     *
489
     * @author     Dominik del Bondio <[email protected]>
490
     * @author     The ICU Project
491
     * @since      0.11.0
492
     */
493
    public function inDaylightTime($date)
494
    {
495
        $raw = 0;
496
        $dst = 0;
497
        $this->getOffsetRef($date, false, $raw, $dst);
498
        return $dst != 0;
499
    }
500
}
501