root/fTimestamp.php

Revision 859, 43.9 kB (checked in by wbond, 3 weeks ago)

Completed ticket #463 - fixed a bug in fTimestamp::__construct() with specifying a timezone other than the default for a relative time string such as "now" or "+2 hours"

LineHide Line Numbers
1 <?php
2 /**
3  * Represents a date and time as a value object
4  *
5  * @copyright  Copyright (c) 2008-2010 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fTimestamp
11  *
12  * @version    1.0.0b10
13  * @changes    1.0.0b10  Fixed a bug in ::__construct() with specifying a timezone other than the default for a relative time string such as "now" or "+2 hours" [wb, 2010-07-05]
14  * @changes    1.0.0b9   Added the `$simple` parameter to ::getFuzzyDifference() [wb, 2010-03-15]
15  * @changes    1.0.0b8   Fixed a bug with ::fixISOWeek() not properly parsing some ISO week dates [wb, 2009-10-06]
16  * @changes    1.0.0b7   Fixed a translation bug with ::getFuzzyDifference() [wb, 2009-07-11]
17  * @changes    1.0.0b6   Added ::registerUnformatCallback() and ::callUnformatCallback() to allow for localization of date/time parsing [wb, 2009-06-01]
18  * @changes    1.0.0b5   Backwards compatibility break - Removed ::getSecondsDifference() and ::getSeconds(), added ::eq(), ::gt(), ::gte(), ::lt(), ::lte() [wb, 2009-03-05]
19  * @changes    1.0.0b4   Updated for new fCore API [wb, 2009-02-16]
20  * @changes    1.0.0b3   Removed a useless double check of the strtotime() return value in ::__construct() [wb, 2009-01-21]
21  * @changes    1.0.0b2   Added support for CURRENT_TIMESTAMP, CURRENT_DATE and CURRENT_TIME SQL keywords [wb, 2009-01-11]
22  * @changes    1.0.0b    The initial implementation [wb, 2008-02-12]
23  */
24 class fTimestamp
25 {
26     // The following constants allow for nice looking callbacks to static methods
27     const callFormatCallback       = 'fTimestamp::callFormatCallback';
28     const callUnformatCallback     = 'fTimestamp::callUnformatCallback';
29     const combine                  = 'fTimestamp::combine';
30     const defineFormat             = 'fTimestamp::defineFormat';
31     const fixISOWeek               = 'fTimestamp::fixISOWeek';
32     const getDefaultTimezone       = 'fTimestamp::getDefaultTimezone';
33     const isValidTimezone          = 'fTimestamp::isValidTimezone';
34     const registerFormatCallback   = 'fTimestamp::registerFormatCallback';
35     const registerUnformatCallback = 'fTimestamp::registerUnformatCallback';
36     const reset                    = 'fTimestamp::reset';
37     const setDefaultTimezone       = 'fTimestamp::setDefaultTimezone';
38     const translateFormat          = 'fTimestamp::translateFormat';
39    
40    
41     /**
42     * Pre-defined formatting styles
43     *
44     * @var array
45     */
46     static private $formats = array();
47    
48     /**
49     * A callback to process all formatting strings through
50     *
51     * @var callback
52     */
53     static private $format_callback = NULL;
54    
55     /**
56     * A callback to parse all date string to allow for locale-specific parsing
57     *
58     * @var callback
59     */
60     static private $unformat_callback = NULL;
61    
62    
63     /**
64     * If a format callback is defined, call it
65     *
66     * @internal
67      *
68     * @param  string $formatted_string  The formatted date/time/timestamp string to be (possibly) modified
69     * @return string  The (possibly) modified formatted string
70     */
71     static public function callFormatCallback($formatted_string)
72     {
73         if (self::$format_callback) {
74             return call_user_func(self::$format_callback, $formatted_string);
75         }
76         return $formatted_string;
77     }
78    
79    
80     /**
81     * If an unformat callback is defined, call it
82     *
83     * @internal
84      *
85     * @param  string $date_time_string  A raw date/time/timestamp string to be (possibly) parsed/modified
86     * @return string  The (possibly) parsed or modified date/time/timestamp
87     */
88     static public function callUnformatCallback($date_time_string)
89     {
90         if (self::$unformat_callback) {
91             return call_user_func(self::$unformat_callback, $date_time_string);
92         }
93         return $date_time_string;
94     }
95    
96    
97     /**
98     * Checks to make sure the current version of PHP is high enough to support timezone features
99     *
100     * @return void
101     */
102     static private function checkPHPVersion()
103     {
104         if (!fCore::checkVersion('5.1')) {
105             throw new fEnvironmentException(
106                 'The %s class takes advantage of the timezone features in PHP 5.1.0 and newer. Unfortunately it appears you are running an older version of PHP.',
107                 __CLASS__
108             );
109         }
110     }
111    
112    
113     /**
114     * Composes text using fText if loaded
115     *
116     * @param  string  $message    The message to compose
117     * @param  mixed   $component  A string or number to insert into the message
118     * @param  mixed   ...
119     * @return string  The composed and possible translated message
120     */
121     static protected function compose($message)
122     {
123         $args = array_slice(func_get_args(), 1);
124        
125         if (class_exists('fText', FALSE)) {
126             return call_user_func_array(
127                 array('fText', 'compose'),
128                 array($message, $args)
129             );
130         } else {
131             return vsprintf($message, $args);
132         }
133     }
134    
135    
136     /**
137     * Creates a reusable format for formatting fDate, fTime, and fTimestamp objects
138     *
139     * @param  string $name               The name of the format
140     * @param  string $formatting_string  The format string compatible with the [http://php.net/date date()] function
141     * @return void
142     */
143     static public function defineFormat($name, $formatting_string)
144     {
145         self::$formats[$name] = $formatting_string;
146     }
147    
148    
149     /**
150     * Fixes an ISO week format into `'Y-m-d'` so [http://php.net/strtotime strtotime()] will accept it
151     *
152     * @internal
153      *
154     * @param  string $date  The date to fix
155     * @return string  The fixed date
156     */
157     static public function fixISOWeek($date)
158     {
159         if (preg_match('#^(.*)(\d{4})-W(5[0-3]|[1-4][0-9]|0?[1-9])-([1-7])(.*)$#D', $date, $matches)) {
160             $before = $matches[1];
161             $year   = $matches[2];
162             $week   = $matches[3];
163             $day    = $matches[4];
164             $after  = $matches[5];
165            
166             $first_of_year  = strtotime($year . '-01-01');
167             $first_thursday = strtotime('thursday', $first_of_year);
168             $iso_year_start = strtotime('last monday', $first_thursday);
169            
170             $ymd = date('Y-m-d', strtotime('+' . ($week-1) . ' weeks +' . ($day-1) . ' days', $iso_year_start));   
171            
172             $date = $before . $ymd . $after;
173         }
174         return $date;
175     }
176    
177    
178     /**
179     * Provides a consistent interface to getting the default timezone. Wraps the [http://php.net/date_default_timezone_get date_default_timezone_get()] function.
180     *
181     * @return string  The default timezone used for all date/time calculations
182     */
183     static public function getDefaultTimezone()
184     {
185         self::checkPHPVersion();
186        
187         return date_default_timezone_get();
188     }
189    
190    
191     /**
192     * Checks to see if a timezone is valid
193     *
194     * @internal
195      *
196     * @param  string  $timezone   The timezone to check
197     * @return boolean  If the timezone is valid
198     */
199     static public function isValidTimezone($timezone)
200     {
201         static $valid_timezones = array(
202             'UTC'                                   => TRUE,
203             'Africa/Abidjan'                        => TRUE,
204             'Africa/Accra'                          => TRUE,
205             'Africa/Addis_Ababa'                    => TRUE,
206             'Africa/Algiers'                        => TRUE,
207             'Africa/Asmara'                         => TRUE,
208             'Africa/Asmera'                         => TRUE,
209             'Africa/Bamako'                         => TRUE,
210             'Africa/Bangui'                         => TRUE,
211             'Africa/Banjul'                         => TRUE,
212             'Africa/Bissau'                         => TRUE,
213             'Africa/Blantyre'                       => TRUE,
214             'Africa/Brazzaville'                    => TRUE,
215             'Africa/Bujumbura'                      => TRUE,
216             'Africa/Cairo'                          => TRUE,
217             'Africa/Casablanca'                     => TRUE,
218             'Africa/Ceuta'                          => TRUE,
219             'Africa/Conakry'                        => TRUE,
220             'Africa/Dakar'                          => TRUE,
221             'Africa/Dar_es_Salaam'                  => TRUE,
222             'Africa/Djibouti'                       => TRUE,
223             'Africa/Douala'                         => TRUE,
224             'Africa/El_Aaiun'                       => TRUE,
225             'Africa/Freetown'                       => TRUE,
226             'Africa/Gaborone'                       => TRUE,
227             'Africa/Harare'                         => TRUE,
228             'Africa/Johannesburg'                   => TRUE,
229             'Africa/Kampala'                        => TRUE,
230             'Africa/Khartoum'                       => TRUE,
231             'Africa/Kigali'                         => TRUE,
232             'Africa/Kinshasa'                       => TRUE,
233             'Africa/Lagos'                          => TRUE,
234             'Africa/Libreville'                     => TRUE,
235             'Africa/Lome'                           => TRUE,
236             'Africa/Luanda'                         => TRUE,
237             'Africa/Lubumbashi'                     => TRUE,
238             'Africa/Lusaka'                         => TRUE,
239             'Africa/Malabo'                         => TRUE,
240             'Africa/Maputo'                         => TRUE,
241             'Africa/Maseru'                         => TRUE,
242             'Africa/Mbabane'                        => TRUE,
243             'Africa/Mogadishu'                      => TRUE,
244             'Africa/Monrovia'                       => TRUE,
245             'Africa/Nairobi'                        => TRUE,
246             'Africa/Ndjamena'                       => TRUE,
247             'Africa/Niamey'                         => TRUE,
248             'Africa/Nouakchott'                     => TRUE,
249             'Africa/Ouagadougou'                    => TRUE,
250             'Africa/Porto-Novo'                     => TRUE,
251             'Africa/Sao_Tome'                       => TRUE,
252             'Africa/Timbuktu'                       => TRUE,
253             'Africa/Tripoli'                        => TRUE,
254             'Africa/Tunis'                          => TRUE,
255             'Africa/Windhoek'                       => TRUE,
256             'America/Adak'                          => TRUE,
257             'America/Anchorage'                     => TRUE,
258             'America/Anguilla'                      => TRUE,
259             'America/Antigua'                       => TRUE,
260             'America/Araguaina'                     => TRUE,
261             'America/Argentina/Buenos_Aires'        => TRUE,
262             'America/Argentina/Catamarca'           => TRUE,
263             'America/Argentina/ComodRivadavia'      => TRUE,
264             'America/Argentina/Cordoba'             => TRUE,
265             'America/Argentina/Jujuy'               => TRUE,
266             'America/Argentina/La_Rioja'            => TRUE,
267             'America/Argentina/Mendoza'             => TRUE,
268             'America/Argentina/Rio_Gallegos'        => TRUE,
269             'America/Argentina/San_Juan'            => TRUE,
270             'America/Argentina/San_Luis'            => TRUE,
271             'America/Argentina/Tucuman'             => TRUE,
272             'America/Argentina/Ushuaia'             => TRUE,
273             'America/Aruba'                         => TRUE,
274             'America/Asuncion'                      => TRUE,
275             'America/Atikokan'                      => TRUE,
276             'America/Atka'                          => TRUE,
277             'America/Bahia'                         => TRUE,
278             'America/Barbados'                      => TRUE,
279             'America/Belem'                         => TRUE,
280             'America/Belize'                        => TRUE,
281             'America/Blanc-Sablon'                  => TRUE,
282             'America/Boa_Vista'                     => TRUE,
283             'America/Bogota'                        => TRUE,
284             'America/Boise'                         => TRUE,
285             'America/Buenos_Aires'                  => TRUE,
286             'America/Cambridge_Bay'                 => TRUE,
287             'America/Campo_Grande'                  => TRUE,
288             'America/Cancun'                        => TRUE,
289             'America/Caracas'                       => TRUE,
290             'America/Catamarca'                     => TRUE,
291             'America/Cayenne'                       => TRUE,
292             'America/Cayman'                        => TRUE,
293             'America/Chicago'                       => TRUE,
294             'America/Chihuahua'                     => TRUE,
295             'America/Coral_Harbour'                 => TRUE,
296             'America/Cordoba'                       => TRUE,
297             'America/Costa_Rica'                    => TRUE,
298             'America/Cuiaba'                        => TRUE,
299             'America/Curacao'                       => TRUE,
300             'America/Danmarkshavn'                  => TRUE,
301             'America/Dawson'                        => TRUE,
302             'America/Dawson_Creek'                  => TRUE,
303             'America/Denver'                        => TRUE,
304             'America/Detroit'                       => TRUE,
305             'America/Dominica'                      => TRUE,
306             'America/Edmonton'                      => TRUE,
307             'America/Eirunepe'                      => TRUE,
308             'America/El_Salvador'                   => TRUE,
309             'America/Ensenada'                      => TRUE,
310             'America/Fort_Wayne'                    => TRUE,
311             'America/Fortaleza'                     => TRUE,
312             'America/Glace_Bay'                     => TRUE,
313             'America/Godthab'                       => TRUE,
314             'America/Goose_Bay'                     => TRUE,
315             'America/Grand_Turk'                    => TRUE,
316             'America/Grenada'                       => TRUE,
317             'America/Guadeloupe'                    => TRUE,
318             'America/Guatemala'                     => TRUE,
319             'America/Guayaquil'                     => TRUE,
320             'America/Guyana'                        => TRUE,
321             'America/Halifax'                       => TRUE,
322             'America/Havana'                        => TRUE,
323             'America/Hermosillo'                    => TRUE,
324             'America/Indiana/Indianapolis'          => TRUE,
325             'America/Indiana/Knox'                  => TRUE,
326             'America/Indiana/Marengo'               => TRUE,
327             'America/Indiana/Petersburg'            => TRUE,
328             'America/Indiana/Tell_City'             => TRUE,
329             'America/Indiana/Vevay'                 => TRUE,
330             'America/Indiana/Vincennes'             => TRUE,
331             'America/Indiana/Winamac'               => TRUE,
332             'America/Indianapolis'                  => TRUE,
333             'America/Inuvik'                        => TRUE,
334             'America/Iqaluit'                       => TRUE,
335             'America/Jamaica'                       => TRUE,
336             'America/Jujuy'                         => TRUE,
337             'America/Juneau'                        => TRUE,
338             'America/Kentucky/Louisville'           => TRUE,
339             'America/Kentucky/Monticello'           => TRUE,
340             'America/Knox_IN'                       => TRUE,
341             'America/La_Paz'                        => TRUE,
342             'America/Lima'                          => TRUE,
343             'America/Los_Angeles'                   => TRUE,
344             'America/Louisville'                    => TRUE,
345             'America/Maceio'                        => TRUE,
346             'America/Managua'                       => TRUE,
347             'America/Manaus'                        => TRUE,
348             'America/Marigot'                       => TRUE,
349             'America/Martinique'                    => TRUE,
350             'America/Mazatlan'                      => TRUE,
351             'America/Mendoza'                       => TRUE,
352             'America/Menominee'                     => TRUE,
353             'America/Merida'                        => TRUE,
354             'America/Mexico_City'                   => TRUE,
355             'America/Miquelon'                      => TRUE,
356             'America/Moncton'                       => TRUE,
357             'America/Monterrey'                     => TRUE,
358             'America/Montevideo'                    => TRUE,
359             'America/Montreal'                      => TRUE,
360             'America/Montserrat'                    => TRUE,
361             'America/Nassau'                        => TRUE,
362             'America/New_York'                      => TRUE,
363             'America/Nipigon'                       => TRUE,
364             'America/Nome'                          => TRUE,
365             'America/Noronha'                       => TRUE,
366             'America/North_Dakota/Center'           => TRUE,
367             'America/North_Dakota/New_Salem'        => TRUE,
368             'America/Panama'                        => TRUE,
369             'America/Pangnirtung'                   => TRUE,
370             'America/Paramaribo'                    => TRUE,
371             'America/Phoenix'                       => TRUE,
372             'America/Port-au-Prince'                => TRUE,
373             'America/Port_of_Spain'                 => TRUE,
374             'America/Porto_Acre'                    => TRUE,
375             'America/Porto_Velho'                   => TRUE,
376             'America/Puerto_Rico'                   => TRUE,
377             'America/Rainy_River'                   => TRUE,
378             'America/Rankin_Inlet'                  => TRUE,
379             'America/Recife'                        => TRUE,
380             'America/Regina'                        => TRUE,
381             'America/Resolute'                      => TRUE,
382             'America/Rio_Branco'                    => TRUE,
383             'America/Rosario'                       => TRUE,
384             'America/Santiago'                      => TRUE,
385             'America/Santo_Domingo'                 => TRUE,
386             'America/Sao_Paulo'                     => TRUE,
387             'America/Scoresbysund'                  => TRUE,
388             'America/Shiprock'                      => TRUE,
389             'America/St_Barthelemy'                 => TRUE,
390             'America/St_Johns'                      => TRUE,
391             'America/St_Kitts'                      => TRUE,
392             'America/St_Lucia'                      => TRUE,
393             'America/St_Thomas'                     => TRUE,
394             'America/St_Vincent'                    => TRUE,
395             'America/Swift_Current'                 => TRUE,
396             'America/Tegucigalpa'                   => TRUE,
397             'America/Thule'                         => TRUE,
398             'America/Thunder_Bay'                   => TRUE,
399             'America/Tijuana'                       => TRUE,
400             'America/Toronto'                       => TRUE,
401             'America/Tortola'                       => TRUE,
402             'America/Vancouver'                     => TRUE,
403             'America/Virgin'                        => TRUE,
404             'America/Whitehorse'                    => TRUE,
405             'America/Winnipeg'                      => TRUE,
406             'America/Yakutat'                       => TRUE,
407             'America/Yellowknife'                   => TRUE,
408             'Antarctica/Casey'                      => TRUE,
409             'Antarctica/Davis'                      => TRUE,
410             'Antarctica/DumontDUrville'             => TRUE,
411             'Antarctica/Mawson'                     => TRUE,
412             'Antarctica/McMurdo'                    => TRUE,
413             'Antarctica/Palmer'                     => TRUE,
414             'Antarctica/Rothera'                    => TRUE,
415             'Antarctica/South_Pole'                 => TRUE,
416             'Antarctica/Syowa'                      => TRUE,
417             'Antarctica/Vostok'                     => TRUE,
418             'Arctic/Longyearbyen'                   => TRUE,
419             'Asia/Aden'                             => TRUE,
420             'Asia/Almaty'                           => TRUE,
421             'Asia/Amman'                            => TRUE,
422             'Asia/Anadyr'                           => TRUE,
423             'Asia/Aqtau'                            => TRUE,
424             'Asia/Aqtobe'                           => TRUE,
425             'Asia/Ashgabat'                         => TRUE,
426             'Asia/Ashkhabad'                        => TRUE,
427             'Asia/Baghdad'                          => TRUE,
428             'Asia/Bahrain'                          => TRUE,
429             'Asia/Baku'                             => TRUE,
430             'Asia/Bangkok'                          => TRUE,
431             'Asia/Beirut'                           => TRUE,
432             'Asia/Bishkek'                          => TRUE,
433             'Asia/Brunei'                           => TRUE,
434             'Asia/Calcutta'                         => TRUE,
435             'Asia/Choibalsan'                       => TRUE,
436             'Asia/Chongqing'                        => TRUE,
437             'Asia/Chungking'                        => TRUE,
438             'Asia/Colombo'                          => TRUE,
439             'Asia/Dacca'                            => TRUE,
440             'Asia/Damascus'                         => TRUE,
441             'Asia/Dhaka'                            => TRUE,
442             'Asia/Dili'                             => TRUE,
443             'Asia/Dubai'                            => TRUE,
444             'Asia/Dushanbe'                         => TRUE,
445             'Asia/Gaza'                             => TRUE,
446             'Asia/Harbin'                           => TRUE,
447             'Asia/Ho_Chi_Minh'                      => TRUE,
448             'Asia/Hong_Kong'                        => TRUE,
449             'Asia/Hovd'                             => TRUE,
450             'Asia/Irkutsk'                          => TRUE,
451             'Asia/Istanbul'                         => TRUE,
452             'Asia/Jakarta'                          => TRUE,
453             'Asia/Jayapura'                         => TRUE,
454             'Asia/Jerusalem'                        => TRUE,
455             'Asia/Kabul'                            => TRUE,
456             'Asia/Kamchatka'                        => TRUE,
457             'Asia/Karachi'                          => TRUE,
458             'Asia/Kashgar'                          => TRUE,
459             'Asia/Katmandu'                         => TRUE,
460             'Asia/Kolkata'                          => TRUE,
461             'Asia/Krasnoyarsk'                      => TRUE,
462             'Asia/Kuala_Lumpur'                     => TRUE,
463             'Asia/Kuching'                          => TRUE,
464             'Asia/Kuwait'                           => TRUE,
465             'Asia/Macao'                            => TRUE,
466             'Asia/Macau'                            => TRUE,
467             'Asia/Magadan'                          => TRUE,
468             'Asia/Makassar'                         => TRUE,
469             'Asia/Manila'                           => TRUE,
470             'Asia/Muscat'                           => TRUE,
471             'Asia/Nicosia'                          => TRUE,
472             'Asia/Novosibirsk'                      => TRUE,
473             'Asia/Omsk'                             => TRUE,
474             'Asia/Oral'                             => TRUE,
475             'Asia/Phnom_Penh'                       => TRUE,
476             'Asia/Pontianak'                        => TRUE,
477             'Asia/Pyongyang'                        => TRUE,
478             'Asia/Qatar'                            => TRUE,
479             'Asia/Qyzylorda'                        => TRUE,
480             'Asia/Rangoon'                          => TRUE,
481             'Asia/Riyadh'                           => TRUE,
482             'Asia/Saigon'                           => TRUE,
483             'Asia/Sakhalin'                         => TRUE,
484             'Asia/Samarkand'                        => TRUE,
485             'Asia/Seoul'                            => TRUE,
486             'Asia/Shanghai'                         => TRUE,
487             'Asia/Singapore'                        => TRUE,
488             'Asia/Taipei'                           => TRUE,
489             'Asia/Tashkent'                         => TRUE,
490             'Asia/Tbilisi'                          => TRUE,
491             'Asia/Tehran'                           => TRUE,
492             'Asia/Tel_Aviv'                         => TRUE,
493             'Asia/Thimbu'                           => TRUE,
494             'Asia/Thimphu'                          => TRUE,
495             'Asia/Tokyo'                            => TRUE,
496             'Asia/Ujung_Pandang'                    => TRUE,
497             'Asia/Ulaanbaatar'                      => TRUE,
498             'Asia/Ulan_Bator'                       => TRUE,
499             'Asia/Urumqi'                           => TRUE,
500             'Asia/Vientiane'                        => TRUE,
501             'Asia/Vladivostok'                      => TRUE,
502             'Asia/Yakutsk'                          => TRUE,
503             'Asia/Yekaterinburg'                    => TRUE,
504             'Asia/Yerevan'                          => TRUE,
505             'Atlantic/Azores'                       => TRUE,
506             'Atlantic/Bermuda'                      => TRUE,
507             'Atlantic/Canary'                       => TRUE,
508             'Atlantic/Cape_Verde'                   => TRUE,
509             'Atlantic/Faeroe'                       => TRUE,
510             'Atlantic/Faroe'                        => TRUE,
511             'Atlantic/Jan_Mayen'                    => TRUE,
512             'Atlantic/Madeira'                      => TRUE,
513             'Atlantic/Reykjavik'                    => TRUE,
514             'Atlantic/South_Georgia'                => TRUE,
515             'Atlantic/St_Helena'                    => TRUE,
516             'Atlantic/Stanley'                      => TRUE,
517             'Australia/ACT'                         => TRUE,
518             'Australia/Adelaide'                    => TRUE,
519             'Australia/Brisbane'                    => TRUE,
520             'Australia/Broken_Hill'                 => TRUE,
521             'Australia/Canberra'                    => TRUE,
522             'Australia/Currie'                      => TRUE,
523             'Australia/Darwin'                      => TRUE,
524             'Australia/Eucla'                       => TRUE,
525             'Australia/Hobart'                      => TRUE,
526             'Australia/LHI'                         => TRUE,
527             'Australia/Lindeman'                    => TRUE,
528             'Australia/Lord_Howe'                   => TRUE,
529             'Australia/Melbourne'                   => TRUE,
530             'Australia/North'                       => TRUE,
531             'Australia/NSW'                         => TRUE,
532             'Australia/Perth'                       => TRUE,
533             'Australia/Queensland'                  => TRUE,
534             'Australia/South'                       => TRUE,
535             'Australia/Sydney'                      => TRUE,
536             'Australia/Tasmania'                    => TRUE,
537             'Australia/Victoria'                    => TRUE,
538             'Australia/West'                        => TRUE,
539             'Australia/Yancowinna'                  => TRUE,
540             'Europe/Amsterdam'                      => TRUE,
541             'Europe/Andorra'                        => TRUE,
542             'Europe/Athens'                         => TRUE,
543             'Europe/Belfast'                        => TRUE,
544             'Europe/Belgrade'                       => TRUE,
545             'Europe/Berlin'                         => TRUE,
546             'Europe/Bratislava'                     => TRUE,
547             'Europe/Brussels'                       => TRUE,
548             'Europe/Bucharest'                      => TRUE,
549             'Europe/Budapest'                       => TRUE,
550             'Europe/Chisinau'                       => TRUE,
551             'Europe/Copenhagen'                     => TRUE,
552             'Europe/Dublin'                         => TRUE,
553             'Europe/Gibraltar'                      => TRUE,
554             'Europe/Guernsey'                       => TRUE,
555             'Europe/Helsinki'                       => TRUE,
556             'Europe/Isle_of_Man'                    => TRUE,
557             'Europe/Istanbul'                       => TRUE,
558             'Europe/Jersey'                         => TRUE,
559             'Europe/Kaliningrad'                    => TRUE,
560             'Europe/Kiev'                           => TRUE,
561             'Europe/Lisbon'                         => TRUE,
562             'Europe/Ljubljana'                      => TRUE,
563             'Europe/London'                         => TRUE,
564             'Europe/Luxembourg'                     => TRUE,
565             'Europe/Madrid'                         => TRUE,
566             'Europe/Malta'                          => TRUE,
567             'Europe/Mariehamn'                      => TRUE,
568             'Europe/Minsk'                          => TRUE,
569             'Europe/Monaco'                         => TRUE,
570             'Europe/Moscow'                         => TRUE,
571             'Europe/Nicosia'                        => TRUE,
572             'Europe/Oslo'                           => TRUE,
573             'Europe/Paris'                          => TRUE,
574             'Europe/Podgorica'                      => TRUE,
575             'Europe/Prague'                         => TRUE,
576             'Europe/Riga'                           => TRUE,
577             'Europe/Rome'                           => TRUE,
578             'Europe/Samara'                         => TRUE,
579             'Europe/San_Marino'                     => TRUE,
580             'Europe/Sarajevo'                       => TRUE,
581             'Europe/Simferopol'                     => TRUE,
582             'Europe/Skopje'                         => TRUE,
583             'Europe/Sofia'                          => TRUE,
584             'Europe/Stockholm'                      => TRUE,
585             'Europe/Tallinn'                        => TRUE,
586             'Europe/Tirane'                         => TRUE,
587             'Europe/Tiraspol'                       => TRUE,
588             'Europe/Uzhgorod'                       => TRUE,
589             'Europe/Vaduz'                          => TRUE,
590             'Europe/Vatican'                        => TRUE,
591             'Europe/Vienna'                         => TRUE,
592             'Europe/Vilnius'                        => TRUE,
593             'Europe/Volgograd'                      => TRUE,
594             'Europe/Warsaw'                         => TRUE,
595             'Europe/Zagreb'                         => TRUE,
596             'Europe/Zaporozhye'                     => TRUE,
597             'Europe/Zurich'                         => TRUE,
598             'Indian/Antananarivo'                   => TRUE,
599             'Indian/Chagos'                         => TRUE,
600             'Indian/Christmas'                      => TRUE,
601             'Indian/Cocos'                          => TRUE,
602             'Indian/Comoro'                         => TRUE,
603             'Indian/Kerguelen'                      => TRUE,
604             'Indian/Mahe'                           => TRUE,
605             'Indian/Maldives'                       => TRUE,
606             'Indian/Mauritius'                      => TRUE,
607             'Indian/Mayotte'                        => TRUE,
608             'Indian/Reunion'                        => TRUE,
609             'Pacific/Apia'                          => TRUE,
610             'Pacific/Auckland'                      => TRUE,
611             'Pacific/Chatham'                       => TRUE,
612             'Pacific/Easter'                        => TRUE,
613             'Pacific/Efate'                         => TRUE,
614             'Pacific/Enderbury'                     => TRUE,
615             'Pacific/Fakaofo'                       => TRUE,
616             'Pacific/Fiji'                          => TRUE,
617             'Pacific/Funafuti'                      => TRUE,
618             'Pacific/Galapagos'                     => TRUE,
619             'Pacific/Gambier'                       => TRUE,
620             'Pacific/Guadalcanal'                   => TRUE,
621             'Pacific/Guam'                          => TRUE,
622             'Pacific/Honolulu'                      => TRUE,
623             'Pacific/Johnston'                      => TRUE,
624             'Pacific/Kiritimati'                    => TRUE,
625             'Pacific/Kosrae'                        => TRUE,
626             'Pacific/Kwajalein'                     => TRUE,
627             'Pacific/Majuro'                        => TRUE,
628             'Pacific/Marquesas'                     => TRUE,
629             'Pacific/Midway'                        => TRUE,
630             'Pacific/Nauru'                         => TRUE,
631             'Pacific/Niue'                          => TRUE,
632             'Pacific/Norfolk'                       => TRUE,
633             'Pacific/Noumea'                        => TRUE,
634             'Pacific/Pago_Pago'                     => TRUE,
635             'Pacific/Palau'                         => TRUE,
636             'Pacific/Pitcairn'                      => TRUE,
637             'Pacific/Ponape'                        => TRUE,
638             'Pacific/Port_Moresby'                  => TRUE,
639             'Pacific/Rarotonga'                     => TRUE,
640             'Pacific/Saipan'                        => TRUE,
641             'Pacific/Samoa'                         => TRUE,
642             'Pacific/Tahiti'                        => TRUE,
643             'Pacific/Tarawa'                        => TRUE,
644             'Pacific/Tongatapu'                     => TRUE,
645             'Pacific/Truk'                          => TRUE,
646             'Pacific/Wake'                          => TRUE,
647             'Pacific/Wallis'                        => TRUE
648         );
649        
650         return isset($valid_timezones[$timezone]);
651     }
652    
653    
654     /**
655     * Allows setting a callback to translate or modify any return values from ::format(), fDate::format() and fTime::format()
656     *
657     * @param  callback $callback  The callback to pass all formatted dates/times/timestamps through. Should accept a single string and return a single string.
658     * @return void
659     */
660     static public function registerFormatCallback($callback)
661     {
662         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
663             $callback = explode('::', $callback);   
664         }
665         self::$format_callback = $callback;
666     }
667    
668    
669     /**
670     * Allows setting a callback to parse any date strings passed into ::__construct(), fDate::__construct() and fTime::__construct()
671     *
672     * @param  callback $callback  The callback to pass all date/time/timestamp strings through. Should accept a single string and return a single string that is parsable by [http://php.net/strtotime `strtotime()`].
673     * @return void
674     */
675     static public function registerUnformatCallback($callback)
676     {
677         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
678             $callback = explode('::', $callback);   
679         }
680         self::$unformat_callback = $callback;
681     }
682    
683    
684     /**
685     * Resets the configuration of the class
686     *
687     * @internal
688      *
689     * @return void
690     */
691     static public function reset()
692     {
693         self::$formats         = array();
694         self::$format_callback = NULL;
695     }
696    
697    
698     /**
699     * Provides a consistent interface to setting the default timezone. Wraps the [http://php.net/date_default_timezone_set date_default_timezone_set()] function.
700     *
701     * @param  string $timezone  The default timezone to use for all date/time calculations
702     * @return void
703     */
704     static public function setDefaultTimezone($timezone)
705     {
706         self::checkPHPVersion();
707        
708         $result = date_default_timezone_set($timezone);
709         if (!$result) {
710             throw new fProgrammerException(
711                 'The timezone specified, %s, is not a valid timezone',
712                 $timezone
713             );
714         }
715     }
716    
717    
718     /**
719     * Takes a format name set via ::defineFormat() and returns the [http://php.net/date date()] function formatting string
720     *
721     * @internal
722      *
723     * @param  string $format  The format to translate
724     * @return string  The formatting string. If no matching format was found, this will be the same as the `$format` parameter.
725     */
726     static public function translateFormat($format)
727     {
728         if (isset(self::$formats[$format])) {
729             $format = self::$formats[$format];
730         }
731         return $format;
732     }
733    
734    
735     /**
736     * The date/time
737     *
738     * @var integer
739     */
740     private $timestamp;
741    
742     /**
743     * The timezone for this date/time
744     *
745     * @var string
746     */
747     private $timezone;
748    
749    
750     /**
751     * Creates the date/time to represent
752     *
753     * @throws fValidationException  When `$datetime` is not a valid date/time, date or time value
754     *
755     * @param  fTimestamp|object|string|integer $datetime  The date/time to represent, `NULL` is interpreted as now
756     * @param  string $timezone  The timezone for the date/time. This causes the date/time to be interpretted as being in the specified timezone. If not specified, will default to timezone set by ::setDefaultTimezone().
757     * @return fTimestamp
758     */
759     public function __construct($datetime=NULL, $timezone=NULL)
760     {
761         self::checkPHPVersion();
762        
763         $default_tz = date_default_timezone_get();
764        
765         if ($timezone) {
766             if (!self::isValidTimezone($timezone)) {
767                 throw new fValidationException(
768                     'The timezone specified, %s, is not a valid timezone',
769                     $timezone
770                 );
771             }
772            
773         } elseif ($datetime instanceof fTimestamp) {
774             $timezone = $datetime->timezone;
775            
776         } else {
777             $timezone = $default_tz;
778         }
779        
780         $this->timezone = $timezone;
781        
782         if ($datetime === NULL) {
783             $timestamp = time();
784         } elseif (is_numeric($datetime) && ctype_digit($datetime)) {
785             $timestamp = (int) $datetime;
786         } elseif (is_string($datetime) && in_array(strtoupper($datetime), array('CURRENT_TIMESTAMP', 'CURRENT_TIME'))) {
787             $timestamp = time();
788         } elseif (is_string($datetime) && strtoupper($datetime) == 'CURRENT_DATE') {
789             $timestamp = strtotime(date('Y-m-d'));
790         } else {
791             if (is_object($datetime) && is_callable(array($datetime, '__toString'))) {
792                 $datetime = $datetime->__toString();   
793             } elseif (is_numeric($datetime) || is_object($datetime)) {
794                 $datetime = (string) $datetime;   
795             }
796            
797             $datetime = self::callUnformatCallback($datetime);
798            
799             if ($timezone != $default_tz) {
800                 date_default_timezone_set($timezone);
801             }
802             $timestamp = strtotime(self::fixISOWeek($datetime));
803             if ($timezone != $default_tz) {
804                 date_default_timezone_set($default_tz);
805             }
806         }
807        
808         if ($timestamp === FALSE) {
809             throw new fValidationException(
810                 'The date/time specified, %s, does not appear to be a valid date/time',
811                 $datetime
812             );
813         }
814        
815         $this->timestamp = $timestamp;
816     }
817    
818    
819     /**
820     * All requests that hit this method should be requests for callbacks
821     *
822     * @internal
823      *
824     * @param  string $method  The method to create a callback for
825     * @return callback  The callback for the method requested
826     */
827     public function __get($method)
828     {
829         return array($this, $method);       
830     }
831    
832    
833     /**
834     * Returns this date/time
835     *
836     * @return string  The `'Y-m-d H:i:s'` format of this date/time
837     */
838     public function __toString()
839     {
840         return $this->format('Y-m-d H:i:s');
841     }
842    
843    
844     /**
845     * Changes the date/time by the adjustment specified
846     *
847     * @throws fValidationException  When `$adjustment` is not a valid relative date/time measurement or timezone
848     *
849     * @param  string $adjustment  The adjustment to make - may be a relative adjustment or a different timezone
850     * @return fTimestamp  The adjusted date/time
851     */
852     public function adjust($adjustment)
853     {
854         if (self::isValidTimezone($adjustment)) {
855             $timezone  = $adjustment;
856             $timestamp = $this->timestamp;
857        
858         } else {
859             $timezone  = $this->timezone;
860             $timestamp = strtotime($adjustment, $this->timestamp);
861            
862             if ($timestamp === FALSE || $timestamp === -1) {
863                 throw new fValidationException(
864                     'The adjustment specified, %s, does not appear to be a valid relative date/time measurement',
865                     $adjustment
866                 );
867             }
868         }
869        
870         return new fTimestamp($timestamp, $timezone);
871     }
872    
873    
874     /**
875     * If this timestamp is equal to the timestamp passed
876     *
877     * @param  fTimestamp|object|string|integer $other_timestamp  The timestamp to compare with, `NULL` is interpreted as today
878     * @return boolean  If this timestamp is equal to the one passed
879     */
880     public function eq($other_timestamp=NULL)
881     {
882         $other_timestamp = new fTimestamp($other_timestamp);
883         return $this->timestamp == $other_timestamp->timestamp;
884     }
885    
886    
887     /**
888     * Formats the date/time
889     *
890     * @param  string $format  The [http://php.net/date date()] function compatible formatting string, or a format name from ::defineFormat()
891     * @return string  The formatted date/time
892     */
893     public function format($format)
894     {
895         $format = self::translateFormat($format);
896        
897         $default_tz = date_default_timezone_get();
898         date_default_timezone_set($this->timezone);
899        
900         $formatted = date($format, $this->timestamp);
901        
902         date_default_timezone_set($default_tz);
903        
904         return self::callFormatCallback($formatted);
905     }
906    
907    
908     /**
909     * Returns the approximate difference in time, discarding any unit of measure but the least specific.
910     *
911     * The output will read like:
912     *
913     *  - "This timestamp is `{return value}` the provided one" when a timestamp it passed
914     *  - "This timestamp is `{return value}`" when no timestamp is passed and comparing with the current timestamp
915     *
916     * Examples of output for a timestamp passed might be:
917     *
918     *  - `'5 minutes after'`
919     *  - `'2 hours before'`
920     *  - `'2 days after'`
921     *  - `'at the same time'`
922     *
923     * Examples of output for no timestamp passed might be:
924     *
925     *  - `'5 minutes ago'`
926     *  - `'2 hours ago'`
927     *  - `'2 days from now'`
928     *  - `'1 year ago'`
929     *  - `'right now'`
930     *
931     * You would never get the following output since it includes more than one unit of time measurement:
932     *
933     *  - `'5 minutes and 28 seconds'`
934     *  - `'3 weeks, 1 day and 4 hours'`
935     *
936     * Values that are close to the next largest unit of measure will be rounded up:
937     *
938     *  - `'55 minutes'` would be represented as `'1 hour'`, however `'45 minutes'` would not
939     *  - `'29 days'` would be represented as `'1 month'`, but `'21 days'` would be shown as `'3 weeks'`
940     *
941     * @param  fTimestamp|object|string|integer $other_timestamp  The timestamp to create the difference with, `NULL` is interpreted as now
942     * @param  boolean                          $simple           When `TRUE`, the returned value will only include the difference in the two timestamps, but not `from now`, `ago`, `after` or `before`
943     * @param  boolean                          :$simple
944     * @return string  The fuzzy difference in time between the this timestamp and the one provided
945     */
946     public function getFuzzyDifference($other_timestamp=NULL, $simple=FALSE)
947     {
948         if (is_bool($other_timestamp)) {
949             $simple          = $other_timestamp;
950             $other_timestamp = NULL;
951         }
952        
953         $relative_to_now = FALSE;
954         if ($other_timestamp === NULL) {
955             $relative_to_now = TRUE;
956         }
957         $other_timestamp = new fTimestamp($other_timestamp);
958        
959         $diff = $this->timestamp - $other_timestamp->timestamp;
960        
961         if (abs($diff) < 10) {
962             if ($relative_to_now) {
963                 return self::compose('right now');
964             }
965             return self::compose('at the same time');
966         }
967        
968         $break_points = array(
969             /* 45 seconds  */
970             45         => array(1,        self::compose('second'), self::compose('seconds')),
971             /* 45 minutes  */
972             2700       => array(60,       self::compose('minute'), self::compose('minutes')),
973             /* 18 hours    */
974             64800      => array(3600,     self::compose('hour'),   self::compose('hours')),
975             /* 5 days      */
976             432000     => array(86400,    self::compose('day'),    self::compose('days')),
977             /* 3 weeks     */
978             1814400    => array(604800,   self::compose('week'),   self::compose('weeks')),
979             /* 9 months    */
980             23328000   => array(2592000self::compose('month')self::compose('months')),
981             /* largest int */
982             2147483647 => array(31536000, self::compose('year'),   self::compose('years'))
983         );
984        
985         foreach ($break_points as $break_point => $unit_info) {
986             if (abs($diff) > $break_point) { continue; }
987            
988             $unit_diff = round(abs($diff)/$unit_info[0]);
989             $units     = fGrammar::inflectOnQuantity($unit_diff, $unit_info[1], $unit_info[2]);
990             break;
991         }
992        
993         if ($simple) {
994             return self::compose('%1$s %2$s', $unit_diff, $units);
995         }
996        
997         if ($relative_to_now) {
998             if ($diff > 0) {
999                 return self::compose('%1$s %2$s from now', $unit_diff, $units);
1000             }
1001        
1002             return self::compose('%1$s %2$s ago', $unit_diff, $units);
1003         }
1004        
1005         if ($diff > 0) {
1006             return self::compose('%1$s %2$s after', $unit_diff, $units);
1007         }
1008        
1009         return self::compose('%1$s %2$s before', $unit_diff, $units);
1010     }
1011    
1012    
1013     /**
1014     * If this timestamp is greater than the timestamp passed
1015     *
1016     * @param  fTimestamp|object|string|integer $other_timestamp  The timestamp to compare with, `NULL` is interpreted as now
1017     * @return boolean  If this timestamp is greater than the one passed
1018     */
1019     public function gt($other_timestamp=NULL)
1020     {
1021         $other_timestamp = new fTimestamp($other_timestamp);
1022         return $this->timestamp > $other_timestamp->timestamp;
1023     }
1024    
1025    
1026     /**
1027     * If this timestamp is greater than or equal to the timestamp passed
1028     *
1029     * @param  fTimestamp|object|string|integer $other_timestamp  The timestamp to compare with, `NULL` is interpreted as now
1030     * @return boolean  If this timestamp is greater than or equal to the one passed
1031     */
1032     public function gte($other_timestamp=NULL)
1033     {
1034         $other_timestamp = new fTimestamp($other_timestamp);
1035         return $this->timestamp >= $other_timestamp->timestamp;
1036     }
1037    
1038    
1039     /**
1040     * If this timestamp is less than the timestamp passed
1041     *
1042     * @param  fTimestamp|object|string|integer $other_timestamp  The timestamp to compare with, `NULL` is interpreted as today
1043     * @return boolean  If this timestamp is less than the one passed
1044     */
1045     public function lt($other_timestamp=NULL)
1046     {
1047         $other_timestamp = new fTimestamp($other_timestamp);
1048         return $this->timestamp < $other_timestamp->timestamp;
1049     }
1050    
1051    
1052     /**
1053     * If this timestamp is less than or equal to the timestamp passed
1054     *
1055     * @param  fTimestamp|object|string|integer $other_timestamp  The timestamp to compare with, `NULL` is interpreted as today
1056     * @return boolean  If this timestamp is less than or equal to the one passed
1057     */
1058     public function lte($other_timestamp=NULL)
1059     {
1060         $other_timestamp = new fTimestamp($other_timestamp);
1061         return $this->timestamp <= $other_timestamp->timestamp;
1062     }
1063    
1064    
1065     /**
1066     * Modifies the current timestamp, creating a new fTimestamp object
1067     *
1068     * The purpose of this method is to allow for easy creation of a timestamp
1069     * based on this timestamp. Below are some examples of formats to
1070     * modify the current timestamp:
1071     *
1072     *  - `'Y-m-01 H:i:s'` to change the date of the timestamp to the first of the month:
1073     *  - `'Y-m-t H:i:s'` to change the date of the timestamp to the last of the month:
1074     *  - `'Y-m-d 17:i:s'` to set the hour of the timestamp to 5 PM:
1075     *
1076     * @param  string $format    The current timestamp will be formatted with this string, and the output used to create a new object. The format should **not** include the timezone (character `e`).
1077     * @param  string $timezone  The timezone for the new object if different from the current timezone
1078     * @return fTimestamp  The new timestamp
1079     */
1080     public function modify($format, $timezone=NULL)
1081     {
1082         $timezone = ($timezone !== NULL) ? $timezone : $this->timezone;
1083         return new fTimestamp($this->format($format), $timezone);
1084     }
1085 }
1086  
1087  
1088  
1089 /**
1090  * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>
1091  *
1092  * Permission is hereby granted, free of charge, to any person obtaining a copy
1093  * of this software and associated documentation files (the "Software"), to deal
1094  * in the Software without restriction, including without limitation the rights
1095  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1096  * copies of the Software, and to permit persons to whom the Software is
1097  * furnished to do so, subject to the following conditions:
1098  *
1099  * The above copyright notice and this permission notice shall be included in
1100  * all copies or substantial portions of the Software.
1101  *
1102  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1103  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1104  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1105  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1106  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1107  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1108  * THE SOFTWARE.
1109  */