<?php
##
## this file name is 'class.calendar123.php'
##
## calendar123 object
##
## [author]
##  - Chilbong Kim, <san2(at)linuxchannel.net>
##  - http://linuxchannel.net/
##
## [changes]
##  - 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
##
## [unit and format]
##  - date(ZONE) <-> JD(TT) : calcurated that date to JD(+64), JD to date(-64)
##  - utime(ZONE) <-> JD(TT): calcurated that utime to JD(-64), JD to utime(+64)
##  - date(ZONE) <-> utime(ZONE) : none
##  * ZONE is local system time zone, such as KST, GMT
##  * KST - 9H = GMT, GMT + 12H = UT, UT - delta T = TT
##  * JD(TT) is adapted delta 'T(J2000.0 is -64)' from UT(= GMT + 12H)
##
## [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 = calendar123::mkjd(23,59,59,12,31,1901);
##  echo calendar123::date('Y-m-d H:i:s T',$jd);
##

class calendar123
{
  ## 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(' ',calendar123::_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 ? 0 : (2.0 - $A + floor($A/4.0));  // juliantojd() $B = 0

    $JD= sprintf('%.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 + 1 + $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
    $JD= sprintf('%.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)
  {
    calendar123::__check_datetime(func_num_args(),&$Y,&$M,&$D,&$H,&$I,&$S);

    list($JD) = calendar123::_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);

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

    $JD = calendar123::mkjd($H,$I,$S,$M,$D,$Y);
    $utime = calendar123::_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>=0 && $utime<2145884400) return date($format,$utime);

    $JD = calendar123::_utime2jd($utime);
    $str = calendar123::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()<2 || $JD==NULL) $JD = calendar123::mkjd(); // current JD(UT)
    if(!$format || is_array($format)) return calendar123::_todate($JD); // array

    list($Y,$M,$D,$H,$I,$S) = calendar123::_todate($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);
    $_Z = date('Z');
    $_W = calendar123::jddayofweek($JD + ($_Z/86400)); // JD apply to local TimeZone
    $_O = date('O'); // example +0900
    $_R = substr($_weeks[$_W],0,3).", $_D ".substr($_months[$M-1],0,3)." $H:$I:$S $_O";
    $_C = "${_Y}-${_M}-${_D}T${_H}:${_I}:${_S}".$_O{0}.$_O{1}.$_O{2}.':'.$_O{3}.$_O{4};

    $l = strlen($format);
    for($i=0; $i<$l; $i++)
    {
        $char = $format[$i];
        switch($char)
        {
            case 'a': $r .= ($H<12) ? 'am' : 'pm'; break;
            case 'A': $r .= ($H<12) ? 'AM' : 'PM'; break;
            case 'B': $r .= calendar123::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 .= 0; break;
            case 'j': $r .= $D; break;
            case 'l': $r .= $_weeks[$_W]; break;
            case 'L': $r .= calendar123::isleap($Y); break;
            case 'm': $r .= $_M; break;
            case 'M': $r .= substr($_months[$M-1],0,3); break;
            case 'n': $r .= $M; break;
            case 'O': $r .= date('O'); break;
            case 'r': $r .= $_R; break;
            case 's': $r .= $_S; break;
            case 'S': $r .= $_ordinals[$D] ? $_ordinals[$D] : 'th'; break;
            case 't': $r .= calendar123::days_in_month($Y,$M); break;
            case 'T': $r .= date('T'); break;
            case 'U': $r .= time(); break;
            case 'w': $r .= $_W; break; // JD to local zone
            case 'W': $r .= calendar123::weeknumber($Y,$M,$D); break; // ISO-8601
            case 'y': $r .= substr($_Y,-2); break;
            case 'Y': $r .= $Y; break;
            case 'z': $r .= calendar123::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) = calendar123::_getjd($Y,$M,$D);
    list($JDS) = calendar123::_getjd($Y,1,1);

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

  ## ISO-8601, start on Thurday
  ##
  function &weeknumber($Y, $M, $D)
  {
    list($JD) = calendar123::_getjd($Y,1,1);

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

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

    if($n == 0)
    {
        list($JD) = calendar123::_getjd($Y-1,1,1);
        $widx = calendar123::jddayofweek($JD);
        $n = ($widx>4) ? 52 : 53;
    }

    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) = calendar123::_getjd($year,$month,1);
    list($JDE) = calendar123::_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 : (calendar123::isleap($year) ? 29 : 28);

    return $n; // integer
  }

  ## public
  ##
  function &month_info($year, $month)
  {
    if($year<1902 || $year>2037)
    {
        list($JD) = calendar123::_getjd($year,$month,1);
        $term = calendar123::days_in_month($year,$month);
        $week = calendar123::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)
  }

  /***
  function &_calendar($year, $month)
  {
    list($week,$term) = calendar123::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) = calendar123::month_info($year,$month);

    $eidx = 3;
    $refs = range(1,$term); // reference of days
    $fsat = 7 - $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 = calendar123::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);
**********************/
?>
