<?php
##
## this file name is 'class.calendar.php'
##
## calendar object
##
## [author]
##  - Chilbong Kim, <san2(at)linuxchannel.net>
##  - http://linuxchannel.net/
##
## [changes]
##  - 2014.09.26 : support PHP/5.4 (calltime reference)
##  - 2011.11.06 : add some utils
##  - 2011.04.29 : comment patch(UT = TT - delta T)
##  - 2011.04.24 : bug fixed, calendar::date('D')
##  - 2010.05.20 : bug fixed, calendar::date('W') of ISO-8601
##  - 2010.05.19 : added calendar::date('J'), is a JD
##  - 2010.05.18 : support calendar::date('I'), DST(daylight saving time) and all support of date() format.
##  - 2009.06.08 : some
##  - 2007.07.28 : support win32 PHP4(on Microsoft Windows) and Unix
##  - 2005.04.12 : new build
##
## [valid date]
##  - unix timestamp base: 1902-01-01 00:00:00 <= date <= 2037-12-31 23:59:59 (guess)
##  - JD(Julian Day) base: BC 4713-01-01 12:00 GMT <= Gregorian date <= AD 9999 (guess)
##
## [download & online source view]
##  - http://ftp.linuxchannel.net/devel/php_calendar/
##
## [demo]
##  - http://linuxchannel.net/gaggle/calendar.php
##
## [references]
##  - http://www.linuxchannel.net/docs/solar-24terms.txt
##  - http://www.linuxchannel.net/docs/lunar.txt
##  - http://ftp.linuxchannel.net/devel/php_solar/
##  - http://ftp.linuxchannel.net/devel/php_lunar/
##  - http://www.merlyn.demon.co.uk/    // Astronomy and Astronautics
##  - http://www.boogle.com/info/cal-overview.html
##  - http://star-www.st-and.ac.uk/~fv/webnotes/index.html  // Positional Astronomy
##
## [time format]
##  - UT = TT - dT
##  - DT = in korean 'yeok-hak-si'
##  - TDT(Terrestrial Dynamical Time) = DT(Dynamical Time) = TT(Terrestrial Time)
##  - local = time(),date()
##  - UT    = gmtime(),gmdate() or local-time_offset
##  - TT    = UT + dT // use the astro caculating
##  - JD <-> UT <-> GST <-> LST
##            <------------->
##     <------------->
##     <-------------------->
##  - jd2ut(JD,-time_offset) <-> ut2jd(ut,+time_offset)
##  - ut2gst(ut) <-> gst2ut(gst)  // Ʒ Լ
##  - gst2lst(gst,+lon_offset) <-> lst2gst(lst,-lon_offset)
##
## [degrees format]
##  - deg2hms(deg) <-> hms2deg(hms)
##  - deg2dms(deg) <-> dms2deg(dms)
##  - deg2h(deg)   <-> h2deg(h)
##  - hms2dms(hms) <-> dms2hms(dms)
##  - hms2h(hms)   <-> h2hms(h)
##  - dms2h(dms)   <-> h2dms(h)
##
## [compare 1. PHP4(win32) internal functions VS this method(object)]
##  - time()            -
##  - date()            _date()        // private, base on unix timestamp, BC 4313 ~ AD 9999(guess)
##  - mktime()            _mktime()    // private, support native value, BC 4313 ~ AD 9999(guess)
##
## [compare 2. PHP4(win32) calendar module VS this method(object)]
##  - gregoriantojd()        mkjd()        // public, support hour, minute, seconds, BC 4313 ~ AD 9999(guess)
##  - jdtogregorian()        date()        // public, same above, but same as `date()'
##  - jddayofweek()        jddayofweek    // public, similar
##  - cal_days_in_month()    days_in_month()    // public, similar
##  - unixtojd()        _utime2jd()    // private, same above
##  - jdtounix()        _jd2utime()    // private, same above
##
## [usage] -- see that last row of this source
##  $jd = calendar::mkjd(23,59,59,12,31,1901);
##  echo calendar::date('Y-m-d H:i:s T',$jd);
##

