1<?php
2/* vim: set expandtab tabstop=4 shiftwidth=4: */
3/**
4 * Driver.php
5 *
6 * PHP Version 5
7 *
8 * Copyright (c) 1997-2008 The PHP Group
9 *
10 * This source file is subject to version 2.0 of the PHP license,
11 * that is bundled with this package in the file LICENSE, and is
12 * available at through the world-wide-web at
13 * http://www.php.net/license/2_02.txt.
14 * If you did not receive a copy of the PHP license and are unable to
15 * obtain it through the world-wide-web, please send a note to
16 * license@php.net so we can mail you a copy immediately.
17 *
18 * Authors:   Carsten Lucke <luckec@tool-garage.de>
19 *
20 * CVS file id: $Id$
21 *
22 * @category Date
23 * @package  Date_Holidays
24 * @author   Carsten Lucke <luckec@tool-garage.de>
25 * @license  http://www.php.net/license/3_01.txt PHP License 3.0.1
26 * @version  CVS: $Id$
27 * @link     http://pear.php.net/package/Date_Holidays
28 */
29
30/**
31 * DriverClass and associated defines.
32 *
33 * @abstract
34 * @category Date
35 * @package  Date_Holidays
36 * @author   Carsten Lucke <luckec@tool-garage.de>
37 * @license  http://www.php.net/license/3_01.txt PHP License 3.0.1
38 * @version  CVS: $Id$
39 * @link     http://pear.php.net/package/Date_Holidays
40 */
41
42/**
43 * uses PEAR_Errorstack
44 */
45require_once 'PEAR/ErrorStack.php';
46require_once 'Date/Holidays/Filter.php';
47require_once 'Date/Holidays/Filter/Whitelist.php';
48require_once 'Date/Holidays/Filter/Blacklist.php';
49
50/**
51 * invalid internal name
52 *
53 * @access  public
54 */
55define('DATE_HOLIDAYS_INVALID_INTERNAL_NAME', 51);
56
57/**
58 * title for a holiday is not available
59 *
60 * @access  public
61 */
62define('DATE_HOLIDAYS_TITLE_UNAVAILABLE', 52);
63
64/**
65 * date could not be converted into a PEAR::Date object
66 *
67 * date was neither a timestamp nor a string
68 *
69 * @access  public
70 * @deprecated   will certainly be removed
71 */
72define('DATE_HOLIDAYS_INVALID_DATE', 53);
73
74/**
75 * string that represents a date has wrong format
76 *
77 * format must be YYYY-MM-DD
78 *
79 * @access  public
80 * @deprecated   will certainly be removed
81 */
82define('DATE_HOLIDAYS_INVALID_DATE_FORMAT', 54);
83
84/**
85 * date for a holiday is not available
86 *
87 * @access  public
88 */
89define('DATE_HOLIDAYS_DATE_UNAVAILABLE', 55);
90
91/**
92 * language-file doesn't exist
93 *
94 * @access  public
95 */
96define('DATE_HOLIDAYS_LANGUAGEFILE_NOT_FOUND', 56);
97
98/**
99 * unable to read language-file
100 *
101 * @access  public
102 */
103define('DATE_HOLIDAYS_UNABLE_TO_READ_TRANSLATIONDATA', 57);
104
105/**
106 * Name of the static {@link Date_Holidays_Driver} method returning
107 * a array of possible ISO3166 codes that identify itself.
108 *
109 * @access  public
110 */
111define('DATE_HOLIDAYS_DRIVER_IDENTIFY_ISO3166_METHOD', 'getISO3166Codes');
112
113/**
114 * class that helps you to locate holidays for a year
115 *
116 * @abstract
117 * @category   Date
118 * @package    Date_Holidays
119 * @subpackage Driver
120 * @author     Carsten Lucke <luckec@tool-garage.de>
121 * @license    http://www.php.net/license/3_01.txt PHP License 3.0.1
122 * @version    CVS: $Id$
123 * @link       http://pear.php.net/package/Date_Holidays
124 */
125class Date_Holidays_Driver
126{
127    /**
128     * this driver's name
129     *
130     * @access   protected
131     * @var      string
132     */
133    var $_driverName;
134
135    /**
136     * locale setting for output
137     *
138     * @access   protected
139     * @var      string
140     */
141    var $_locale;
142
143    /**
144     * locales for which translations of holiday titles are available
145     *
146     * @access   private
147     * @var      array
148     */
149    var $_availableLocales = array('C');
150
151    /**
152     * object's current year
153     *
154     * @access   protected
155     * @var      int
156     */
157    var $_year;
158
159    /**
160     * internal names for the available holidays
161     *
162     * @access   protected
163     * @var      array
164     */
165    var $_internalNames = array();
166
167    /**
168     * dates of the available holidays
169     *
170     * @access   protected
171     * @var      array
172     */
173    var $_dates = array();
174
175    /**
176     * array of the available holidays indexed by date
177     *
178     * @access   protected
179     * @var      array
180     */
181    var $_holidays = array();
182
183    /**
184     * localized names of the available holidays
185     *
186     * @access   protected
187     * @var      array
188     */
189    var $_titles = array();
190
191    /**
192     * Array of holiday-properties indexed by internal-names and
193     * furthermore by locales.
194     *
195     * <code>
196     * $_holidayProperties = array(
197     *       'internalName1' =>  array(
198     *                               'de_DE' => array(),
199     *                               'en_US' => array(),
200     *                               'fr_FR' => array()
201     *                           )
202     *       'internalName2' =>  array(
203     *                               'de_DE' => array(),
204     *                               'en_US' => array(),
205     *                               'fr_FR' => array()
206     *                           )
207     * );
208     * </code>
209     */
210    var $_holidayProperties = array();
211
212    /**
213     * Constructor
214     *
215     * Use the Date_Holidays::factory() method to construct an object of a
216     * certain driver
217     *
218     * @access   protected
219     */
220    function Date_Holidays_Driver()
221    {
222    }
223
224    /**
225     * Method that returns an array containing the ISO3166 codes that may possibly
226     * identify a driver.
227     *
228     * @static
229     * @access public
230     * @return array possible ISO3166 codes
231     */
232    function getISO3166Codes()
233    {
234        return array();
235    }
236
237    /**
238     * Sets the driver's current year
239     *
240     * Calling this method forces the object to rebuild the holidays
241     *
242     * @param int $year year
243     *
244     * @access   public
245     * @return   boolean true on success, otherwise a PEAR_ErrorStack object
246     * @throws   object PEAR_ErrorStack
247     * @uses     _buildHolidays()
248     */
249    function setYear($year)
250    {
251        $this->_year = $year;
252        return $this->_buildHolidays();
253    }
254
255    /**
256     * Returns the driver's current year
257     *
258     * @access   public
259     * @return   int     current year
260     */
261    function getYear()
262    {
263        return $this->_year;
264    }
265
266    /**
267     * Build the internal arrays that contain data about the calculated holidays
268     *
269     * @abstract
270     * @access   protected
271     * @return   boolean true on success, otherwise a PEAR_ErrorStack object
272     * @throws   object PEAR_ErrorStack
273     */
274    function _buildHolidays()
275    {
276    }
277
278    /**
279     * Add a driver component
280     *
281     * @param object $driver Date_Holidays_Driver object
282     *
283     * @abstract
284     * @access public
285     * @return void
286     */
287    function addDriver($driver)
288    {
289    }
290
291    /**
292     * addTranslation
293     *
294     * Search for installed language files appropriate for the specified
295     * locale and add them to the driver
296     *
297     * @param string $locale locale setting to be used
298     *
299     * @access public
300     * @return boolean true on success, otherwise false
301     */
302    function addTranslation($locale)
303    {
304        $data_dir = "@DATA-DIR@";
305        $bestLocale = $this->_findBestLocale($locale);
306        $matches = array();
307        $loaded = false;
308
309        if ($data_dir == '@'.'DATA-DIR'.'@') {
310            $data_dir = dirname(dirname(dirname(__FILE__)));
311            $stubdirs = array(
312                "$data_dir/lang/{$this->_driverName}/",
313                "$data_dir/lang/Christian/");
314        } else {
315            //Christian driver is exceptional...
316            if ($this->_driverName == 'Christian') {
317                $stubdir = "$data_dir/Date_Holidays/lang/Christian/";
318            } else {
319                $stubdir = "$data_dir/Date_Holidays_{$this->_driverName}/lang/{$this->_driverName}/";
320                if (! is_dir($stubdir)) {
321                    $stubdir = $data_dir . "/Date_Holidays/lang/";
322                }
323            }
324            $stubdirs = array(
325                $stubdir,
326                "$data_dir/Date_Holidays_{$this->_driverName}/lang/Christian/");
327        }
328
329        foreach ($stubdirs as $stubdir) {
330            if (is_dir($stubdir)) {
331                if ($dh = opendir($stubdir)) {
332                    while (($file = readdir($dh)) !== false) {
333                        if (strlen($locale) == 5) {
334                            if (((strncasecmp($file, $bestLocale, 5) == 0))
335                                || (strncasecmp($file, $locale, 5) == 0)
336                            ) {
337                                array_push($matches, $file);
338                            }
339                        }
340                        if (strlen($locale) == 2) {
341                            if (((strncasecmp($file, $bestLocale, 2) == 0))
342                                || (strncasecmp($file, $locale, 2) == 0)
343                            ) {
344                                array_push($matches, $file);
345                            }
346                        }
347                    }
348                    closedir($dh);
349                    $forget = array();
350                    sort($matches);
351                    foreach ($matches as $am) {
352                        if (strpos($am, ".ser") !== false) {
353                            $this->addCompiledTranslationFile($stubdir.$am, $locale);
354                            $loaded = true;
355                            array_push($forget, basename($am, ".ser") . ".xml");
356                        } else {
357                            if (!in_array($am, $forget)) {
358                                $this->addTranslationFile(
359                                    $stubdir . $am,
360                                    str_replace(".xml", "", $am)
361                                );
362                                $loaded = true;
363                            }
364                        }
365                    }
366                }
367            }
368        }
369        return $loaded;
370    }
371
372    /**
373     * Remove a driver component
374     *
375     * @param object $driver Date_Holidays_Driver driver-object
376     *
377     * @abstract
378     * @access   public
379     * @return   boolean true on success, otherwise a PEAR_Error object
380     * @throws   object PEAR_Error   DATE_HOLIDAYS_DRIVER_NOT_FOUND
381     */
382    function removeDriver($driver)
383    {
384    }
385
386    /**
387     * Returns the internal names of holidays that were calculated
388     *
389     * @access   public
390     * @return   array
391     */
392    function getInternalHolidayNames()
393    {
394        return $this->_internalNames;
395    }
396
397    /**
398     * Returns localized titles of all holidays or those accepted by the filter
399     *
400     * @param Date_Holidays_Filter $filter filter-object (or an array !DEPRECATED!)
401     * @param string               $locale locale setting that shall be used
402     *                                     by this method
403     *
404     * @access   public
405     * @return   array   $filter array with localized holiday titles on success,
406     *                           otherwise a PEAR_Error object
407     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_INTERNAL_NAME
408     * @uses     getHolidayTitle()
409     */
410    function getHolidayTitles($filter = null, $locale = null)
411    {
412        if (is_null($filter)) {
413            $filter = new Date_Holidays_Filter_Blacklist(array());
414        } elseif (is_array($filter)) {
415            $filter = new Date_Holidays_Filter_Whitelist($filter);
416        }
417
418        $titles =   array();
419
420        foreach ($this->_internalNames as $internalName) {
421            if ($filter->accept($internalName)) {
422                $title = $this->getHolidayTitle($internalName, $locale);
423                if (Date_Holidays::isError($title)) {
424                    return $title;
425                }
426                $titles[$internalName] = $title;
427            }
428        }
429
430        return $titles;
431    }
432
433    /**
434     * Returns localized title for a holiday
435     *
436     * @param string $internalName internal name for holiday
437     * @param string $locale       locale setting to be used by this method
438     *
439     * @access   public
440     * @return   string  title on success, otherwise a PEAR_Error object
441     * @throws   object PEAR_Error DATE_HOLIDAYS_INVALID_INTERNAL_NAME
442     * @throws   object PEAR_Error DATE_HOLIDAYS_TITLE_UNAVAILABLE
443     */
444    function getHolidayTitle($internalName, $locale = null)
445    {
446        if (! in_array($internalName, $this->_internalNames)) {
447            $msg = 'Invalid internal name: ' . $internalName;
448            return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_INTERNAL_NAME,
449                                             $msg);
450
451        }
452
453        if (is_null($locale)) {
454            $locale = $this->_findBestLocale($this->_locale);
455        } else {
456            $locale = $this->_findBestLocale($locale);
457        }
458
459        if (! isset($this->_titles[$locale][$internalName])) {
460            if (Date_Holidays::staticGetProperty('DIE_ON_MISSING_LOCALE')) {
461                $err = DATE_HOLIDAYS_TITLE_UNAVAILABLE;
462                $msg = 'The internal name (' . $internalName . ') ' .
463                       'for the holiday was correct but no ' .
464                       'localized title could be found';
465                return Date_Holidays::raiseError($err, $msg);
466            }
467        }
468
469        if (isset($this->_titles[$locale][$internalName])) {
470            return $this->_titles[$locale][$internalName];
471        } else {
472            return $this->_titles['C'][$internalName];
473        }
474    }
475
476
477    /**
478     * Returns the localized properties of a holiday. If no properties have
479     * been stored an empty array will be returned.
480     *
481     * @param string $internalName internal name for holiday
482     * @param string $locale       locale setting that shall be used by this method
483     *
484     * @access   public
485     * @return   array   array of properties on success, otherwise
486     *                   a PEAR_Error object
487     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_INTERNAL_NAME
488     */
489    function getHolidayProperties($internalName, $locale = null)
490    {
491        if (! in_array($internalName, $this->_internalNames)) {
492            $msg = 'Invalid internal name: ' . $internalName;
493            return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_INTERNAL_NAME,
494                                             $msg);
495        }
496
497        if (is_null($locale)) {
498            $locale =   $this->_findBestLocale($this->_locale);
499        } else {
500            $locale =   $this->_findBestLocale($locale);
501        }
502
503
504        $properties = array();
505        if (isset($this->_holidayProperties[$internalName][$locale])) {
506            $properties = $this->_holidayProperties[$internalName][$locale];
507        }
508        return $properties;
509    }
510
511
512    /**
513     * Returns all holidays that the driver knows.
514     *
515     * You can limit the holidays by passing a filter, then only those
516     * holidays accepted by the filter will be returned.
517     *
518     * Return format:
519     * <pre>
520     *   array(
521     *       'easter'        =>  object of type Date_Holidays_Holiday,
522     *       'eastermonday'  =>  object of type Date_Holidays_Holiday,
523     *       ...
524     *   )
525     * </pre>
526     *
527     * @param Date_Holidays_Filter $filter filter-object
528     *                                     (or an array !DEPRECATED!)
529     * @param string               $locale locale setting that shall be used
530     *                                      by this method
531     *
532     * @access   public
533     * @return   array   numeric array containing objects of
534     *                   Date_Holidays_Holiday on success, otherwise a
535     *                   PEAR_Error object
536     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_INTERNAL_NAME
537     * @see      getHoliday()
538     */
539    function getHolidays($filter = null, $locale = null)
540    {
541        if (is_null($filter)) {
542            $filter = new Date_Holidays_Filter_Blacklist(array());
543        } elseif (is_array($filter)) {
544            $filter = new Date_Holidays_Filter_Whitelist($filter);
545        }
546
547        if (is_null($locale)) {
548            $locale = $this->_locale;
549        }
550
551        $holidays = array();
552
553        foreach ($this->_internalNames as $internalName) {
554            if ($filter->accept($internalName)) {
555                // no need to check for valid internal-name, will be
556                // done by #getHoliday()
557                $holidays[$internalName] = $this->getHoliday($internalName,
558                                                             $locale);
559            }
560        }
561
562        return $holidays;
563    }
564
565    /**
566     * Returns the specified holiday
567     *
568     * Return format:
569     * <pre>
570     *   array(
571     *       'title' =>  'Easter Sunday'
572     *       'date'  =>  '2004-04-11'
573     *   )
574     * </pre>
575     *
576     * @param string $internalName internal name of the holiday
577     * @param string $locale       locale setting that shall be used
578     *                              by this method
579     *
580     * @access   public
581     * @return   object Date_Holidays_Holiday holiday's information on
582     *                                         success, otherwise a PEAR_Error
583     *                                         object
584     * @throws   object PEAR_Error       DATE_HOLIDAYS_INVALID_INTERNAL_NAME
585     * @uses     getHolidayTitle()
586     * @uses     getHolidayDate()
587     */
588    function getHoliday($internalName, $locale = null)
589    {
590        if (! in_array($internalName, $this->_internalNames)) {
591            return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_INTERNAL_NAME,
592                'Invalid internal name: ' . $internalName);
593        }
594        if (is_null($locale)) {
595            $locale = $this->_locale;
596        }
597
598        $title = $this->getHolidayTitle($internalName, $locale);
599        if (Date_Holidays::isError($title)) {
600            return $title;
601        }
602        $date = $this->getHolidayDate($internalName);
603        if (Date_Holidays::isError($date)) {
604            return $date;
605        }
606        $properties = $this->getHolidayProperties($internalName, $locale);
607        if (Date_Holidays::isError($properties)) {
608            return $properties;
609        }
610
611        $holiday = new Date_Holidays_Holiday($internalName,
612                                             $title,
613                                             $date,
614                                             $properties);
615        return $holiday;
616    }
617
618    /**
619     * Determines whether a date represents a holiday or not
620     *
621     * @param mixed                $date   a timestamp, string or PEAR::Date object
622     * @param Date_Holidays_Filter $filter filter-object (or an array !DEPRECATED!)
623     *
624     * @access   public
625     * @return   boolean true if date represents a holiday, otherwise false
626     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE_FORMAT
627     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE
628     */
629    function isHoliday($date, $filter = null)
630    {
631        if (! is_a($date, 'Date')) {
632            $date = $this->_convertDate($date);
633            if (Date_Holidays::isError($date)) {
634                return $date;
635            }
636        }
637
638        //rebuild internal array of holidays if required.
639        $compare_year = $date->getYear();
640        $this_year = $this->getYear();
641        if ($this_year !== $compare_year) {
642            $this->setYear($compare_year);
643        }
644
645        if (is_null($filter)) {
646            $filter = new Date_Holidays_Filter_Blacklist(array());
647        } elseif (is_array($filter)) {
648            $filter = new Date_Holidays_Filter_Whitelist($filter);
649        }
650
651        foreach (array_keys($this->_dates) as $internalName) {
652            if ($filter->accept($internalName)) {
653                if (Date_Holidays_Driver::dateSloppyCompare($date,
654                                          $this->_dates[$internalName]) != 0) {
655                    continue;
656                }
657                $this->setYear($this_year);
658                return true;
659            }
660        }
661        $this->setYear($this_year);
662        return false;
663    }
664
665    /**
666     * Returns a <code>Date_Holidays_Holiday</code> object, if any was found,
667     * matching the specified date.
668     *
669     * Normally the method will return the object of the first holiday matching
670     * the date. If you want the method to continue searching holidays for the
671     * specified date, set the 4th param to true.
672     *
673     * If multiple holidays match your date, the return value will be an array
674     * containing a number of <code>Date_Holidays_Holiday</code> items.
675     *
676     * @param mixed   $date     date (timestamp | string | PEAR::Date object)
677     * @param string  $locale   locale setting that shall be used by this method
678     * @param boolean $multiple if true, continue searching holidays for
679     *                           specified date
680     *
681     * @access   public
682     * @return   object  object of type Date_Holidays_Holiday on success
683     *                   (numeric array of those on multiple search),
684     *                   if no holiday was found, matching this date,
685     *                   null is returned
686     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE_FORMAT
687     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE
688     * @uses     getHoliday()
689     * @uses     getHolidayTitle()
690     * @see      getHoliday()
691     **/
692    function getHolidayForDate($date, $locale = null, $multiple = false)
693    {
694        if (!is_a($date, 'Date')) {
695            $date = $this->_convertDate($date);
696            if (Date_Holidays::isError($date)) {
697                return $date;
698            }
699        }
700
701        if ($date->getYear() != $this->_year) {
702            return null;
703        }
704
705        $isodate = mktime(0,
706                          0,
707                          0,
708                          $date->getMonth(),
709                          $date->getDay(),
710                          $date->getYear());
711        unset($date);
712        if (is_null($locale)) {
713            $locale = $this->_locale;
714        }
715        if (array_key_exists($isodate, $this->_holidays)) {
716            if (!$multiple) {
717                //get only the first feast for this day
718                $internalName = $this->_holidays[$isodate][0];
719                $result       = $this->getHoliday($internalName, $locale);
720                return Date_Holidays::isError($result) ? null : $result;
721            }
722            // array that collects data, if multiple searching is done
723            $data = array();
724            foreach ($this->_holidays[$isodate] as $internalName) {
725                $result = $this->getHoliday($internalName, $locale);
726                if (Date_Holidays::isError($result)) {
727                    continue;
728                }
729                $data[] = $result;
730            }
731            return $data;
732        }
733        return null;
734    }
735
736    /**
737     * Returns an array containing a number of
738     * <code>Date_Holidays_Holiday</code> items.
739     *
740     * If no items have been found the returned array will be empty.
741     *
742     * @param mixed                $start  date: timestamp, string or PEAR::Date
743     * @param mixed                $end    date: timestamp, string or PEAR::Date
744     * @param Date_Holidays_Filter $filter filter-object (or
745     *                                      an array !DEPRECATED!)
746     * @param string               $locale locale setting that shall be used
747     *                                      by this method
748     *
749     * @access   public
750     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE_FORMAT
751     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE
752     * @return   array   an array containing a number
753     *                   of <code>Date_Holidays_Holiday</code> items
754     */
755    function getHolidaysForDatespan($start, $end, $filter = null, $locale = null)
756    {
757        if (is_null($filter)) {
758            $filter = new Date_Holidays_Filter_Blacklist(array());
759        } elseif (is_array($filter)) {
760            $filter = new Date_Holidays_Filter_Whitelist($filter);
761        }
762
763        if (!is_a($start, 'Date')) {
764            $start = $this->_convertDate($start);
765            if (Date_Holidays::isError($start)) {
766                return $start;
767            }
768        }
769        if (!is_a($end, 'Date')) {
770            $end = $this->_convertDate($end);
771            if (Date_Holidays::isError($end)) {
772                return $end;
773            }
774        }
775
776        $isodateStart = mktime(0,
777                               0,
778                               0,
779                               $start->getMonth(),
780                               $start->getDay(),
781                               $start->getYear());
782        unset($start);
783        $isodateEnd = mktime(0,
784                             0,
785                             0,
786                             $end->getMonth(),
787                             $end->getDay(),
788                             $end->getYear());
789        unset($end);
790        if (is_null($locale)) {
791            $locale = $this->_locale;
792        }
793
794        $internalNames = array();
795
796        foreach ($this->_holidays as $isoDateTS => $arHolidays) {
797            if ($isoDateTS >= $isodateStart && $isoDateTS <= $isodateEnd) {
798                $internalNames = array_merge($internalNames, $arHolidays);
799            }
800        }
801
802        $retval = array();
803        foreach ($internalNames as $internalName) {
804            if ($filter->accept($internalName)) {
805                $retval[] = $this->getHoliday($internalName, $locale);
806            }
807        }
808        return $retval;
809
810    }
811
812    /**
813     * Converts timestamp or date-string into da PEAR::Date object
814     *
815     * @param mixed $date date
816     *
817     * @static
818     * @access   private
819     * @return   object PEAR_Date
820     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE_FORMAT
821     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_DATE
822     */
823    function _convertDate($date)
824    {
825        if (is_string($date)) {
826            if (! preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}/', $date)) {
827                return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_DATE_FORMAT,
828                    'Date-string has wrong format (must be YYYY-MM-DD)');
829            }
830            $date = new Date($date);
831            return $date;
832        }
833
834        if (is_int($date)) {
835            $date = new Date(date('Y-m-d', $date));
836            return $date;
837        }
838
839        return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_DATE,
840            'The date you specified is invalid');
841    }
842
843    /**
844     * Adds all holidays in the array to the driver's internal list of holidays.
845     *
846     * Format of the array:
847     * <pre>
848     *   array(
849     *       'newYearsDay'   => array(
850     *           'date'          => '01-01',
851     *           'title'         => 'New Year\'s Day',
852     *           'translations'  => array(
853     *               'de_DE' =>  'Neujahr',
854     *               'en_EN' =>  'New Year\'s Day'
855     *           )
856     *       ),
857     *       'valentinesDay' => array(
858     *           ...
859     *       )
860     *   );
861     * </pre>
862     *
863     * @param array $holidays static holidays' data
864     *
865     * @access   protected
866     * @uses     _addHoliday()
867     * @return   void
868     */
869    function _addStaticHolidays($holidays)
870    {
871        foreach ($holidays as $internalName => $holiday) {
872            // add the holiday's basic data
873            $this->_addHoliday($internalName,
874                               $this->_year . '-' . $holiday['date'],
875                               $holiday['title']);
876        }
877    }
878
879    /**
880     * Adds a holiday to the driver's holidays
881     *
882     * @param string $internalName internal name - must not contain characters
883     *                              that aren't allowed as variable-names
884     * @param mixed  $date         date (timestamp | string | PEAR::Date object)
885     * @param string $title        holiday title
886     *
887     * @access   protected
888     * @return   void
889     */
890    function _addHoliday($internalName, $date, $title)
891    {
892        if (! is_a($date, 'Date')) {
893            $date = new Date($date);
894        }
895
896        $this->_dates[$internalName]       = $date;
897        $this->_titles['C'][$internalName] = $title;
898        $isodate                           = mktime(0, 0, 0,
899                                                    $date->getMonth(),
900                                                    $date->getDay(),
901                                                    $date->getYear());
902        if (!isset($this->_holidays[$isodate])) {
903            $this->_holidays[$isodate] = array();
904        }
905        array_push($this->_holidays[$isodate], $internalName);
906        array_push($this->_internalNames, $internalName);
907    }
908
909    /**
910     * Add a localized translation for a holiday's title. Overwrites existing data.
911     *
912     * @param string $internalName internal name of an existing holiday
913     * @param string $locale       locale setting that shall be used by this method
914     * @param string $title        title
915     *
916     * @access   protected
917     * @return   true on success, otherwise a PEAR_Error object
918     * @throws   object PEAR_Error       DATE_HOLIDAYS_INVALID_INTERNAL_NAME
919     */
920    function _addTranslationForHoliday($internalName, $locale, $title)
921    {
922        if (! in_array($internalName, $this->_internalNames)) {
923            $msg = 'Couldn\'t add translation (' . $locale . ') ' .
924                   'for holiday with this internal name: ' . $internalName;
925            return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_INTERNAL_NAME,
926                                             $msg);
927        }
928
929        if (! in_array($locale, $this->_availableLocales)) {
930            array_push($this->_availableLocales, $locale);
931        }
932        $this->_titles[$locale][$internalName] = $title;
933        return true;
934    }
935
936    /**
937     * Adds a localized (regrading translation etc.) string-property for a holiday.
938     * Overwrites existing data.
939     *
940     * @param string $internalName internal-name
941     * @param string $locale       locale-setting
942     * @param string $propId       property-identifier
943     * @param mixed  $propVal      property-value
944     *
945     * @access   public
946     * @return   boolean true on success, false otherwise
947     * @throws   PEAR_ErrorStack if internal-name does not exist
948     */
949    function _addStringPropertyForHoliday($internalName, $locale, $propId, $propVal)
950    {
951        if (! in_array($internalName, $this->_internalNames)) {
952            $msg = 'Couldn\'t add property (locale: ' . $locale . ') '.
953                   'for holiday with this internal name: ' . $internalName;
954            return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_INTERNAL_NAME,
955                                             $msg);
956        }
957
958        if (!isset($this->_holidayProperties[$internalName]) ||
959                !is_array($this->_holidayProperties[$internalName])) {
960
961            $this->_holidayProperties[$internalName] = array();
962        }
963
964        if (! isset($this->_holidayProperties[$internalName][$locale]) ||
965                !is_array($this->_holidayProperties[$internalName][$locale])) {
966
967            $this->_holidayProperties[$internalName][$locale] = array();
968        }
969
970        $this->_holidayProperties[$internalName][$locale][$propId] = $propVal;
971        return true;
972    }
973
974    /**
975     * Adds a arbitrary number of localized string-properties for the
976     * specified holiday.
977     *
978     * @param string $internalName internal-name
979     * @param string $locale       locale-setting
980     * @param array  $properties   associative array: array(propId1 => val1,...)
981     *
982     * @access   public
983     * @return   boolean true on success, false otherwise
984     * @throws   PEAR_ErrorStack if internal-name does not exist
985     */
986    function _addStringPropertiesForHoliday($internalName, $locale, $properties)
987    {
988        foreach ($properties as $propId => $propValue) {
989            return $this->_addStringPropertyForHoliday($internalName,
990                                                       $locale,
991                                                       $propId,
992                                                       $propValue);
993        }
994
995        return true;
996    }
997
998    /**
999     * Add a language-file's content
1000     *
1001     * The language-file's content will be parsed and translations,
1002     * properties, etc. for holidays will be made available with the specified
1003     * locale.
1004     *
1005     * @param string $file   filename of the language file
1006     * @param string $locale locale-code of the translation
1007     *
1008     * @access   public
1009     * @return   boolean true on success, otherwise a PEAR_ErrorStack object
1010     * @throws   object PEAR_Errorstack
1011     */
1012    function addTranslationFile($file, $locale)
1013    {
1014        if (! file_exists($file)) {
1015            Date_Holidays::raiseError(DATE_HOLIDAYS_LANGUAGEFILE_NOT_FOUND,
1016                    'Language-file not found: ' . $file);
1017            return Date_Holidays::getErrorStack();
1018        }
1019
1020        // unserialize the document
1021        $document = simplexml_load_file($file);
1022
1023        $content = array();
1024        $content['holidays'] = array();
1025        $content['holidays']['holiday'] = array();
1026
1027        $nodes = $document->xpath('//holiday');
1028        foreach ($nodes as $node) {
1029            $content['holidays']['holiday'][] = (array)$node;
1030        }
1031
1032        return $this->_addTranslationData($content, $locale);
1033    }
1034
1035    /**
1036     * Add a compiled language-file's content
1037     *
1038     * The language-file's content will be unserialized and translations,
1039     * properties, etc. for holidays will be made available with the
1040     * specified locale.
1041     *
1042     * @param string $file   filename of the compiled language file
1043     * @param string $locale locale-code of the translation
1044     *
1045     * @access   public
1046     * @return   boolean true on success, otherwise a PEAR_ErrorStack object
1047     * @throws   object PEAR_Errorstack
1048     */
1049    function addCompiledTranslationFile($file, $locale)
1050    {
1051        if (! file_exists($file)) {
1052            Date_Holidays::raiseError(DATE_HOLIDAYS_LANGUAGEFILE_NOT_FOUND,
1053                    'Language-file not found: ' . $file);
1054            return Date_Holidays::getErrorStack();
1055        }
1056
1057        $content = file_get_contents($file);
1058        if ($content === false) {
1059            return false;
1060        }
1061        $data = unserialize($content);
1062        if ($data === false) {
1063            $e   = DATE_HOLIDAYS_UNABLE_TO_READ_TRANSLATIONDATA;
1064            $msg = "Unable to read translation-data - file maybe damaged: $file";
1065            return Date_Holidays::raiseError($e, $msg);
1066        }
1067        return $this->_addTranslationData($data, $locale);
1068    }
1069
1070    /**
1071     * Add a language-file's content. Translations, properties, etc. for
1072     * holidays will be made available with the specified locale.
1073     *
1074     * @param array  $data   translated data
1075     * @param string $locale locale-code of the translation
1076     *
1077     * @access   public
1078     * @return   boolean true on success, otherwise a PEAR_ErrorStack object
1079     * @throws   object PEAR_Errorstack
1080     */
1081    function _addTranslationData($data, $locale)
1082    {
1083        foreach ($data['holidays']['holiday'] as $holiday) {
1084            $this->_addTranslationForHoliday($holiday['internal-name'],
1085                                             $locale,
1086                                             $holiday['translation']);
1087
1088            if (isset($holiday['properties']) && is_array($holiday['properties'])) {
1089                foreach ($holiday['properties'] as $propId => $propVal) {
1090                    $this->_addStringPropertyForHoliday($holiday['internal-name'],
1091                                                        $locale,
1092                                                        $propId,
1093                                                        $propVal);
1094                }
1095            }
1096
1097        }
1098
1099        if (Date_Holidays::errorsOccurred()) {
1100            return Date_Holidays::getErrorStack();
1101        }
1102
1103        return true;
1104    }
1105
1106    /**
1107     * Remove a holiday from internal storage
1108     *
1109     * This method should be used within driver classes to unset holidays that
1110     * were inherited from parent-drivers
1111     *
1112     * @param $string $internalName internal name
1113     *
1114     * @access   protected
1115     * @return   boolean     true on success, otherwise a PEAR_Error object
1116     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_INTERNAL_NAME
1117     */
1118    function _removeHoliday($internalName)
1119    {
1120        if (! in_array($internalName, $this->_internalNames)) {
1121            $msg = "Couldn't remove holiday with this internal name: $internalName";
1122            return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_INTERNAL_NAME,
1123                                             $msg);
1124        }
1125
1126        if (isset($this->_dates[$internalName])) {
1127            unset($this->_dates[$internalName]);
1128        }
1129        $locales = array_keys($this->_titles);
1130        foreach ($locales as $locale) {
1131            if (isset($this->_titles[$locale][$internalName])) {
1132                unset($this->_titles[$locale][$internalName]);
1133            }
1134        }
1135        $index = array_search($internalName, $this->_internalNames);
1136        if (! is_null($index)) {
1137            unset($this->_internalNames[$index]);
1138        }
1139        return true;
1140    }
1141
1142    /**
1143     * Finds the best internally available locale for the specified one
1144     *
1145     * @param string $locale locale
1146     *
1147     * @access   protected
1148     * @return   string  best locale available
1149     */
1150    function _findBestLocale($locale)
1151    {
1152        /* exact locale is available */
1153        if (in_array($locale, $this->_availableLocales)) {
1154            return $locale;
1155        }
1156
1157        /* first two letter are equal */
1158        foreach ($this->_availableLocales as $aLocale) {
1159            if (strncasecmp($aLocale, $locale, 2) == 0) {
1160                return $aLocale;
1161            }
1162        }
1163
1164        /* no appropriate locale available, will use driver's internal locale */
1165        return 'C';
1166    }
1167
1168    /**
1169     * Returns date of a holiday
1170     *
1171     * @param string $internalName internal name for holiday
1172     *
1173     * @access   public
1174     * @return   object Date             date of holiday as PEAR::Date object
1175     *                                   on success, otherwise a PEAR_Error object
1176     * @throws   object PEAR_Error       DATE_HOLIDAYS_DATE_UNAVAILABLE
1177     * @throws   object PEAR_Error       DATE_HOLIDAYS_INVALID_INTERNAL_NAME
1178     */
1179    function getHolidayDate($internalName)
1180    {
1181        if (! in_array($internalName, $this->_internalNames)) {
1182            $msg = 'Invalid internal name: ' . $internalName;
1183            return Date_Holidays::raiseError(DATE_HOLIDAYS_INVALID_INTERNAL_NAME,
1184                                             $msg);
1185        }
1186
1187        if (! isset($this->_dates[$internalName])) {
1188            $msg = 'Date for holiday with internal name ' .
1189                   $internalName . ' is not available';
1190            return Date_Holidays::raiseError(DATE_HOLIDAYS_DATE_UNAVAILABLE, $msg);
1191        }
1192
1193        return $this->_dates[$internalName];
1194    }
1195
1196    /**
1197     * Returns dates of all holidays or those accepted by the applied filter.
1198     *
1199     * Structure of the returned array:
1200     * <pre>
1201     * array(
1202     *   'internalNameFoo' => object of type date,
1203     *   'internalNameBar' => object of type date
1204     * )
1205     * </pre>
1206     *
1207     * @param Date_Holidays_Filter $filter filter-object (or an array !DEPRECATED!)
1208     *
1209     * @access   public
1210     * @return   array with holidays' dates on success, otherwise a PEAR_Error object
1211     * @throws   object PEAR_Error   DATE_HOLIDAYS_INVALID_INTERNAL_NAME
1212     * @uses     getHolidayDate()
1213     */
1214    function getHolidayDates($filter = null)
1215    {
1216        if (is_null($filter)) {
1217            $filter = new Date_Holidays_Filter_Blacklist(array());
1218        } elseif (is_array($filter)) {
1219            $filter = new Date_Holidays_Filter_Whitelist($filter);
1220        }
1221
1222        $dates = array();
1223
1224        foreach ($this->_internalNames as $internalName) {
1225            if ($filter->accept($internalName)) {
1226                $date = $this->getHolidayDate($internalName);
1227                if (Date_Holidays::isError($date)) {
1228                    return $date;
1229                }
1230                $dates[$internalName] = $this->getHolidayDate($internalName);
1231            }
1232        }
1233        return $dates;
1234    }
1235
1236    /**
1237     * Sets the driver's locale
1238     *
1239     * @param string $locale locale
1240     *
1241     * @access   public
1242     * @return   void
1243     */
1244    function setLocale($locale)
1245    {
1246        $this->_locale = $locale;
1247        //if possible, load the translation files for this locale
1248        $this->addTranslation($locale);
1249    }
1250
1251    /**
1252     * Sloppily compares two date objects (only year, month and day are compared).
1253     * Does not take the date's timezone into account.
1254     *
1255     * @param Date $d1 a date object
1256     * @param Date $d2 another date object
1257     *
1258     * @static
1259     * @access private
1260     * @return int 0 if the dates are equal,
1261     *             -1 if d1 is before d2,
1262     *             1 if d1 is after d2
1263     */
1264    function dateSloppyCompare($d1, $d2)
1265    {
1266        $d1->setTZ(new Date_TimeZone('UTC'));
1267        $d2->setTZ(new Date_TimeZone('UTC'));
1268        $days1 = Date_Calc::dateToDays($d1->day, $d1->month, $d1->year);
1269        $days2 = Date_Calc::dateToDays($d2->day, $d2->month, $d2->year);
1270        if ($days1 < $days2) return -1;
1271        if ($days1 > $days2) return 1;
1272        return 0;
1273    }
1274    /**
1275     * Find the date of the first monday in the specified year of the current year.
1276     *
1277     * @param integer $month month
1278     *
1279     * @access   private
1280     * @return   object Date date of first monday in specified month.
1281     */
1282    function _calcFirstMonday($month)
1283    {
1284        $month = sprintf("%02d", $month);
1285        $date = new Date($this->_year . "-$month-01");
1286        while ($date->getDayOfWeek() != 1) {
1287            $date = $date->getNextDay();
1288        }
1289        return ($date);
1290    }
1291    /**
1292     * Find the date of the last monday in the specified year of the current year.
1293     *
1294     * @param integer $month month
1295     *
1296     * @access   private
1297     * @return   object Date date of last monday in specified month.
1298     */
1299    function _calcLastMonday($month)
1300    {
1301        //work backwards from the first day of the next month.
1302        $month = sprintf("%02d", $month);
1303        $nm = ((int) $month ) + 1;
1304        if ($nm > 12) {
1305            $nm = 1;
1306        }
1307        $nm = sprintf("%02d", $nm);
1308
1309        $date = new Date($this->_year . "-$nm-01");
1310        $date = $date->getPrevDay();
1311        while ($date->getDayOfWeek() != 1) {
1312            $date = $date->getPrevDay();
1313        }
1314        return ($date);
1315    }
1316    /**
1317     * Calculate Nth monday in a month
1318     *
1319     * @param int $month    month
1320     * @param int $position position
1321     *
1322     * @access   private
1323     * @return   object Date date
1324     */
1325    function _calcNthMondayInMonth($month, $position)
1326    {
1327        if ($position  == 1) {
1328            $startday = '01';
1329        } elseif ($position == 2) {
1330            $startday = '08';
1331        } elseif ($position == 3) {
1332            $startday = '15';
1333        } elseif ($position == 4) {
1334            $startday = '22';
1335        } elseif ($position == 5) {
1336            $startday = '29';
1337        }
1338        $month = sprintf("%02d", $month);
1339
1340        $date = new Date($this->_year . '-' . $month . '-' . $startday);
1341        while ($date->getDayOfWeek() != 1) {
1342            $date = $date->getNextDay();
1343        }
1344        return $date;
1345    }
1346
1347    /**
1348     * Calculate Nth day of the week in a month
1349     *
1350     * @param int $position position
1351     * @param int $weekday  day of the week starting from 1 == sunday
1352     * @param int $month    month
1353     *
1354     * @access   private
1355     * @return   object Date date
1356     */
1357    function _calcNthWeekDayInMonth($position, $weekday, $month)
1358    {
1359        if ($position  == 1) {
1360            $startday = '01';
1361        } elseif ($position == 2) {
1362            $startday = '08';
1363        } elseif ($position == 3) {
1364            $startday = '15';
1365        } elseif ($position == 4) {
1366            $startday = '22';
1367        } elseif ($position == 5) {
1368            $startday = '29';
1369        }
1370        $month = sprintf("%02d", $month);
1371
1372        $date = new Date($this->_year . '-' . $month . '-' . $startday);
1373        while ($date->getDayOfWeek() != $weekday) {
1374            $date = $date->getNextDay();
1375        }
1376        return $date;
1377    }
1378
1379    /**
1380     * Converts the date to the specified no of days from the given date
1381     *
1382     * To subtract days use a negative value for the '$pn_days' parameter
1383     *
1384     * @param Date $date Date object
1385     * @param int $pn_days days to add
1386     *
1387     * @return   Date
1388     * @access   protected
1389     */
1390    function _addDays($date, $pn_days)
1391    {
1392        $new_date = new Date($date);
1393        list($new_date->year, $new_date->month, $new_date->day) =
1394            explode(' ',
1395                    Date_Calc::daysToDate(Date_Calc::dateToDays($date->day,
1396                                                                $date->month,
1397                                                                $date->year) +
1398                                          $pn_days,
1399                                          '%Y %m %d'));
1400        if (isset($new_date->on_standardyear)) {
1401            $new_date->on_standardyear = $new_date->year;
1402            $new_date->on_standardmonth = $new_date->month;
1403            $new_date->on_standardday = $new_date->day;
1404        }
1405        return $new_date;
1406    }
1407
1408}
1409?>
1410