1<?php
2/**
3 * Jalali (Shamsi) DateTime Class. Supports years higher than 2038.
4 *
5 * Copyright (c) 2012 Sallar Kaboli <sallar.kaboli@gmail.com>
6 * http://sallar.me
7 *
8 * The MIT License (MIT)
9 *
10 * Permission is hereby granted, free of charge, to any person obtaining a
11 * copy of this software and associated documentation files (the "Software"),
12 * to deal in the Software without restriction, including without limitation
13 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 * and/or sell copies of the Software, and to permit persons to whom the
15 * Software is furnished to do so, subject to the following conditions:
16 *
17 * 1- The above copyright notice and this permission notice shall be included
18 * in all copies or substantial portions of the Software.
19 *
20 * 2- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26 * DEALINGS IN THE SOFTWARE.
27 *
28 * Original Jalali to Gregorian (and vice versa) convertor:
29 * Copyright (C) 2000  Roozbeh Pournader and Mohammad Toossi
30 *
31 * List of supported timezones can be found here:
32 * http://www.php.net/manual/en/timezones.php
33 *
34 *
35 * @package    jDateTime
36 * @author     Sallar Kaboli <sallar.kaboli@gmail.com>
37 * @author     Omid Pilevar <omid.pixel@gmail.com>
38 * @copyright  2003-2012 Sallar Kaboli
39 * @license    http://opensource.org/licenses/mit-license.php The MIT License
40 * @link       https://github.com/sallar/jDateTime
41 * @see        DateTime
42 * @version    2.2.0
43 */
44class jDateTime
45{
46
47    /**
48     * Defaults
49     */
50    private static $jalali   = true; //Use Jalali Date, If set to false, falls back to gregorian
51    private static $convert  = true; //Convert numbers to Farsi characters in utf-8
52    private static $timezone = null; //Timezone String e.g Asia/Tehran, Defaults to Server Timezone Settings
53    private static $temp = array();
54
55    /**
56     * jDateTime::Constructor
57     *
58     * Pass these parameteres when creating a new instance
59     * of this Class, and they will be used as defaults.
60     * e.g $obj = new jDateTime(false, true, 'Asia/Tehran');
61     * To use system defaults pass null for each one or just
62     * create the object without any parameters.
63     *
64     * @author Sallar Kaboli
65     * @param $convert bool Converts numbers to Farsi
66     * @param $jalali bool Converts date to Jalali
67     * @param $timezone string Timezone string
68     */
69    public function __construct($convert = null, $jalali = null, $timezone = null)
70    {
71        if ( $jalali   !== null ) self::$jalali   = (bool) $jalali;
72        if ( $convert  !== null ) self::$convert  = (bool) $convert;
73        if ( $timezone !== null ) self::$timezone = $timezone;
74    }
75
76    /**
77     * jDateTime::Date
78     *
79     * Formats and returns given timestamp just like php's
80     * built in date() function.
81     * e.g:
82     * $obj->date("Y-m-d H:i", time());
83     * $obj->date("Y-m-d", time(), false, false, 'America/New_York');
84     *
85     * @author Sallar Kaboli
86     * @param $format string Acceps format string based on: php.net/date
87     * @param $stamp int Unix Timestamp (Epoch Time)
88     * @param $convert bool (Optional) forces convert action. pass null to use system default
89     * @param $jalali bool (Optional) forces jalali conversion. pass null to use system default
90     * @param $timezone string (Optional) forces a different timezone. pass null to use system default
91     * @return string Formatted input
92     */
93    public static function date($format, $stamp = false, $convert = null, $jalali = null, $timezone = null)
94    {
95        //Timestamp + Timezone
96        $stamp    = ($stamp !== false) ? $stamp : time();
97        $timezone = ($timezone != null) ? $timezone : ((self::$timezone != null) ? self::$timezone : date_default_timezone_get());
98        $obj      = new DateTime('@' . $stamp, new DateTimeZone($timezone));
99        $obj->setTimezone(new DateTimeZone($timezone));
100
101        if ( (self::$jalali === false && $jalali === null) || $jalali === false ) {
102            return $obj->format($format);
103        }
104        else {
105
106            //Find what to replace
107            $chars  = (preg_match_all('/([a-zA-Z]{1})/', $format, $chars)) ? $chars[0] : array();
108
109            //Intact Keys
110            $intact = array('B','h','H','g','G','i','s','I','U','u','Z','O','P');
111            $intact = self::filterArray($chars, $intact);
112            $intactValues = array();
113
114            foreach ($intact as $k => $v) {
115                $intactValues[$k] = $obj->format($v);
116            }
117            //End Intact Keys
118
119
120            //Changed Keys
121            list($year, $month, $day) = array($obj->format('Y'), $obj->format('n'), $obj->format('j'));
122            list($jyear, $jmonth, $jday) = self::toJalali($year, $month, $day);
123
124            $keys   = array('d','D','j','l','N','S','w','z','W','F','m','M','n','t','L','o','Y','y','a','A','c','r','e','T');
125            $keys   = self::filterArray($chars, $keys, array('z'));
126            $values = array();
127
128            foreach ($keys as $k => $key) {
129
130                $v = '';
131                switch ($key) {
132                    //Day
133                    case 'd':
134                        $v = sprintf('%02d', $jday);
135                        break;
136                    case 'D':
137                        $v = self::getDayNames($obj->format('D'), true);
138                        break;
139                    case 'j':
140                        $v = $jday;
141                        break;
142                    case 'l':
143                        $v = self::getDayNames($obj->format('l'));
144                        break;
145                    case 'N':
146                        $v = self::getDayNames($obj->format('l'), false, 1, true);
147                        break;
148                    case 'S':
149                        $v = 'ام';
150                        break;
151                    case 'w':
152                        $v = self::getDayNames($obj->format('l'), false, 1, true) - 1;
153                        break;
154                    case 'z':
155                        if ($jmonth > 6) {
156                            $v = 186 + (($jmonth - 6 - 1) * 30) + $jday;
157                        }
158                        else {
159                            $v = (($jmonth - 1) * 31) + $jday;
160                        }
161                        self::$temp['z'] = $v;
162                        break;
163                    //Week
164                    case 'W':
165                        $v = is_int(self::$temp['z'] / 7) ? (self::$temp['z'] / 7) : intval(self::$temp['z'] / 7 + 1);
166                        break;
167                    //Month
168                    case 'F':
169                        $v = self::getMonthNames($jmonth);
170                        break;
171                    case 'm':
172                        $v = sprintf('%02d', $jmonth);
173                        break;
174                    case 'M':
175                        $v = self::getMonthNames($jmonth, true);
176                        break;
177                    case 'n':
178                        $v = $jmonth;
179                        break;
180                    case 't':
181                    	if ($jmonth>=1 && $jmonth<=6) $v=31;
182                    	else if ($jmonth>=7 && $jmonth<=11) $v=30;
183                    	else if($jmonth==12 && $jyear % 4 ==3) $v=30;
184                    	else if ($jmonth==12 && $jyear % 4 !=3) $v=29;
185                        break;
186                    //Year
187                    case 'L':
188                        $tmpObj = new DateTime('@'.(time()-31536000));
189                        $v = $tmpObj->format('L');
190                        break;
191                    case 'o':
192                    case 'Y':
193                        $v = $jyear;
194                        break;
195                    case 'y':
196                        $v = $jyear % 100;
197                        break;
198                    //Time
199                    case 'a':
200                        $v = ($obj->format('a') == 'am') ? 'ق.ظ' : 'ب.ظ';
201                        break;
202                    case 'A':
203                        $v = ($obj->format('A') == 'AM') ? 'قبل از ظهر' : 'بعد از ظهر';
204                        break;
205                    //Full Dates
206                    case 'c':
207                        $v  = $jyear.'-'.sprintf('%02d', $jmonth).'-'.sprintf('%02d', $jday).'T';
208                        $v .= $obj->format('H').':'.$obj->format('i').':'.$obj->format('s').$obj->format('P');
209                        break;
210                    case 'r':
211                        $v  = self::getDayNames($obj->format('D'), true).', '.sprintf('%02d', $jday).' '.self::getMonthNames($jmonth, true);
212                        $v .= ' '.$jyear.' '.$obj->format('H').':'.$obj->format('i').':'.$obj->format('s').' '.$obj->format('P');
213                        break;
214                    //Timezone
215                    case 'e':
216                        $v = $obj->format('e');
217                        break;
218                    case 'T':
219                        $v = $obj->format('T');
220                        break;
221
222                }
223                $values[$k] = $v;
224
225            }
226            //End Changed Keys
227
228            //Merge
229            $keys   = array_merge($intact, $keys);
230            $values = array_merge($intactValues, $values);
231
232            //Return
233            $ret = strtr($format, array_combine($keys, $values));
234            return
235                ($convert === false ||
236                ($convert === null && self::$convert === false) ||
237                ( $jalali === false || $jalali === null && self::$jalali === false ))
238                ? $ret : self::convertNumbers($ret);
239
240        }
241
242    }
243
244    /**
245     * jDateTime::gDate
246     *
247     * Same as jDateTime::Date method
248     * but this one works as a helper and returns Gregorian Date
249     * in case someone doesn't like to pass all those false arguments
250     * to Date method.
251     *
252     * e.g. $obj->gDate("Y-m-d") //Outputs: 2011-05-05
253     *      $obj->date("Y-m-d", false, false, false); //Outputs: 2011-05-05
254     *      Both return the exact same result.
255     *
256     * @author Sallar Kaboli
257     * @param $format string Acceps format string based on: php.net/date
258     * @param $stamp int Unix Timestamp (Epoch Time)
259     * @param $timezone string (Optional) forces a different timezone. pass null to use system default
260     * @return string Formatted input
261     */
262    public static function gDate($format, $stamp = false, $timezone = null)
263    {
264        return self::date($format, $stamp, false, false, $timezone);
265    }
266
267    /**
268     * jDateTime::Strftime
269     *
270     * Format a local time/date according to locale settings
271     * built in strftime() function.
272     * e.g:
273     * $obj->strftime("%x %H", time());
274     * $obj->strftime("%H", time(), false, false, 'America/New_York');
275     *
276     * @author Omid Pilevar
277     * @param $format string Acceps format string based on: php.net/date
278     * @param $stamp int Unix Timestamp (Epoch Time)
279     * @param $convert bool (Optional) forces convert action. pass null to use system default
280     * @param $jalali bool (Optional) forces jalali conversion. pass null to use system default
281     * @param $timezone string (Optional) forces a different timezone. pass null to use system default
282     * @return string Formatted input
283     */
284    public static function strftime($format, $stamp = false, $convert = null, $jalali = null, $timezone = null)
285    {
286        $str_format_code = array(
287            '%a', '%A', '%d', '%e', '%j', '%u', '%w',
288            '%U', '%V', '%W',
289            '%b', '%B', '%h', '%m',
290            '%C', '%g', '%G', '%y', '%Y',
291            '%H', '%I', '%l', '%M', '%p', '%P', '%r', '%R', '%S', '%T', '%X', '%z', '%Z',
292            '%c', '%D', '%F', '%s', '%x',
293            '%n', '%t', '%%'
294        );
295
296        $date_format_code = array(
297            'D', 'l', 'd', 'j', 'z', 'N', 'w',
298            'W', 'W', 'W',
299            'M', 'F', 'M', 'm',
300            'y', 'y', 'y', 'y', 'Y',
301            'H', 'h', 'g', 'i', 'A', 'a', 'h:i:s A', 'H:i', 's', 'H:i:s', 'h:i:s', 'H', 'H',
302            'D j M H:i:s', 'd/m/y', 'Y-m-d', 'U', 'd/m/y',
303            '\n', '\t', '%'
304        );
305
306        //Change Strftime format to Date format
307        $format = str_replace($str_format_code, $date_format_code, $format);
308
309        //Convert to date
310        return self::date($format, $stamp, $convert, $jalali, $timezone);
311    }
312
313   /**
314     * jDateTime::Mktime
315     *
316     * Creates a Unix Timestamp (Epoch Time) based on given parameters
317     * works like php's built in mktime() function.
318     * e.g:
319     * $time = $obj->mktime(0,0,0,2,10,1368);
320     * $obj->date("Y-m-d", $time); //Format and Display
321     * $obj->date("Y-m-d", $time, false, false); //Display in Gregorian !
322     *
323     * You can force gregorian mktime if system default is jalali and you
324     * need to create a timestamp based on gregorian date
325     * $time2 = $obj->mktime(0,0,0,12,23,1989, false);
326     *
327     * @author Sallar Kaboli
328     * @param $hour int Hour based on 24 hour system
329     * @param $minute int Minutes
330     * @param $second int Seconds
331     * @param $month int Month Number
332     * @param $day int Day Number
333     * @param $year int Four-digit Year number eg. 1390
334     * @param $jalali bool (Optional) pass false if you want to input gregorian time
335     * @param $timezone string (Optional) acceps an optional timezone if you want one
336     * @return int Unix Timestamp (Epoch Time)
337     */
338    public static function mktime($hour, $minute, $second, $month, $day, $year, $jalali = null, $timezone = null)
339    {
340        //Defaults
341        $month = (intval($month) == 0) ? self::date('m') : $month;
342        $day   = (intval($day)   == 0) ? self::date('d') : $day;
343        $year  = (intval($year)  == 0) ? self::date('Y') : $year;
344
345        //Convert to Gregorian if necessary
346        if ( $jalali === true || ($jalali === null && self::$jalali === true) ) {
347            list($year, $month, $day) = self::toGregorian($year, $month, $day);
348        }
349
350        //Create a new object and set the timezone if available
351        $date = $year.'-'.sprintf('%02d', $month).'-'.sprintf('%02d', $day).' '.$hour.':'.$minute.':'.$second;
352
353        if ( self::$timezone != null || $timezone != null ) {
354            $obj = new DateTime($date, new DateTimeZone(($timezone != null) ? $timezone : self::$timezone));
355        }
356        else {
357            $obj = new DateTime($date);
358        }
359
360        //Return
361        return $obj->format('U');
362    }
363
364    /**
365     * jDateTime::Checkdate
366     *
367     * Checks the validity of the date formed by the arguments.
368     * A date is considered valid if each parameter is properly defined.
369     * works like php's built in checkdate() function.
370     * Leap years are taken into consideration.
371     * e.g:
372     * $obj->checkdate(10, 21, 1390); // Return true
373     * $obj->checkdate(9, 31, 1390);  // Return false
374     *
375     * You can force gregorian checkdate if system default is jalali and you
376     * need to check based on gregorian date
377     * $check = $obj->checkdate(12, 31, 2011, false);
378     *
379     * @author Omid Pilevar
380     * @param $month int The month is between 1 and 12 inclusive.
381     * @param $day int The day is within the allowed number of days for the given month.
382     * @param $year int The year is between 1 and 32767 inclusive.
383     * @param $jalali bool (Optional) pass false if you want to input gregorian time
384     * @return bool
385     */
386    public static function checkdate($month, $day, $year, $jalali = null)
387    {
388        //Defaults
389        $month = (intval($month) == 0) ? self::date('n') : intval($month);
390        $day   = (intval($day)   == 0) ? self::date('j') : intval($day);
391        $year  = (intval($year)  == 0) ? self::date('Y') : intval($year);
392
393        //Check if its jalali date
394        if ( $jalali === true || ($jalali === null && self::$jalali === true) )
395        {
396            $epoch = self::mktime(0, 0, 0, $month, $day, $year);
397
398            if( self::date('Y-n-j', $epoch,false) == "$year-$month-$day" ) {
399                $ret = true;
400            }
401            else{
402                $ret = false;
403            }
404        }
405        else //Gregorian Date
406        {
407            $ret = checkdate($month, $day, $year);
408        }
409
410        //Return
411        return $ret;
412    }
413
414    /**
415     * System Helpers below
416     * ------------------------------------------------------
417     */
418
419    /**
420     * Filters out an array
421     */
422    private static function filterArray($needle, $heystack, $always = array())
423    {
424        return array_intersect(array_merge($needle, $always), $heystack);
425    }
426
427    /**
428     * Returns correct names for week days
429     */
430    private static function getDayNames($day, $shorten = false, $len = 1, $numeric = false)
431    {
432        $days = array(
433            'sat' => array(1, 'شنبه'),
434            'sun' => array(2, 'یکشنبه'),
435            'mon' => array(3, 'دوشنبه'),
436            'tue' => array(4, 'سه شنبه'),
437            'wed' => array(5, 'چهارشنبه'),
438            'thu' => array(6, 'پنجشنبه'),
439            'fri' => array(7, 'جمعه')
440        );
441
442        $day = substr(strtolower($day), 0, 3);
443        $day = $days[$day];
444
445        return ($numeric) ? $day[0] : (($shorten) ? self::substr($day[1], 0, $len) : $day[1]);
446    }
447
448    /**
449     * Returns correct names for months
450     */
451    private static function getMonthNames($month, $shorten = false, $len = 3)
452    {
453        // Convert
454        $months = array(
455            'فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند'
456        );
457        $ret    = $months[$month - 1];
458
459        // Return
460        return ($shorten) ? self::substr($ret, 0, $len) : $ret;
461    }
462
463    /**
464     * Converts latin numbers to farsi script
465     */
466    private static function convertNumbers($matches)
467    {
468        $farsi_array   = array('۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹');
469        $english_array = array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
470
471        return str_replace($english_array, $farsi_array, $matches);
472    }
473
474    /**
475     * Division
476     */
477    private static function div($a, $b)
478    {
479        return (int) ($a / $b);
480    }
481
482    /**
483     * Substring helper
484     */
485    private static function substr($str, $start, $len)
486    {
487        if( function_exists('mb_substr') ){
488            return mb_substr($str, $start, $len, 'UTF-8');
489        }
490        else{
491            return substr($str, $start, $len * 2);
492        }
493    }
494
495    /**
496     * Gregorian to Jalali Conversion
497     * Copyright (C) 2000  Roozbeh Pournader and Mohammad Toossi
498     */
499    public static function toJalali($g_y, $g_m, $g_d)
500    {
501
502        $g_days_in_month = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
503        $j_days_in_month = array(31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29);
504
505        $gy = $g_y-1600;
506        $gm = $g_m-1;
507        $gd = $g_d-1;
508
509        $g_day_no = 365*$gy+self::div($gy+3, 4)-self::div($gy+99, 100)+self::div($gy+399, 400);
510
511        for ($i=0; $i < $gm; ++$i)
512            $g_day_no += $g_days_in_month[$i];
513        if ($gm>1 && (($gy%4==0 && $gy%100!=0) || ($gy%400==0)))
514            $g_day_no++;
515        $g_day_no += $gd;
516
517        $j_day_no = $g_day_no-79;
518
519        $j_np = self::div($j_day_no, 12053);
520        $j_day_no = $j_day_no % 12053;
521
522        $jy = 979+33*$j_np+4*self::div($j_day_no, 1461);
523
524        $j_day_no %= 1461;
525
526        if ($j_day_no >= 366) {
527            $jy += self::div($j_day_no-1, 365);
528            $j_day_no = ($j_day_no-1)%365;
529        }
530
531        for ($i = 0; $i < 11 && $j_day_no >= $j_days_in_month[$i]; ++$i)
532            $j_day_no -= $j_days_in_month[$i];
533        $jm = $i+1;
534        $jd = $j_day_no+1;
535
536        return array($jy, $jm, $jd);
537
538    }
539
540    /**
541     * Jalali to Gregorian Conversion
542     * Copyright (C) 2000  Roozbeh Pournader and Mohammad Toossi
543     *
544     */
545    public static function toGregorian($j_y, $j_m, $j_d)
546    {
547
548        $g_days_in_month = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
549        $j_days_in_month = array(31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29);
550
551        $jy = $j_y-979;
552        $jm = $j_m-1;
553        $jd = $j_d-1;
554
555        $j_day_no = 365*$jy + self::div($jy, 33)*8 + self::div($jy%33+3, 4);
556        for ($i=0; $i < $jm; ++$i)
557            $j_day_no += $j_days_in_month[$i];
558
559        $j_day_no += $jd;
560
561        $g_day_no = $j_day_no+79;
562
563        $gy = 1600 + 400*self::div($g_day_no, 146097);
564        $g_day_no = $g_day_no % 146097;
565
566        $leap = true;
567        if ($g_day_no >= 36525) {
568            $g_day_no--;
569            $gy += 100*self::div($g_day_no,  36524);
570            $g_day_no = $g_day_no % 36524;
571
572            if ($g_day_no >= 365)
573                $g_day_no++;
574            else
575                $leap = false;
576        }
577
578        $gy += 4*self::div($g_day_no, 1461);
579        $g_day_no %= 1461;
580
581        if ($g_day_no >= 366) {
582            $leap = false;
583
584            $g_day_no--;
585            $gy += self::div($g_day_no, 365);
586            $g_day_no = $g_day_no % 365;
587        }
588
589        for ($i = 0; $g_day_no >= $g_days_in_month[$i] + ($i == 1 && $leap); $i++)
590            $g_day_no -= $g_days_in_month[$i] + ($i == 1 && $leap);
591        $gm = $i+1;
592        $gd = $g_day_no+1;
593
594        return array($gy, $gm, $gd);
595
596    }
597
598}
599