class calendar
{
  
## private, get Julian day -- same as gregoriantojd()
  ##
  ## http://en.wikipedia.org/wiki/Julian_day
  ## http://ko.wikipedia.org/wiki/%EC%9C%A8%EB%A6%AC%EC%9A%B0%EC%8A%A4%EC%9D%BC
  ## ftp://ssd.jpl.nasa.gov/pub/eph/export/C-versions/hoffman/
  ## http://blog.jidolstar.com/482
  ##
  ## Julian date
  ## JD 0.0 => BC 4713-01-01 12:00 GMT <= BC 4713-01-01 21:00 KST
  ##
  
function &_getjd($Y$M=1$D=1$H=21$I=0$S=0$tojulian=FALSE)
  {
    
$H -= date('Z')/3600// local zone to GMT

    
if(func_num_args() < 3// Y is unix_timestamp
    
{ list($Y,$M,$D,$H,$I,$S) = explode(' ',calendar::_date('Y n j G i s',$Y-date('Z'))); }

    if(
$M 3) { $M += 12$Y--; }

    
$S += 64// is J2000.0 delta 'T', patch san2@2007.07.28
    
$D += ($H/24.0) + ($I/1440.0) + ($S/86400.0);
    
$A floor($Y/100.0);
    
$B $tojulian : (2.0 $A floor($A/4.0));  // juliantojd() $B = 0
    
    
$JDsprintf('%.13f',floor(365.25*($Y+4716.0))+floor(30.6001*($M+1.0))+$D+$B-1524.5);
    
$D sprintf('%.13f',$JD-2451545.0); // float, number of days
    
$J sprintf('%.4f',2000.0+($D/365.25)); // // Jxxxx.xxxx format
    
$T sprintf('%.13f',$D/36525.0); // // Julian century

    
return array($JD,$J,$D,$T);
  }

  
## private, get date(gregorian) from JD -- same as jdtogregorian()
  ##
  ## JD to `YYYY MM DD HH II SS', JD is UT
  ##
  
function &_todate($JD)
  {
    
// JD >= 2299160.5 gregorian
    
$JD += date('Z')/86400// JD to local zone(JD)
    
$JD -= 64/86400// is J2000.0 delta 'T', patch san2@2007.07.28

    
$Z $JD 0.5// float
    
$W = (int)(($Z-1867216.25) / 36524.25);
    
$X = (int)($W 4);
    
$A = (int)($Z $W $X);
    
$B = (int)($A 1524);
    
$C = (int)(($B-122.1) / 365.25);
    
$D = (int)(365.25 $C); // is not $D = ($B - 122.1)
    
$E = (int)(($B-$D) / 30.6001);
    
$F = (int)(30.6001 $E); // is not $F = ($B -$D)

    
$_d $B $D $F;
    
$_m $E 1;
    
$_y $C 4716;

    
$JD -= 0.5// UT to GMT -12.0H
    
$JD = ($JD - (int)$JD) * 24.0;
    
$_h = (int)$JD;
    
$JD = ($JD $_h) * 60.0;
    
$_i = (int)$JD;
    
$JD = ($JD $_i) * 60.0;
    
$_s round($JD);

    if(
$_s 59) { $_s -= 60$_i++; }
    else if(
$_s 0) { $_s += 60$_i--; }    

    if(
$_i 59) { $_i -= 60$_h++; }
    else if(
$_i 0) { $_i += 60$_h--; }

    if(
$_h 23) { $_h -= 24$_d++; }
    else if(
$_h 0) { $_h +=24$_d--; }

    if(
$_m 12) { $_m -= 12$_y++; }
    else if(
$_m 0) { $_m +=12$_y--; }

    return array(
$_y,$_m,$_d,$_h,$_i,$_s);
  }

  
## private, get JD(julian day) from unix timestamp -- same as unixtojd()
  ##
  ## D -- get the number of days from base JD
  ## D = JD(Julian Day) - 2451545.0, base JD(J2000.0)
  ##
  ## base position (J2000.0), 2000-01-01 12:00:00 GMT
  ## as   mktime(12,0,0-64,1,1,2000) == 946695536 unix timestamp at KST, -64 is delta 'T'
  ## as gmmktime(12,0,0-64,1,1,2000) == 946727936 unix timestamp at GMT, -64 is delta 'T'
  ##
  ## valid JD: 1902-01-01 00:00:00 ZONE <= JD <= 2037-12-31 23:59:59 ZONE
  ##
  
function &_utime2jd($utime)
  {
    
$D $utime 946727936// number of time
    
$D sprintf('%.13f',$D/86400); // float, number of days
    
$JDsprintf('%.13f',$D+2451545.0); // float, Julian Day
    //$J = sprintf('%.4f',2000.0+($D/365.25)); // Jxxxx.xxxx format
    //$T = sprintf('%.13f',$D/36525.0); // Julian century

    
return $JD// float
  
}

  
## private, get unix timestamp from JD -- same as jdtounix()
  ##
  ## 1970-01-01 12:00:00 GMT = 2440587.6257407409139 JD = J1970.0
  ## valid JD: 1902-01-01 00:00:00 ZONE <= JD <= 2037-12-31 23:59:59 ZONE
  ##
  
function &_jd2utime($JD)
  {
    
$JD -= 2440587.6257407409139// convert to base JD(J1970.0), J2000.0 delta 'T', but it's not need

    
$seconds round($JD*86400); // convert to time seconds base on 1970-01-01 00:00:00
    
$seconds += 43200// to GMT -12H(43200 seconds)
    
$seconds -= date('Z'); // to local time zone

     
return $seconds;
  }

  
## private, check datetime that it's null or not null
  ##
  
function &__check_datetime($argc, &$Y, &$M, &$D, &$H, &$I, &$S)
  {
    if(
$argc >= 6) return TRUE;

    list(
$Y,$_M,$_D,$_H,$_I,$_S) = explode(' ',date('Y n j G i s',time()));
    if(
$argc 5$D $_D;
    if(
$argc 4$M $_M;
    if(
$argc 3$S $_S;
    if(
$argc 2$I $_I;
    if(
$argc 1$H $_H;
  }

  
## public, make JD -- match to mktime()
  ##
  ## Julian date
  ## J0.0 = BC 4713-01-01 12:00 GMT = BC 4713-01-01 21:00 KST ~ AD 9999
  ##
  
function &mkjd($H=21$I=0$S=0$M=1$D=1$Y=NULL)
  {
    
calendar::__check_datetime(func_num_args(),$Y,$M,$D,$H,$I,$S);

    list(
$JD) = calendar::_getjd($Y,$M,$D,$H,(int)$I,(int)$S);

    return 
$JD// folat, JD is UT base
  
}

  
## private, get unix timestamp from date -- same as mktime()
  ##
  ## valid date: 1902-01-01 00:00:00 ZONE <= date <= 2037-12-31 23:59:59 ZONE
  ##
  
function &_mktime($H=9$I=0$S=0$M=1$D=1$Y=NULL)
  {
    if(
$Y>1970 && $Y<2038) return mktime($H,$I,$S,$M,$D,$Y);

    
calendar::__check_datetime(func_num_args(),$Y,$M,$D,$H,$I,$S);

    
$JD calendar::mkjd($H,$I,$S,$M,$D,$Y);
    
$utime calendar::_jd2utime($JD);

     return 
$utime;
  }

  
## private,  same as `date()' function, base on unix timestamp(support Microsoft Windows PHP4)
  ##
  ## valid date: 1902-01-01 00:00:00 ZONE <= date <= 2037-12-31 23:59:59 ZONE
  ##
  
function &_date($format$utime=NULL)
  {
    if(
$utime === NULL$utime time();
    if(
$utime>=&& $utime<2145884400) return date($format,$utime);

    
$JD calendar::_utime2jd($utime);
    
$str calendar::date($format,$JD);

     return 
$str;
  }

  
## public, same as `date()' function, but base on JD by UT(delta T)
  ##
  ## valid JD: BC 4713-01-01 12:00 GMT ~ AD 9999
  ##
  
function &date($format$JD=NULL)
  {
    static 
$_weeks = array('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday');
    static 
$_months = array('January','February','March','April','May','June','July','August',
        
'September','Octorber','November','December');
    static 
$_ordinals = array(1=>'st',21=>'st',31=>'st',2=>'nd',22=>'nd',3=>'rd',23=>'rd');

    if(
func_num_args()<|| $JD==NULL$JD calendar::mkjd(); // current JD(UT)
    
if(!$format || is_array($format)) return calendar::_todate($JD); // array

    
list($Y,$M,$D,$H,$I,$S) = calendar::_todate($JD);

    
## get DST(daylight saving time), patch san2@2010.05.19
    ##
    
if($Y>=1916 && $Y<2038)
    {
        list(
$_DST,$_Z,$_O,$_T) = explode(' ',date('I Z O T',mktime(12,0,0,$M,$D,$Y)));
    } else
    {
        
$_DST 0;
        list(
$_Z,$_O,$_T) = explode(' ',date('Z O T')); // $_O example +0900
    
}

    
## patch san2@2010.05.19
    ##
    
if($Y>1970 && $Y<2038$_U mktime($H,$I,$S,$M,$D,$Y);
    else 
$_U calendar::_jd2utime($JD);

    
$_Y sprintf('%04d',$Y);
    
$_M sprintf('%02d',$M);
    
$_D sprintf('%02d',$D);
    
$_H sprintf('%02d',$H);
    
$_I sprintf('%02d',$I);
    
$_S sprintf('%02d',$S);
    
$_w calendar::jddayofweek($JD + ($_Z/86400)); // JD apply to local TimeZone
    
$_W calendar::weeknumber($Y,$M,$D); // ISO-8601
    
$_R substr($_weeks[$_W],0,3).", $_D ".substr($_months[$M-1],0,3).$H:$I:$S $_O";
    
$_P substr($_O,0,3).':'.substr($_O,-2);
    
$_C "${_Y}-${_M}-${_D}T${_H}:${_I}:${_S}${_P}";
    
$_N = ($_W == 0) ? $_W;
    
$_o = ($M==12 && $_W==1) ? $Y+: (($M==&& $_W>=52) ? $Y-$Y);

    
$r '';
    
$nextskip FALSE;
    
$l strlen($format);
    for(
$i=0$i<$l$i++)
    {
        
$char $format[$i];
        if(!
trim($char)) { $r .= $char; continue; }
        if(
$nextskip) { $r .= $char$nextskip FALSE; continue; } // patch san2@2010.05.19
        
if($char == '\\') { $nextskip TRUE; continue; } else $nextskip FALSE;
        switch(
$char)
        {
            case 
'a'$r .= ($H<12) ? 'am' 'pm'; break;
            case 
'A'$r .= ($H<12) ? 'AM' 'PM'; break;
            case 
'B'$r .= calendar::itime($H,$I,$S); break;
            case 
'c'$r .= $_C; break; // ISO 8601 date (added in PHP5)
            
case 'd'$r .= $_D; break;
            case 
'D'$r .= substr($_weeks[$_w],0,3); break;
            case 
'F'$r .= $_months[$M-1]; break;
            case 
'g'$r .= (($H-1) % 12) + 1; break;
            case 
'G'$r .= $H; break;
            case 
'h'$r .= sprintf('%02d',(($H-1)%12)+1); break;
            case 
'H'$r .= $_H; break;
            case 
'i'$r .= $_I; break;
            case 
'I'$r .= $_DST; break;
            case 
'j'$r .= $D; break;
            case 
'J'$r .= $JD; break;
            case 
'l'$r .= $_weeks[$_W]; break;
            case 
'L'$r .= calendar::isleap($Y); break;
            case 
'm'$r .= $_M; break;
            case 
'M'$r .= substr($_months[$M-1],0,3); break;
            case 
'n'$r .= $M; break;
            case 
'N'$r .= $_N; break; // ISO-8601, day of the week, 1(Monday) ~ 7(Sunday)
            
case 'o'$r .= $_o; break; // ISO-8601 year number
            
case 'O'$r .= $_O; break;
            case 
'P'$r .= $_P; break;
            case 
'r'$r .= $_R; break;
            case 
's'$r .= $_S; break;
            case 
'S'$r .= $_ordinals[$D] ? $_ordinals[$D] : 'th'; break;
            case 
't'$r .= calendar::days_in_month($Y,$M); break;
            case 
'T'$r .= $_T; break;
            case 
'u'$r .= date('u'); break;
            case 
'U'$r .= $_U; break;
            case 
'w'$r .= $_w; break; // JD to local zone
            
case 'W'$r .= sprintf('%02d',$_W); break; // ISO-8601
            
case 'y'$r .= substr($_Y,-2); break;
            case 
'Y'$r .= $Y; break;
            case 
'z'$r .= calendar::dayofyear($Y,$M,$D); break;
            case 
'Z'$r .= $_Z; break; // KST zone +9H, in seconds

            
default : $r .= $char; break;
        }
    }

    return 
$r// string
  
}

  
## public, get leap year
  ##
  ## #define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0)
  ##
  ## +-- 4*Y ! // normal
  ## `-- 4*Y
  ##      |-- 100*Y ! // leap
  ##      `-- 100*Y
  ##           |-- 400*Y ! // normal
  ##           `-- 400*Y   // leap
  ##
  ## but, 4000*Y is not normal year, is leap year
  ## http://user.chollian.net/~kimdbin/re/leap_year.html
  ##
  
function &isleap($year)
  {
    if(
$year%4) return FALSE;
    else if(
$year%100) return TRUE;
    else if(
$year%400) return FALSE;

    return 
TRUE// else 400*Y
  
}

  
## public, get week idx
  ##
  ## 0(sun), 1(mon), 2(tue), 3(wed), 4(thu), 5(fri), 6(sat)
  ##
  
function &jddayofweek($JD)
  {
    return 
floor($JD+1.5)%7// integer
  
}

  function &
dayofyear($Y$M$D)
  {
    list(
$JDE) = calendar::_getjd($Y,$M,$D);
    list(
$JDS) = calendar::_getjd($Y,1,1);

    return (int)(
$JDE $JDS);
  }

  
## ISO-8601, start on Monday
  ##
  
function &weeknumber_f($Y$M$D)
  {
    list(
$JD) = calendar::_getjd($Y,1,1);

    
$widx calendar::jddayofweek($JD);
    
$days calendar::dayofyear($Y,$M,$D);

    
//$midx = ($widx<0) ? 6 : $widx;
    //$days = ($midx<1) ? ($days+$midx) : ($days+$midx-7);
    
$midx = ($widx==0) ? $widx// to ISO-8601
    
$days += ($midx>1) ? ($midx-7-1) : 0;
    
$n ceil($days/7);

    if(
$n >= 52// last week
    
{
        list(
$JD) = calendar::_getjd($Y,12,31);
        
$lidx calendar::jddayofweek($JD);
        if(
$widx>&& $lidx>0$n 1;
    }
    else if(
$n <= 1// first week
    
{
        list(
$JD) = calendar::_getjd($Y-1,1,1);
        
$widx calendar::jddayofweek($JD);
        
$n = ($widx>1) ? 52 53;
    }

    return 
$n// integer
  
}

  
## ISO-8601, start on Thursday
  ## patch san2@2010.05.20
  ##
  
function &weeknumber($Y$M$D)
  {
    list(
$JD) = calendar::_getjd($Y,1,1);

    
$widx calendar::jddayofweek($JD) - 1;
    
$days calendar::dayofyear($Y,$M,$D);

    
$midx = ($widx<0) ? $widx;
    
$days = ($midx<4) ? ($days+$midx) : ($days+$midx-7);
    
$n floor($days/7) + 1;

    if(
$n == 0// ok, first week or last of preious year
    
{
        list(
$JD) = calendar::_getjd($Y-1,1,1);
        
$widx calendar::jddayofweek($JD);
        
$n = ($widx>4) ? 52 53;
    }
    else if(
$n 52// last week or first week of next year
    
{
        list(
$JD) = calendar::_getjd($Y,12,31);
        
$widx calendar::jddayofweek($JD);
        if(
$widx>&& $widx<4$n 1// Monday ~ Wednesday
    
}

    return 
$n// integer
  
}

  
## public, get swatch internet time, base BMT = GMT + 1
  ## same as date('B')
  ##
  
function &itime($H$I$S)
  {
    
$B = ($H-(date('Z')/3600)+1)*41.666 $I*0.6944 $S*0.01157;
    
$B = ($B>0) ? $B $B+1000.0;

    return 
sprintf('%03d',$B);
  }

  
/***
  function &days_in_month($year, $month, $JDS=0)
  {
    list($JDS) = calendar::_getjd($year,$month,1);
    list($JDE) = calendar::_getjd($year,$month+1,1);

    $term = (int)($JDE - $JDS);

    return $term; // integer
  }
  ***/

  ## public
  ##
  
function &days_in_month($year$month)
  {
    static 
$months = array(31,0,31,30,31,30,31,31,30,31,30,31);

    
$n $months[$month-1];
    
$n $n $n : (calendar::isleap($year) ? 29 28);

    return 
$n// integer
  
}

  
## public
  ##
  
function &month_info($year$month)
  {
    if(
$year<1902 || $year>2037)
    {
        list(
$JD) = calendar::_getjd($year,$month,1);
        
$term calendar::days_in_month($year,$month);
        
$week calendar::jddayofweek($JD); // week idx
        
$minfo = array($week,$term);
    } else
    {
        
$utime mktime(23,59,59,$month,1,$year);
        
$minfo explode(' ',date('w t',$utime));
    }

    return 
$minfo// array($week,$term)
  
}  

  
## utils
  ##
  ## - deg2hms(deg) <-> hms2deg(hms)
  ## - deg2dms(deg) <-> dms2deg(dms)
  ## - deg2h(deg)   <-> h2deg(h)
  ## - hms2dms(hms) <-> dms2hms(dms)
  ## - hms2h(hms)   <-> h2hms(h)
  ## - dms2h(dms)   <-> h2dms(h)

  ## public
  ##
  
function &deg2dms($deg$singed=FALSE)
  {
    if(
$singed$singed '+';
    if(
$deg <0) { $singed '-'$deg abs($deg); }

    
$d floor($deg);
    
$m floor(fmod($deg*60,60));
    
$s sprintf('%.4f',fmod($deg*3600,60));

    return array(
$singed.$d,$m,$s);
  }

  
/***
  function &_calendar($year, $month)
  {
    list($week,$term) = calendar::month_info($year,$month);

    $eidx = 3;
    $sat = 7 - $week;
    $chk = $sat + 28;
    $sats = array($sat,$sat+7,$sat+14,$sat+21);
    $suns = array($sat-6,$sat+1,$sat+8,$sat+15);
    $refs = range(1,$term);

    if($chk <= $term)
    {
        $eidx++;
        $sats[] = $chk;
        $suns[] = $sat + 22;
    }

    ## check last sunday
    ##
    if($term-$sats[$eidx] > 0)
    {
        $sats[] = $sats[$eidx] + 7;
        $suns[] = $sats[$eidx] + 1;
        $eidx++;
    }

    ## rewrite array
    ##
    for($i=0; $i<=$eidx; $i++)
    {
        for($j=$suns[$i]; $j<=$sats[$i]; $j++) $r[$i][] = &$refs[$j-1];
    }

    //ksort($r);

    echo "$week;$term\n";
    print_r($suns);
    print_r($sats);
    print_r($r);
  }
  ***/

  ## public
  ##
  
function &calendar($year$month)
  {
    list(
$week,$term) = calendar::month_info($year,$month);

    
$eidx 3;
    
$refs range(1,$term); // reference of days
    
$fsat $week// first Saturday

    ## make index array such as (Sun,Sat)
    ##
    
for($i=0$i<=3$i++)
    {
        
$isat $fsat + ($i*7); // index of Saturday
        
$idxs[] = array($isat-6,$isat);
    }

    
## check last Saturday and Sunday
    ##
    
if(($fsat+28) <= $term$idxs[++$eidx] = array($fsat+22,$fsat+28);
    if((
$term-$idxs[$eidx][1]) > 0)
    {
        
$idxs[] = array($idxs[$eidx][0]+7,$idxs[$eidx][1]+7);
        
$eidx++;
    }

    
## rewrite days
    ##
    
for($i=0$i<=$eidx$i++)
    {
        for(
$j=$idxs[$i][0]; $j<=$idxs[$i][1]; $j++) $r[$i][] = &$refs[$j-1];
    }

    return 
$r// array
  
}
// end of class

/**** example *********
$_y = 2040;
$_m = 12;
$r = calendar::calendar($_y,$_m);

echo '<PRE>';
echo "      $_m $_y\n";
echo "Su Mo Tu We Th Fr Sa\n";

$size = sizeof($r);
for($i=0; $i<$size; $i++)
{
  printf("%2s",$r[$i][0]);
  for($j=1; $j<7; $j++) printf("%3s",$r[$i][$j]);
  echo "\n";
}
print_r($r);
**********************/
?>