<?php

/*
   ###########################################################################
   Compute the dates (without times) of all
   lunar quarter phases for the given month.

   Built around the NASA/JPL Horizons API

   AUTHOR   : Jay Tanner - 2025
   LANGUAGE : PHP v8.2.12
   LICENSE  : Public Domain

  ###########################################################################
*/

   
ob_start(); // Initialize output buffer.


// ----------------------------------
// Code to suppress program warnings.

   
Error_Reporting(E_ERROR E_PARSE);

// -------------------------------------------------------------
// Get current year and define English month name abbreviations.

   
$cYear date('Y');

   
$MONTHS 'JanFebMarAprMayJunJulAugSepOctNovDec';

// ---------------------------------------------------------------
// Define the program cookie name and set it to expire in 30 days.

   
$CookieName 'Moon-Quarter-Phases-Dates-Calculator';
   
$ExpiresIn30Days time() + 30*86400;


// ---------------------------------
// Define PHP program and HTML info.

   
$_AUTHOR_           "Jay Tanner";
   
$_PROGRAM_VERSION_  'v1.00 - '$at "&#97;&#116;&#32;&#76;&#111;&#99;&#97;&#108;&#32;&#84;&#105;&#109;&#101;&#32;"$LTC "&#85;&#84;&#67;";
   
$_SCRIPT_FILE_PATH_ Filter_Input(INPUT_SERVER'SCRIPT_FILENAME');
   
$_REVISION_DATE_    $_PROGRAM_VERSION_ .'Revised: 'date("Y-F-d-l $at h:i:s A   ($LTC"FileMTime($_SCRIPT_FILE_PATH_))."&minus;05:00)";
   
$_BROWSER_TAB_TEXT_ "Lunar Quarter Phases Dates Calculator";
   
$_INTERFACE_TITLE_  "<span style='font-size:15pt;'>Lunar Quarter Phases Dates Calculator</span><br><span>Ephemeris Span:&nbsp; BC 9999-Apr &nbsp;to&nbsp;  AD 9999-Nov<br><br><span style='font-size:11pt;'>Built Around the NASA/JPL Horizons API</span><br><span style='font-size:8.5pt;'>Program by Jay Tanner - $cYear</span>";


// -------------------------------------
// Define main TextArea text and background
// colors and HTML table row span. If an
// error is reported, then these colors
// will change internally to red/white.

   
$TxColor 'black';
   
$BgColor 'white';


// ---------------------------------------------
// Do this only if [SUBMIT] button was clicked.

   
$w Filter_Input(INPUT_POST'SubmitButton');

   if (!IsSet(
$w))
  {

/* ----------------------------------------------------------------------
   If this program is being called externally, rather than being executed
   by clicking the [SUBMIT] button, and an active cookie also exists,
   then restore the previously saved interface settings from it. If
   the user leaves and comes back later, all the interface settings
   will be remembered and restored if the cookie was not deleted.
*/
   
$w Filter_Input(INPUT_COOKIE$CookieName);

   if (IsSet(
$w))
      {
       
$CookieDataString Filter_Input(INPUT_COOKIE$CookieName);
       list(
$BCAD,$Year,$Month,$TimeZone,$DaySumTimeYN) = Preg_Split("[\|]"$CookieDataString);
       
$StepSize '1 day ';
      }

   else


// -----------------------------------------------------------
// If there is no previous cookie with the interface settings,
// then set the initial default interface startup values and
// store them in a new cookie.

 
{
   
$BCAD         'AD';
   
$Year         date('Y');
   
$Month        date('M');
   
$TimeZone     '-05:00'// MUST be in '+-HH:mm' format.
   
$DaySumTimeYN 'No';
   
$StepSize     '1 day';

// -------------------------------------------
// Store current interface settings in cookie.

   
$CookieDataString "$BCAD|$Year|$Month|$TimeZone|$DaySumTimeYN";
   
SetCookie ($CookieName$CookieDataString$ExpiresIn30Days);
  } 
// End of  else {...}

  
// End of  if (!isset(_POST['SubmitButton']))


// ------------------------------------------
// Read values of all interface arguments and
// set any empty arguments to default values.

   
$w Filter_Input(INPUT_POST'SubmitButton');

   if (isset(
$w))
{
   
$BCAD StrToUpper(trim(Filter_Input(INPUT_POST'BCAD')));
           if (
substr($BCAD,0,1) == 'B') {$BCAD 'BC';}
           if (
substr($BCAD,0,1) == 'A') {$BCAD 'AD';}

   
$Year trim(Filter_Input(INPUT_POST'Year'));


/* -------------------------------------------
   Get month as a number (1 to 12) or as a
   3-letter abbreviation ('Jan' to 'Dec').
   NOT case-sensitive.
*/
   
$Month UCFirst(substr(StrToLower(trim(Filter_Input(INPUT_POST'Month'))),0,3));
            if (
$Month == '') {$Month date('M');}
   
$xMonth $Month;

   
$Month Alt_Month($Month);  if (Is_Numeric($Month)) {$Month Alt_Month($Month);}
if (
$Month === FALSE){$mErrFlag TRUE;}else{$mErrFlag FALSE; }

   
$TimeZone trim(Filter_Input(INPUT_POST'TimeZone'));
   if (
$TimeZone == '') {$TimeZone '-05:00';}
   
$TimeZoneHrs  HMS_to_Hours ($TimeZone);
   
$TimeZone     substr(Hours_to_HMS ($TimeZoneHrs0'+'':'),0,6);

   
$DaySumTimeYN StrToUpper(substr(trim(Filter_Input(INPUT_POST'DaySumTimeYN')),0,1));
   
$DaySumTimeYN = ($DaySumTimeYN <> 'Y')? 'No':'Yes';

   
$StepSize '1 day ';

// ----------------------------------
// Set default values if empty input.

   
if ($BCAD           == '') {$BCAD         'AD';}
   if (
$Year           == '') {$Year         date('Y');}
   if (
$Month          == '') {$Month        date('M');}
   if (
$DaySumTimeYN   == '') {$DaySumTimeYN 'No';}

// -------------------------------------
// Store interface argument in a cookie.

   
$CookieDataString "$BCAD|$Year|$Month|$TimeZone|$DaySumTimeYN";
   
SetCookie ($CookieName$CookieDataString$ExpiresIn30Days);
}


// ---------------------------------------------
// Check input values for validity.
// If error, set error flag and message values.

   
$ErrFlag FALSE;
   
$ErrMssg '';

   if (
$mErrFlag === TRUE)
      {
       
$ErrFlag TRUE;
       
$ErrMssg "Bad month number or abbreviation.\n'$xMonth'\n\nThe month must be an integer from 1 to 12 or a valid 3-letter\nEnglish month abbreviation from 'Jan' to 'Dec'.\nNOT case sensitive.";
       
$Month $xMonth;
      }

   if (!
Is_Numeric($Year) or $Year == or $Year 19999)
      {
       
$ErrFlag TRUE;
       
$ErrMssg "Bad year number.\n'$Year'\n\nThe year must be a non-zero integer &le; 9999.";
      }

   if (
$BCAD <> 'BC' and $BCAD <> 'AD')
      {
       
$ErrFlag TRUE;
       
$ErrMssg "'$BCAD'\nBad BC/AD era symbol.\n\nThe era symbol must be BC or AD only.\nAn empty input defaults to AD era.";
      }


// -----------------------------
// Set initial uniform width for
// tables alignment in pixels.

   
$TableWidth '780';


// ******************************************
// ******************************************
// If error was reported (TRUE), then display
// the error message on a red background.

   
if ($ErrFlag === TRUE)
  {
   
$TxColor 'white';
   
$BgColor '#CC0000';
   
$TextArea2Text '';

   
$TextArea1Text =
"=== ERROR ===

$ErrMssg";
  }

else


  {
// *********************************************************
// BEGIN MAIN COMPUTATIONS HERE IF NO ERRORS DETECTED ABOVE.
// *********************************************************

// ----------------------------------
// Get number of days in given month.

   
$m Alt_Month($Month);
        if (
Is_Numeric($m)) {$m Alt_Month($m);}
   
$mDays Month_Days ("$BCAD $Year"$m'A');


// ----------------------------------------------
// Force year value into positive 4-digit format.

   
$Year SPrintF("%04d"abs($Year));

// -------------------------------------------------
// Define East/West indicator string for (TimeZone).
// If (TimeZone) equates to zero, then time = UT.

   
$EWStr = (substr($TimeZone,0,1) == '-')? '-West' '+East';
   if (
$TimeZone == '+00:00') {$EWStr 'Greenwich';}



// QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ

/* ----------------------------------------------------
   Determine last date of previous month and first date
   of next month and use these for the table span.
*/
   
$w Prev_and_Next_Months ("$BCAD $Year"$Month'A');

   list(
$PrevMonthEnd$NextMonthStart) = PReg_Split("[,]"$w);

   
$PrevMonthEnd trim($PrevMonthEnd);

// ------------------
// Set tabular range.

   
$StartDateTime "$PrevMonthEnd 00:00:00";
   
$StopDateTime  "$NextMonthStart 00:00:00";

// QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ

// ----------------------------------------
// Call the function to generate the table.

   
$QPhasesDatesTable Lunar_Phase_Table($StartDateTime,$StopDateTime,
                                          
$StepSize,$TimeZone,$DaySumTimeYN);

// ---------------------------------------------------------
// Extract table of quarter phases from (QPhasesDatesTable).

   
$QPADatesTable ''// Extract_Phase_Dates ($QPhasesDatesTable);

   
$QPhasesDatesTable trim($QPhasesDatesTable);

// ----------------------------------------------
// Determine calendar mode (Gregorian = Default)
// or Julian calendar mode.

   
$CalMode = ($BCAD == 'AD' and $Year '1583')? 'Julian':'Gregorian';

/* -------------------------------------------
   DROP THROUGH HERE AFTER COMPUTATIONS ABOVE
   TO PRINT OUT THE RESULTS OF THE OPERATIONS.
   -------------------------------------------
*/

   
$TextArea1Text =
"
MOON PHASE ANGLES AND QUARTER PHASE DATES
Ephemeris Span: BC 9999-Apr  to  AD 9999-Nov

Local Calendar Date    :  
$BCAD $Year-$Month ($CalMode Calendar)
Local Time Zone Offset : 
$TimeZone       ($EWStr)
Daylight/Summer Time   :  
$DaySumTimeYN

--------------------------------------
TABLE OF CALENDAR DATES OF MOON PHASES
$QPADatesTable
$QPhasesDatesTable
"
;
}



// ****************************
// Define TextArea2 text block.

   
$TextArea2Text =
"
SEEKING THE CALENDAR DATES OF THE LUNAR PHASES

Given any month within the range of the ephemeris, this program will first
generate the table of lunar phase angles for 00:00 local time on each date
of the given month, then it will scan the table and extract ONLY the dates
of the lunar phases.  The clock time of the event is NOT computed.

Seeking the phase dates within the table is actually quite simple as long
as we only want the calendar dates without the actual event times.

We scan forward through the zero-indexed table starting at array index [1]
and test the simple phase angle values in pairs to find the phase dates.

Any phase occuring near the beginning of the month may possibly be repeated
again near the end of the month, such as a second (blue) full moon if there
is a full moon near the beginning of the month.

If there are multiple similar phases, such as a blue moon or any other phase
that repeats during the month, it will also be listed.

---------------------------------------------------------
The table  lines array is indexed from [0] and the pairing
works according to this algorithm working forward from [1]
up until reaching [wCount]:

PrevLine = wArray[CurrIndex - 1]
CurrLine = wArray[CurrIndex]

------------------------------------------------------------
The phase angle in degrees is at the end of each table line.

PrevPA = Trimmed last 10 characters of raw (PrevLine).
CurrPA = Trimmed last 10 characters of raw (CurrLine).

When two successive values meet the test conditions, then the
first of the two corresponding dates is the phase date on the
calendar.

-------------------------------------------------------------
Phase_Ang      Phase_ID      Phase Seeking Rule Within Table
---------    -------------   --------------------------------
   0         New Noon        (CurrPA - PrevPA) < 0
   90        First Quarter   (PrevPA <  90 and CurrPA >=  90)
   180       Full Moon       (PrevPA < 180 and CurrPA >= 180)
   270       Last Quarter    (PrevPA < 270 and CurrPA >= 270)
-------------------------------------------------------------

Following these rules yields the moon quarter phases date
table for the current given month as computed above.

"
;



/* ---------------------------------------------------------------------------
   Determine number of text columns and rows to use in the output text areas.
   These values vary randomly according to the text block width and length.
   The idea is to eliminate the need for scroll-bars within the text areas
   or worry as much about the variable dimensions of a text display area.
*/

// --------------------------------------------
// Text Area 1 - Default = At least 80 columns.

   
$Text1Cols Max(Array_Map('StrLen'PReg_Split("[\n]"trim($TextArea1Text))));
   if (
$Text1Cols 80) {$Text1Cols 80;}  // Default
   
$Text1Rows Substr_Count($TextArea1Text"\n");

// --------------------------------------------
// Text Area 2 - Default = At least 80 columns.

   
$Text2Cols Max(Array_Map('StrLen'PReg_Split("[\n]"trim($TextArea2Text))));
   if (
$Text2Cols 80) {$Text2Cols 80;} // Default
   
$Text2Rows Substr_Count($TextArea2Text"\n");



// ******************************************
// ******************************************
// GENERATE CLIENT WEB PAGE TO DISPLAY OUTPUT

   
print <<< _HTML

<!DOCTYPE HTML>
<HTML>

<head>
<title>
$_BROWSER_TAB_TEXT_</title>

<meta name='viewport' content='width=device-width, initial-scale=0.8'>

<meta http-equiv='content-type' content='text/html; charset=UTF-8'>
<meta http-equiv='pragma'  content='no-cache'>
<meta http-equiv='expires' content='-1'>
<meta name='description' content='Hourly Geocentric Moon Distance Calculator'>
<meta name='keywords' content='PHPScienceLabs.com'>
<meta name='author' content='Jay Tanner - https://www.PHPScienceLabs.com'>
<meta name='robots' content='index,follow'>
<meta name='googlebot' content='index,follow'>

<style>

 BODY {color:white; background:black; font-family:Verdana; font-size:12pt; line-height:125%;}

 TABLE
{font-size:13pt; border: 1px solid black;}


 TD
{
 color:black; background:white; line-height:150%; font-size:10pt;
 padding:6px; text-align:center;
}


 UL
{font-family:Verdana; font-size:12pt; line-height:150%; text-align:justify;}


 PRE
{
 background:white; color:black; font-family:monospace; font-size:12.5pt;
 font-weight:bold; text-align:left; line-height:125%; padding:6px;
 border:2px solid black; border-radius:8px;
 page-break-before:page;
}


 DIV
{
 background:white; color:black; font-family:Verdana; font-size:11pt;
 font-weight:normal; line-height:125%; padding:6px;
}


 TEXTAREA
{
 background:white; color:black; font-family:monospace; font-size:12pt;
 font-weight:bold; padding:4pt; white-space:pre; border-radius:8px;
 line-height:125%;
}


 INPUT[type='text']::-ms-clear {width:0; height:0;}

 INPUT[type='text']
{
 font-family:monospace; color:black; background:white; font-size:13pt;
 font-weight:bold; text-align:center; box-shadow:2px 2px 3px #666666;
 border:2px solid black; border-radius:4px;
}
 INPUT[type='text']:focus
{
 font-family:monospace; background:white; box-shadow:2px 2px 3px #666666;
 font-size:13pt; border:2px solid blue; text-align:center; font-weight:bold;
 border-radius:4px;
}



 INPUT[type='submit']
{
 background:black; color:cyan; font-family:Verdana; font-size:10pt;
 font-weight:bold; border-radius:4px; border:4px solid #777777;
 padding:3pt;
}
 INPUT[type='submit']:hover
{
 background:black; color:white; font-family:Verdana; font-size:10pt;
 font-weight:bold; border-radius:4px; border:4px solid red;
 padding:3pt;
}





// Link states MUST be set in the following order:
// :link, :visited, :hover, :active

 A:link
{
 font-size:10pt; background:transparent; color:#8080FF; border-radius:4px;
 font-family:Verdana; font-weight:bold; text-decoration:none;
 line-height:175%; padding:3px; border:1px solid transparent;
}
 A:visited
{
 font-size:10pt; background:transparent; color:DarkCyan; border-radius:4px;
}
 A:hover
{
 font-size:10pt; background:yellow; color:black; border:1px solid black;
 box-shadow:1px 1px 3px #222222; border-radius:4px;
}
 A:active
{
 font-size:10pt; background:yellow; color:black; border-radius:4px;
}


 HR {background:red; height:4px; border:0px;}


[title-text]:hover:after
{
 opacity:1.0;
 transition:all 1.0s ease 1.0s;
 text-align:left;
 visibility:visible;
}

[title-text]:after
{
 opacity:1.0;
 content:attr(title-text);
 text-align:left;
 left:50%;
 background-color:yellow;
 color:black;
 font-size:10pt;
 position:absolute;
 padding:1px 5px 2px 5px;
 white-space:pre;
 border:1px solid red;
 z-index:1;
 visibility:hidden;
}

[title-text] {position: relative;}


::selection{background-color:yellow !important; color:black !important;}
::-moz-selection{background-color:yellow !important; color:black !important;}
</style>

</head>

<body>

<!-- Define container form --->
<form name="form1" method="post" action="">

<!-- Define main page title/header. --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">


<tr><td colspan="99" style="color:white; background-color:#000066; border:2px solid white; border-radius:8px 8px 0px 0px;">
$_INTERFACE_TITLE_</td></tr>
</table>


<!-- Define input  text boxes  --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr><td style="background:LightYellow; line-height:175%; border-radius:0px 0px 8px 8px;">
Date <input name="BCAD"    type="text" value="
$BCAD"         size="3" maxlength="2"><input name="Year"         type="text" value="$Year"         size="5" maxlength="4">
<input name="Month"        type="text" value="
$Month"        size="4" maxlength="3">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Time Zone <input name="TimeZone"     type="text" value="
$TimeZone"     size="7" maxlength="6">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Daylight/Summer Time <input name="DaySumTimeYN" type="text" value="
$DaySumTimeYN" size="4" maxlength="3" title=' No = Standard Time \n Yes = Daylight Saving / Summer Time '>

</td></tr>
</table>


<!-- Define [SUBMIT] button --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr><td colspan="99" style="background-color:black;"><input type="submit" name="SubmitButton" value=" S U B M I T ">
<br><br>
<a href='View-Source-Code.php' target='_blank'
style='font-family:Verdana; color:black; background:yellow;
            text-decoration:none; border:1px solid black; padding:4px;
            border-radius:4px;'>
            &nbsp;View/Copy PHP Source Code&nbsp;</a></td></tr>
</table>










<!-- Define TextArea1 --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="text-align:center; color:GreenYellow; background-color:black;">Double-Click Within Text Area to Select ALL Text<br>
<textarea ID="TextArea1" name="TextArea1" style="color:
$TxColor; background:$BgColor; padding:6px;" cols="$Text1Cols" rows="$Text1Rows" ReadOnly OnDblClick="this.select();" OnMouseUp="return true;">
$TextArea1Text
</textarea>
</td>
</tr>
</table>


<!-- Define TextArea2 --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="text-align:center; color:GreenYellow; background:black;">Double-Click Within Text Area to Select ALL Text<br>

<textarea ID="TextArea2" name="TextArea2" style="color:black; background:white; padding:6px;" cols="
$Text2Cols" rows="$Text2Rows" ReadOnly OnDblClick="this.select();" OnMouseUp="return true;">
$TextArea2Text
</textarea>


</tr>
</table>


<!-- Define page footer --->
<table width="
$TableWidth" align="center" border="0" cellspacing="1" cellpadding="3">
<tr>
<td colspan="99" style="color:GreenYellow; background:black;">PHP Program by 
$_AUTHOR_<br><span style="color:silver; background:black;">$_REVISION_DATE_</span></td>
</tr>
</table>

</form>
<!-- End of container form --->


<!-- Extra bottom scroll space --->
<br><br><br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br><br><br><br>

</body>
</HTML>


_HTML;











/*
   ###########################################################################
   This function generates a table of geocentric lunar phases starting on any
   given date and spanning a given interval at regular time steps.

   It can also be used for a single computation for any given date and time.

   It takes into account the Time Zone and Daylight Saving/Summer Time.
   A table header is optional.

   ###########################################################################
*/

   
function Lunar_Phase_Table ($StartDateTimeStr,$StopDateTimeStr,$StepSize,
                               
$TimeZone='+00:00',$DaySumYN='No',
                               
$HeaderYN='No')
{
   
$StartDateTimeStr trim($StartDateTimeStr);
   
$StopDateTimeStr  trim($StopDateTimeStr);

   
$HeaderYN substr(StrToUpper(trim($HeaderYN)),0,1);

/* -----------------------------------------------------------
   Adjust for Daylight/Summer Time, if indicated. This assumes
   that the Time Zone string is given in the standard +-HH:mm
   format or an error may occur.
*/
   
$DaySumYN  substr(StrToUpper(trim($DaySumYN)),0,1);
   
$DSSTAdj = ($DaySumYN == 'N')? 0:1;

   list(
$TZHH$TZmm) = PReg_Split("[\:]"$TimeZone);
   
$TZSign substr($TZHH,0,1);
   
$TZHours = (($TZSign == '-')? -1:1)*(abs($TZHH) + $TZmm/60) + $DSSTAdj;
   
$i StrPos($TZHours'.');  if ($i == FALSE) {$TZHours .= '.00';}
   
$i StrPos($TZHours'.');
   
$TZHH $TZSign.SPrintF("%02d"abs(substr($TZHours,0,$i)));
   
$TimeZone URLEncode("$TZHH:$TZmm");

// **********************************************************
// MOON - GEOCENTRIC ECLIPTICAL LONGITUDE AND LATITUDE (GELL)

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='301'"                     .
   
"&OBJ_DATA='NO'"                     .
   
"&MAKE_EPHEM='YES'"                  .
   
"&EPHEM_TYPE='OBSERVER'"             .
   
"&CAL_FORMAT='BOTH'"                 .
   
"&CAL_TYPE='MIXED'"                  .
   
"&REF_SYSTEM='ICRF'"                 .
   
"&APPARENT='AIRLESS'"                .
   
"&RANGE_UNITS='AU'"                  .
   
"&CENTER='500@399'"                  .
   
"&TIME_DIGITS='SECONDS'"             .
   
"&TIME_ZONE='$TimeZone'"             .
   
"&START_TIME='$StartDateTimeStr UT'" .
   
"&STOP_TIME='$StopDateTimeStr'"      .
   
"&STEP_SIZE='$StepSize'"             .
   
"&EXTRA_PREC='YES'"                  .
   
"&CSV_FORMAT='YES'"                  .
   
"&QUANTITIES='31,29'"                ;

// -----------------------------------------------------------------
// Get the geocentric ecliptical longitude and latitude of the moon.

   
$MoonGELL Str_Replace(",\n"" \n"trim(File_Get_Contents($From_Horizons_API)));

/* ----------------------------------------------------
   Set pointers to start and end of ephemeris table and
   extract ONLY the required ephemeris CSV data lines
   from between the Start/End pointers.
*/
   
$i StrPos($MoonGELL'$$SOE');
   
$j StrPos($MoonGELL'$$EOE');

   
$MoonTable trim(substr($MoonGELL$i+5$j-$i-5));
   
$MoonTable = (substr($MoonTable,0,1) <> 'b')?
                 
$MoonTable$MoonTable;





// *********************************************************
// SUN - GEOCENTRIC ECLIPTICAL LONGITUDE AND LATITUDE (GELL)

   
$From_Horizons_API =
   
"https://ssd.jpl.nasa.gov/api/horizons.api?format=text" .
   
"&COMMAND='10'"                      .
   
"&OBJ_DATA='NO'"                     .
   
"&MAKE_EPHEM='YES'"                  .
   
"&EPHEM_TYPE='OBSERVER'"             .
   
"&CAL_FORMAT='BOTH'"                 .
   
"&CAL_TYPE='MIXED'"                  .
   
"&REF_SYSTEM='ICRF'"                 .
   
"&APPARENT='AIRLESS'"                .
   
"&RANGE_UNITS='AU'"                  .
   
"&CENTER='500@399'"                  .
   
"&TIME_DIGITS='SECONDS'"             .
   
"&TIME_ZONE='$TimeZone'"             .
   
"&START_TIME='$StartDateTimeStr UT'" .
   
"&STOP_TIME='$StopDateTimeStr'"      .
   
"&STEP_SIZE='$StepSize'"             .
   
"&EXTRA_PREC='YES'"                  .
   
"&CSV_FORMAT='YES'"                  .
   
"&QUANTITIES='31,29"                 ;

// ----------------------------------------------------------------
// Get the geocentric ecliptical longitude and latitude of the sun.

   
$SunGELL Str_Replace(",\n"" \n"trim(File_Get_Contents($From_Horizons_API)));

/* ----------------------------------------------------
   Set pointers to start and end of ephemeris table and
   extract ONLY the required ephemeris CSV data lines
   from between the Start/End pointers.
*/
   
$i StrPos($SunGELL'$$SOE');
   
$j StrPos($SunGELL'$$EOE');

   
$SunTable trim(substr($SunGELL$i+5$j-$i-5));
   
$SunTable = (substr($SunTable,0,1) <> 'b')? $SunTable$SunTable;

/* ------------------------------------------
   PARSE THE TABLES AND CONSTRUCT A SINGLE
   CUSTOMIZED GEOCENTRIC LUNAR PHASE TABLE.

=========================================================
  Loc_Date    Loc_Time  Cnst   Julian_Date_UT   Phase_Ang
============  ========  ==== =================  =========
 2025-Feb-01  00:00:00  Aqr  2460707.708333333   35.36319
 ...

*/

   
$LunarPhaseAngleTable '';

/* --------------------------------------------
   Store lunar and solar ecliptical coordinates
   in their respective work arrays.
*/
   
$MoonLonArray PReg_Split("[\n]"$MoonTable);
   
$MoonLonCount count($MoonLonArray);

   
$SunLonArray PReg_Split("[\n]"$SunTable);
   
$SunLonCount count($SunLonArray);


/* -----------------------------------
   Fatal error if unequal array count.
*/
   
if ($MoonLonCount <> $SunLonCount)
      {exit (
"FATAL ERROR: Unequal array count.");}

/* -------------------------------------------
   Construct geocentric ecliptical coordinates
   and lunar phase table.
*/
   
for ($i=0;   $i $MoonLonCount;   $i++)
       {
        
$CurrMoonLine $MoonLonArray[$i];
        
$CurrSunLine  $SunLonArray[$i];

        list (
$DateTimeStr,$JDate,$w,$w,$CurrMoonLon,$w$MoonCnst) = PReg_Split("[,]"$CurrMoonLine);
        list (
$w,$JDate,$w,$w,$CurrSunLon) = PReg_Split("[,]"$CurrSunLine);

        
$DateTimeStr Str_Replace(' ''  'trim($DateTimeStr));
        
$DateTimeStr = (substr($DateTimeStr,0,1) <> 'b')? $DateTimeStr$DateTimeStr;

//      ----------------------------------------------------------
//      Get IAU symbol for constellation in which moon is located.

        
$MoonCnst trim($MoonCnst);

//      -------------------------------
//      Get longitudes of moon and sun.

        
$Lm trim($CurrMoonLon);
        
$Ls trim($CurrSunLon);

//      -------------------------------
//      Compute the simple phase angle.

        
$w 360 $Lm $Ls;

        
$PhaseAng SPrintF("% 9.5f"360 - ($w -= ($w 360)? 360:0));

//      -----------------------------------------
//      Construct current output table text line.

        
$LunarPhaseAngleTable .= "$DateTimeStr  $MoonCnst $JDate  $PhaseAng\n";
       }
        
$LunarPhaseAngleTable RTrim($LunarPhaseAngleTable);


// ###############################################

   
$LunarPhaseAngleTable Str_Replace('  ''|'$LunarPhaseAngleTable);

   
$wArray PReg_Split("[\n]"trim($LunarPhaseAngleTable));
   
$wCount count($wArray);

   
$wOut '';

   for (
$i=0;   $i $wCount;   $i++)
   {
    
$CurrLine trim($wArray[$i]);

    if (
substr($CurrLine,0,1) == 'b')
       {
        
$wOut .= 'BC ' substr($CurrLine,1,StrLen($CurrLine))."\n";
       }
    else
       {
        
$wOut .= "AD $CurrLine\n";
       }
   }

   
$wOut Str_Replace('|''  '$wOut);

   
$MoonPhaseAnglesTable $wOut;

// ###############################################

// --------------------------------------------
// Return table with header text, if indicated.

   
if ($HeaderYN == 'Y')
  {
   return
"
$MoonPhaseAnglesTable
===========================================================
   Loc_Date     Loc_Time  Cnst   Julian_Date_UT   Phase_Ang
==============  ========  ==== =================  =========
"
;
  }

// --------------------------------------------
// Otherwise, return table without header test.

//   return $MoonPhaseAnglesTable;

/*
   Append function code to extract table to the end of this function.
*/

   
$PATable trim($MoonPhaseAnglesTable);

// --------------------------------------------
// Store table into text string working  array.

   
$wArray PReg_Split("[\n]"$PATable);
   
$wCount count($wArray);

   
$QPADates '';

   for (
$i=1;   $i $wCount;   $i++)
{
        
$PrevLine trim($wArray[$i-1]);
        
$PrevPA   substr($PrevLine, -10);
        
$CurrLine trim($wArray[$i-0]);
        
$CurrPA   substr($CurrLine, -10);

// -------------------
// Check for New Moon.

   
if (($CurrPA $PrevPA) <= 0)
  {
   
$QPADates .= substr($PrevLine,0,14) . "  New Moon\n";
  }

// ------------------------
// Check for First Quarter.

   
if (($PrevPA 90 and $CurrPA >= 90))
      {
       
$QPADates .= substr($PrevLine,0,14) . "  First Quarter\n";
      }

// --------------------
// Check for Full Moon.

   
if (($PrevPA 180 and $CurrPA >= 180))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  Full Moon\n";
  }

// -----------------------
// Check for Last Quarter.

   
if (($PrevPA 270 and $CurrPA >= 270))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  Last Quarter\n";
  }

// End of  for(...)

//   return $PATable;


// @@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@
//exit("<pre>942:\n$QPADates</pre>");

   
$PATable trim($MoonPhaseAnglesTable);

// --------------------------------------------
// Store table into text string working  array.

   
$wArray PReg_Split("[\n]"$PATable);
   
$wCount count($wArray);

   
$QPADates '';

   for (
$i=1;   $i $wCount;   $i++)
{
        
$PrevLine trim($wArray[$i-1]);
        
$PrevPA   substr($PrevLine, -10);
        
$CurrLine trim($wArray[$i-0]);
        
$CurrPA   substr($CurrLine, -10);

// -------------------
// Check for New Moon.

   
if (($CurrPA $PrevPA) <= 0)
  {
   
$QPADates .= substr($PrevLine,0,14) . "  New Moon\n";
  }

// ------------------------
// Check for First Quarter.

   
if (($PrevPA 90 and $CurrPA >= 90))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  First Quarter\n";
  }

// --------------------
// Check for Full Moon.

   
if (($PrevPA 180 and $CurrPA >= 180))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  Full Moon\n";
  }

// -----------------------
// Check for Last Quarter.

   
if (($PrevPA 270 and $CurrPA >= 270))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  Last Quarter\n";
  }

// End of  for(...)

   
$MoonPhaseAnglesTable trim($MoonPhaseAnglesTable);

// DONE:
   
return trim($QPADates) .
"

-----------------------------------------------------------
 TABLE OF SIMPLE MOON PHASE ANGLES FOR 00:00 ON EACH DATE

===========================================================
   Loc_Date     Loc_Time  Cnst   Julian_Date_UT   Phase_Ang
==============  ========  ==== =================  =========
$MoonPhaseAnglesTable
==============  ========  ==== =================  =========
   Loc_Date     Loc_Time  Cnst   Julian_Date_UT   Phase_Ang
==========================================================="
;


// End of  Lunar_Phase_Table (...)































/* ###########################################################################
   Given a moon phase angles table, this function will scan the table and then
   extract ONLY the dates of the moon quarter phases.

   When two successive values meet the test conditions, then the first of the
   two corresponding dates is the phase date on the calendar.

   Phase Ang      Phase ID      Phase Seeking Rule Within Table
   ---------    -------------   --------------------------------
        0       New Noon        (CurrPA - PrevPA) < 0
       90       First Quarter   (PrevPA <  90 and CurrPA >=  90)
      180       Full Moon       (PrevPA < 180 and CurrPA >= 180)
      270       Last Quarter    (PrevPA < 270 and CurrPA >= 270)

   ###########################################################################
*/
   
function Extract_Phase_Dates ($MoonPhaseAnglesTable)
{
   
$PATable $xPATable trim($MoonPhaseAnglesTable);

// --------------------------------------------
// Store table into text string working  array.

   
$wArray PReg_Split("[\n]"$PATable);
   
$wCount count($wArray);

   
$QPADates '';

   for (
$i=1;   $i $wCount;   $i++)
{
        
$PrevLine trim($wArray[$i-1]);
        
$PrevPA   substr($PrevLine, -10);
        
$CurrLine trim($wArray[$i-0]);
        
$CurrPA   substr($CurrLine, -10);

// -------------------
// Check for New Moon.

   
if (($CurrPA $PrevPA) <= 0)
  {
   
$QPADates .= substr($PrevLine,0,14) . "  New Moon\n";
  }

// ------------------------
// Check for First Quarter.

   
if (($PrevPA 90 and $CurrPA >= 90))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  First Quarter\n";
  }

// --------------------
// Check for Full Moon.

   
if (($PrevPA 180 and $CurrPA >= 180))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  Full Moon\n";
  }

// -----------------------
// Check for Last Quarter.

   
if (($PrevPA 270 and $CurrPA >= 270))
  {
   
$QPADates .= substr($PrevLine,0,14) . "  Last Quarter\n";
  }

// End of  for(...)

   
return "$xPATable\n" trim($QPADates);
}







/*
   *******************************************************************
   *******************************************************************
*/

/*
   This is the  inclusion module with all functions required for the program
   to compute the calendar date (without times) of all the quarter phases of
   the moon for any given year/month.

   Random Uniqueness Code:
   KQ6Z03qyWPu5GhofUJ09q683UF5cBzVI
*/



/*
   ###########################################################################
   This function returns the Julian Day Number for any given calendar date in
   the range from BC 19999 to AD 19999 on the old Julian calendar or on the
   modern Gregorian calendar system.

   It returns negative JD numbers for dates prior to mathematical origins of
   the respective calendar systems.

   --------------------
   CALENDAR YEAR RANGE:

   BC 19999  to  AD 19999
   There is no calendar year 0 (zero).


   --------------------------------------------------------
   For the mathematical year number (y) used in calendrical
   computations for a BC calendar year Y = ('BC Year'):
   Y = 'BC 1949'  --->  y = '-1948'

   We take the numerical value  following the 'BC' prefix
   and subtract 1  and then change the result to negative
   to obtain the mathematical year value required for use
   in the (JDNum) computation.  This adjustment will only
   apply to 'BC' date strings.

   Example:  'BC 1949-May-20'
              y = -(Y - 1)
                = -(1949 - 1)
                = -1948
   So, (y,m,d)  = (-1948,5,20)


   -------------------------------------------------------------
   Positive calendar year numbers do NOT require any adjustment.
   Example:  'AD 1949-May-20'
   So, (y,m,d) = (1949,5,20)


   --------------------------------------------
   MATHEMATICAL ORIGINS OF THE CALENDAR SYSTEMS

   BC 4713-Jan-01-Mon   JDNum = 0   On the old Julian calendar
   BC 4714-Nov-24-Mon   JDNum = 0   on the modern Gregorian calendar

   NOTE:
   If a year is  given as a negative number,  it refers to a 'BC' year
   and will be converted to 'BC|AD' format internally for computations
   because 'BC' years first require a special numerical adjustment not
   needed for 'AD' years.

   Month = 1 to 12 or as 3-letter abbreviation string ('Jan' to 'Dec').
   Date strings are NOT case-sensitive.

   The returned signed JD Number is left-space-padded to 8 characters
   to facilitate easy columnar alignment if used for tabulation.

   ARGUMENTS:
   $BCADDateStr = Date string in BC|AD format.
                  VALID EXAMPLES:
                  'BC 9949-May-20'
                  'AD 1949-5-20'
                  '16959-1-29'
                  '-1023-Nov-15'

   JGAMode = Calendar mode to apply, where:
             'A' = Auto-select mode = Default
             'G' = Gregorian
             'J' = Julian

   ERRORS:
   FALSE is returned if an invalid argument is detected.

   NO DEPENDENCIES

   ###########################################################################
*/

   
function JD_Num ($BCADDateStr$JGAMode='A')
{
// --------------------------------------------------
// Read and adjust input date string argument format.

   
$BCADDateStr PReg_Replace("/\s+/"" "trim($BCADDateStr));


/* ---------------------------------------------------
   If first character is a minus sign (negative year),
   then convert it into a 'BC' calendar year string.

   '-1949' becomes ---> 'BC 1949'
*/
   
if (substr($BCADDateStr,0,1) == '-')
      {
$BCADDateStr 'BC ' substr($BCADDateStr1StrLen($BCADDateStr));}


// ---------------------------------------------------------------
// If no 'BC|AD' prefix at all, then attach a default 'AD' prefix.

   
$ww StrToUpper(substr($BCADDateStr,0,2));
   if (
$ww <> 'BC' and $ww <> 'AD') {$BCADDateStr "AD $BCADDateStr";}


// ------------------------------
// Read and parse date arguments.

   
list($BCADYear,$Month,$Day) = PReg_Split("[-]"$BCADDateStr);


// ------------------
// A few adjustments.
   
$BCADYear trim($BCADYear);
   
$Month    trim($Month);
   
$Day      trim($Day);


// -----------------------------------
// Get BC|AD prefix and calendar year.

   
$BCAD StrToUpper(substr($BCADYear0,2));
   
$Y trim(substr($BCADYear2StrLen($BCADYear)));


// ---------------------------------
// Adjust for BC year, if necessary.

   
if ($BCAD == 'BC') {$Y = -$Y;}


// ------------------------------------------------------------
// Read calendar year argument value and return FALSE on error.

   
$w abs($Y);  if ($w < -20000 or $w == or $w 20000)  {return FALSE;}


// ---------------------------------------------------
// Read month argument. Could be a string or a number.

   
$m UCFirst(substr(StrToLower(trim($Month)),0,3));


// ------------------------
// Read day argument value.

   
$d trim($Day);


/* ----------------------------
  Read calendar mode argument.
  'G' = Gregorian | 'J' = Julian
  'A' = Auto-select = Default
*/
   
$JGAMode substr(StrToUpper(trim($JGAMode)),0,1);
   if (
$JGAMode == '') {$JGAMode 'A';}


// -------------------------------------------------
// Define abbreviations for month and weekday names.

   
$MONTHS   'JanFebMarAprMayJunJulAugSepOctNovDec';
   
$WEEKDAYS 'SunMonTueWedThuFriSat';
   
$JGDiff   0;


/* -----------------------------------------------------
   If month is a 3-letter abbreviation ('Jan' to 'Dec'),
   then replace it with the month number 1 to 12, if
   possible.  Otherwise, return FALSE if it cannot
   resolve the abbreviation text.
*/
   
if (!Is_Numeric($m))
      {
       
$i StrPos($MONTHS$m);
       if (
$i === FALSE) {return $i;}
       
$m $i/3;
      }


// ---------------------------------------
// Error if invalid month number.

   
if ($m or $m 12) {return FALSE;}


/* -------------------------------------------------
   Proceed to compute the Julian calendar JD Number.
   This is the base JD Number value. If the Gregorian
   calendar is selected, then the difference between
   the calendars is applied to obtain the Gregorian
   calendar JD Number.
*/
   
$A floor((14-$m) / 12);
   
$B = (($Y 0)? $Y+$Y) - $A;
   
$C floor($B/100);

   
$JDNum floor(30.6001*(12*$A $m 1))
          + 
floor(365.25*($B 4716)) - 1524 $d;


/* ----------------------------------------------
   Handle automatic calendar mode selection.
   If calendar mode = 'A' = Auto-select, then the
   calendar mode is automatically determined by
   the computed JD Number value.
*/

   
if ($JGAMode == 'A')
      {
       
$JGAMode = ($JDNum 2299161)? 'J':'G';
      }


/* ---------------------------------------------
   Handle Gregorian (= default) calendar mode by
   by ADDING the difference  in days between the
   Julian and Gregorian JD Numbers, if indicated
   by the JGAMode setting.  This value could be
   negative or positive, depending on the given
   date.  Default Logic: If not 'J', then 'G'.

   GregorianJDNum = JulianJDNum + (+-JGDiff)
*/

   
if ($JGAMode <> 'J')
      {
       
$A = ($Y 0)? $Y+$Y;
       
$B trim($m);
       
$C $A floor((14-$B) / 12);
       
$D floor($C/100);
       
$JGDiff = (floor($D/4) - $D 2);
      }
       
$JDNum += $JGDiff;


/* -------------------------------------------------
   Error if (JDNum) is outside of the valid calendar
   range from:
   BC 19999-Jan-01-Thu J   -5584211
   BC 19999-Jan-01-Tue G   -5583059
   to
   AD 19999-Dec-31-Sat J    9026057
   AD 19999-Dec-31-Fri G    9025909
*/
   
if ($JDNum < -5584211 or $JDNum 9026908) {return FALSE;}


/* -----------------------------------------------
   Left-pad the signed JD number digits field with
   spaces to span exactly 8 characters width, just
   in case the values are used in a table.  This
   helps to arrange any (JDNum) column uniformly.

   The white space can be removed by performing
   a simple trim(JDNum) command, if not wanted.
*/
   
$JDNum SPrintF("% 8d"$JDNum);


// DONE:
   
return $JDNum;

// End of  JD_Num(...)




/*
   ###########################################################################
   This function is the inverse of the JD number function. Given any signed
   JD Number, it will return the corresponding calendar date string in
   the same format as:  'BC|AD Yyyyy-Mmm-dd-DoW'

   The year will be formatted to 5 digits padded with zeros as needed.
   EXAMPLE: 'BC 17191-May-12',  'AD 01949-June-02',  'BC 00107-Oct-13'

   CALENDAR YEAR RANGE:
   BC 19999 Jan-01  to  AD 19999-Dec-31
   There is no calendar year 0 (zero).

   Mathematical origins of the calendar systems:
   BC 4713-Jan-01-Mon   JDNum = 0   On old Julian calendar
   BC 4714-Nov-24-Mon   JDNum = 0   On modern Gregorian calendar

   ARGUMENT:
   JDNumber = Julian Day number for the calendar date to be computed.

   JGAMode = Calendar mode
             'G' = Gregorian
             'J' = Julian
             'A' = Auto-select mode = Default

   RETURNS:
   Calendar date string in  'BC|AD Yyyy-Mmm-dd-DoW'  format
   according to the selected calendar mode.

   ERRORS:
   FALSE is returned if JD number argument is non-numeric.

   NO DEPENDENCIES
   ###########################################################################
*/

   
function Inv_JD_Num ($JDNumber$JGAMode='A')
 {
   
$JDNum trim($JDNumber);

// -------------------------------------------
// Define Month and Day-Of-Week abbreviations.

   
$MONTHS   'JanFebMarAprMayJunJulAugSepOctNovDec';
   
$WEEKDAYS 'SunMonTueWedThuFriSat';

// ----------------------------
// Read calendar mode argument.
// 'G' = Gregorian = Default
// 'J' = Julian
// 'A' = Auto-select

   
$JGAMode substr(StrToUpper(trim($JGAMode)),0,1);
   if (
$JGAMode == '') {$JGAMode 'A';}

// ----------------------------------------------
// If calendar mode = 'A' = Auto-select, then the
// calendar mode is automatically determined by
// the given JD Number argument.

   
if ($JGAMode == 'A')
      {
       
$CMode  = ($JDNum 2299161)? 0:1;
       
$JGAMode = ($CMode == 0)? 'J':'G';
      }
   else
      {
       
$CMode = ($JGAMode == 'J')? 0:1;
      }

// -----------------------------------------
// Compute numerical date elements (y, m, d)
// according to the calendar mode selection.

  
$A floor($JDNum 0.5);
  
$B $CMode*floor(($A 1867216.25) / 36524.25);
  
$C $A $CMode*($B floor($B/4) + 1);
  
$D $C 1524;
  
$E floor(($D 122.1) / 365.25);
  
$F floor(365.25 $E);
  
$G floor(($D $F) / 30.6001);
  
$d $D $F floor(30.6001 $G);     // Day num   (1 to 31)
  
$m $G 12*floor($G/14) - 1;          // Month num (1 to 12)
  
$y $E 4716 floor((14 $m) / 12); // Mathematical year
  
$Y = ($y 0)? $y $y-1;               // Calendar year (Negative = BC)

// ------------------------------------------------------------
// At this point we have the numerical date elements (y, m, d).
// The next step is to construct the full calendar date text
// string for output. EXAMPLE OUTPUT: 'BC 9998-May-20-Tue'

   
$i     = (+ ($JDNum 1) % 7) % 7;
   
$DoW   substr($WEEKDAYS3*$i3);
   
$Y     SPrintF("%+04d"$Y);
   
$Y     Str_Replace('-''BC '$Y);
   
$Y     Str_Replace('+''AD '$Y);
   
$Mmm   substr($MONTHS3*($m-1), 3);
   
$dd    SPrintf("%02d"$d);
   
$JDNum SPrintF("% 8d"$JDNum);

// DONE.
   
return "$Y-$Mmm-$dd-$DoW";

 } 
// End of  Inv_JD_Num(...)




/*
   ###########################################################################
   This function converts a HMS time string into equivalent decimal hours.

   INPUT: 1, 2 or 3-element HMS (Hours Minutes Seconds) separated by spaces
          as angular string as 'hrs min sec' values.

   Example: Given HMS string = '19 12 50.306'
            Returned = 1.2139738888888889

   NO DEPENDENCIES

   ERRORS:
   No special error checking is done.
   ###########################################################################
*/

   
function HMS_to_Hours ($HMSstr)
{
   
$hms StrToLower(trim($HMSstr));

/* ----------------------------------------------------
   Normalize the spacing and then split and extract the
   individual angular string elements (dh or hh, mm,ss).
*/
   
$hms PReg_Replace("/\s+/"" "trim($hms));
   
$whms PReg_Split("[ ]"$hms);
   
$whmscount count($whms);
   
$hh = ($whmscount >= 1)? bcAdd((float)$whms[0],'0'20) : '0';
   
$mm = ($whmscount >= 2)? bcAdd($whms[1],'0'20) : '0';
   
$ss = ($whmscount >= 3)? bcAdd($whms[2],'0'20) : '0';

/* -------------------------------------------------
   Remember and then remove any numerical (+/-) sign
   for reattachment to the returned output.
*/
   
$NumSign = (substr($HMSstr,0,1) == '-')? '-' '';
   
$hh Str_Replace('+'''Str_Replace('-'''$hh));

/* -------------------------------------------------------------
   If original angle argument began with a + sign, then preserve
   it so that all returned positive results will have a + sign.
   Otherwise, positive results will NOT have a + sign.
*/
   
if (substr($HMSstr,0,1) == '+') {$NumSign '+';}

// ----------------------------------------------
// Compute decimal hours value equivalent
// to the given HHMS argument elements.

   
$w2 bcAdd(bcAdd(bcMul($hh,"3600",20),bcMul($mm,"60",20),20),$ss,20);

// -----------------------------------------------------------
// If result equates to zero, then suppress any numerical sign.

   
if (bcComp($w2'0'20) == 0) {$NumSign '';}

// ---------------------------------------------------------
// Round off result to 16 decimals, recalling original sign.

   
$w $NumSign bcAdd(bcDiv($w2,"3600",20), "0.00000000000000005",16);

   
$w RTrim(RTrim($w'0'), '.');

   return 
$w;


// end of  HMS_to_Hours (...)





/* ###########################################################################
   This function returns the equivalent H:M:S time string with
   several formatting options.

  hours         = Signed decimal hours value
  ssDecimals    = Number of decimals in seconds part

  posSign = Symbol to use for positive values ('' or '+')
      ''  = Empty = Return numbers only, no symbols.
      '+' = Attach '+' sign to positive values.
            Has no effect on negative values.

  SymbolsMode   = If or not to attach time symbols (h m s) or (:)
                  'h' = '01h 02m 03s' (Default)
                  ':' = '01:02:03'
                  ''  = '01 02 03'

   NO DEPENDENCIES

   ERRORS
   No special  error checking is done by this function.
*/

   
function Hours_to_HMS ($hours$ssDec=0$posSignSymb=''$SymbMode='h')
{
// Initialize symbol carriers.
   
$_h_ $_m_ $_s_ '';
   if (
trim($posSignSymb) == '')  {$posSignSymb FALSE;}

// Remember original numerical sign and work with absolute value.
   
$sign = ($hours 0)? '-' '';    $hours abs($hours);

// Remember numerical sign to be restored on exit, if any.
   
if (($posSignSymb === TRUE or $posSignSymb == '+') and $sign == '')
       {
$sign '+';}

// Compute time elements from absolute hours argument.
   
$hh floor($hours);
   
$minutes 60*($hours $hh);    $mm floor($minutes);
   
$seconds 60*($minutes $mm);  $ss SPrintF("%1.3f",  $seconds);

// Format the time elements.
   
$hh SPrintF("%02d",  $hh);
   
$mm SPrintF("%02d",  $mm);
   
$ss SPrintF("%1.$ssDec"f"$ss);

// Patch for that blasted 60s glitch.
   
if ($ss == 60) {$ss 0$mm++;}
   if (
$mm == 60) {$mm 0$hh++;}

   
$hh SPrintF("%02d",  $hh);
   
$mm SPrintF("%02d",  $mm);

   
$ss SPrintF("%1.$ssDec"f"$ss);
   if (
$ss 10) {$ss "0$ss";}

// Attach optional (h, m, s, :) symbols as indicated.
// Default = 'h'
   
if ($SymbMode == or $SymbMode == '')
      {
$_h_  =  $_m_  =  $_s_  ' ';}

   if (
$SymbMode == or $SymbMode == ':')
      {
$_h_  =  ':';  $_m_  =  ':';  $_s_  '';}

   if (
$SymbMode == or StrToLower($SymbMode) == 'h')
      {
$_h_  =  'h ';  $_m_  =  'm ';  $_s_  =  's';}

   
$w "$sign$hh$_h_$mm$_m_$ss$_s_";

// Done.
   
return $w;

// End of  Hours_to_HMS(...)



/*
   This function returns an alternate month designation.
   If given a month number, it returns the 3-letter English
   abbreviation of the corresponding month,  if given the
   3-letter English month abbreviation, it will return the
   number of the corresponding month.

   ERRORS:
   If the month number or 3-letter abbreviation cannot be
   resolved, then FALSE is returned.
*/

   
function Alt_Month ($MonthStr)
{

// -----------------------------------
// Define English month abbreviations.

   
$MONTHS 'JanFebMarAprMayJunJulAugSepOctNovDec';

/* ---------------------------------------------------------
   Read month argument. This may be a number from 1 to 12 or
   a 3-letter English month abbreviation from 'Jan' to 'Dec'
*/
   
$mStr substr(UCFirst(StrToLower(trim($MonthStr))),0,3);

/* ----------------------------------------------------
   Handle the case of a numeric argument and return the
   corresponding 3-letter English month abbreviation.
*/
   
if (Is_Numeric($mStr) and $mStr and $mStr 13)
      {
       return 
substr($MONTHS3*($mStr-1), 3);
      }


/* ---------------------------------------------------------
   Check if the 3-letter English month abbreviation is found
   in the MONTHS string. If not, then return FALSE. A partial
   match is also allowed.
*/
   
$i StrPos($MONTHS$mStr);  if ($i === FALSE) {return $i;}


/* -------------------------------------------------------------
   Return the month number corresponding to the 3-letter English
   month abbreviation.
*/
   
return StrPos($MONTHS$mStr)/3;
}



/*
  This function returns the number of days in any given month.

  DEPENDENCIES:
  Alt_Month()
  Is_Leap_Year()
*/
   
function Month_Days ($YearStr$MonthStr$JGAMode='A')
{
//   $MDAYS = '312831303130313130313031';

   
$Y StrToUpper(trim($YearStr));
   
$m trim($MonthStr);

        if (!
Is_Numeric($Y))
           {
            
$Y Str_Replace('BC''-'$Y);
            
$Y Str_Replace('AD''',  $Y);
           }
   
$Y Str_Replace(' '''$Y);
   
$m trim($MonthStr);  if (!Is_Numeric($m)) {$m Alt_Month($m);}

// Determine the J or G calendar mode.

   
$JGAMode substr(StrToUpper(trim($JGAMode)),0,1);

/* ------------------------------------------------
   Get days in month assuming 28 days for February.
   Any leap year adjustment is added later.
*/
   
$mDays substr('312831303130313130313031'2*($m-1), 2);

/* ----------------------------------------------
   Handle automatic calendar mode selection.
   If calendar mode = 'A' = Auto-select, then the
   calendar mode is automatically determined by
   the calendar year (Y) value.
*/
   
if ($JGAMode == 'A') {$JGAMode = ($Y 1583)? 'J':'G';}

/* -------------------------------------------------------
   If it is a century year AND ALSO a Gregorian year, then
   an extra rule applies.  A century year is a leap year
   ONLY if it is a perfect multiple of 400, otherwise it
   is a common year.
*/
   
if ($m == 2) {$mDays += ((Is_Leap_Year ($Y$JGAMode))? 1:0);}

   return 
$mDays;
}









/*
   ###########################################################################
   This boolean function determines if a given year on the Gregorian calendar
   or the old Julian calendar is a common year or a leap year.

   Y = Calendar Year Number.
       There is no year 0 (zero) on the calendar.

   CalMode = 'G' for Gregorian (default) or 'J' for Julian calendar
             Logic: If not Julian, then Gregorian by default.

   Returns boolean FALSE if given year is a common year.
   Returns boolean TRUE  if given year is a leap year.

   ERRORS
   No special error checking is done.

   NO DEPENDENCIES
   ###########################################################################
*/

   
function Is_Leap_Year ($Year$JGAMode='A')
{
   
$Y trim($Year);
        if (
$Y == '') {$Y 'AD ' date("Y-M-d");}
        if (!
Is_Numeric($Y))
           {
            
$Y Str_Replace('BC''-'$Y);
            
$Y Str_Replace('AD''',  $Y);
           }

   
$Y Str_Replace(' '''$Y);
   
$y = ($Y 0)? $Y-$Y;

/* ----------------------------------------------
   Handle automatic calendar mode selection.
   If calendar mode = 'A' = Auto-select, then
   the calendar mode is automatically determined
   by the calendar year (Y) value.
*/
   
$JGAMode StrToUpper(substr(trim($JGAMode),0,1));

   if (
$JGAMode == 'A') {$JGAMode = ($y 1583)? 'J':'G';}

   return (
$JGAMode == 'J')? $y == :
          (
$y == and $y 100 <> 0) or $y 400 == 0;
}







   function 
Prev_Month_Last_Day ($YearStr$MonthStr$JGAMode='A')
{
   
$Y trim($YearStr);

        if (!
Is_Numeric($Y))
           {
            
$Y Str_Replace('BC ''-'$Y);
            
$Y Str_Replace('AD ''',  $Y);
           }

   
$BCAD = ($Y 0)? 'BC''AD';

   
$m trim($MonthStr);
        if (!
Is_Numeric($m)) {$m Alt_Month($m);}

   if (
$m ==  1) {$Y -= 1;  return "$BCAD $Y-Dec-31";}
   if (
$m == 12) {$Y += 1;  return "$BCAD $Y-Jan-31";}

   
$m -= 1;

   
$mDays Month_Days (trim($YearStr), $m$JGAMode);

   if (
Is_Numeric($m)) {$m Alt_Month($m);}

   
$Y    SPrintF("%04d"abs(IntVal($Y)));

   return 
"$BCAD $Y-$m-$mDays";
}






/*
   Get JD number of 1st day of month and add (mDays) to it to
   obtain the JD number for the first date of the next month
*/

   
function Next_Month_First_Day ($YearStr$MonthStr$JGAMode='A')
{
   
$Y trim($YearStr);

        if (!
Is_Numeric($Y))
           {
            
$Y Str_Replace('BC ''-'$Y);
            
$Y Str_Replace('AD ''',  $Y);
           }

   
$BCAD = ($Y 0)? 'BC''AD';

   
$m trim($MonthStr);
        if (!
Is_Numeric($m)) {$m Alt_Month($m);}

   
$mDays Month_Days (trim("$BCAD $Y"), $m$JGAMode);

   
$Y SPrintF("%04d"abs(IntVal($Y)));

   if (
$m ==  1) {$Y -= 1;  return "$Y-Feb-$mDays";}
   if (
$m == 12) {$Y += 1;  return "$Y-Jan-01";}

// JD number for first date of given month.
   
$JDNum01 JD_Num("$Y-$m-01"$JGAMode);

   
$m += 1;

   
$Y    SPrintF("%04d"abs(IntVal($Y)));

   if (
Is_Numeric($m)) {$m Alt_Month($m);}

   return 
"$BCAD $Y-$m-01";
}







   function 
Prev_and_Next_Months ($YearStr$MonthStr$JGAMode='A')
{
   
$Y StrToUpper(trim($YearStr));

   
$JGAMode StrToUpper(substr(trim($JGAMode),0,1));

/* --------------------------------------------------------
   If year is non-numeric, assume it has a BC/AD prefix and
   convert it into a signed value where negative = BC.
   --------------------------------------------------------
*/
   
if (!Is_Numeric($Y))
           {
            
$Y Str_Replace('BC ''-'$Y);
            
$Y Str_Replace('AD ''',  $Y);
           }

// ----------------------------
// Determine BC/AD era applies.

   
$BCAD = ($Y 0)? 'BC''AD';

   
$Y abs($Y);


// -------------------------------
// Get month as a numerical value.

   
$m trim($MonthStr);  if (!Is_Numeric($m)) {$m Alt_Month($m);}


// ----------------------------------
// Get number of days in given month.

   
$mDays Month_Days (trim("$BCAD $Y"), $m$JGAMode);

// ---------------------------------
// Cast Y value into 4-digit format.

   
$Y SPrintF("%04d"$Y);

// ----------------------------------------------
// Cast month as a 3-letter English abbreviation.

   
$m Alt_Month($m);

// ---------------------------------------------------
// Compute JD Number of first date of the given month.

   
$JDNum01 JD_Num("$BCAD $Y-$m-01"$JGAMode);

   
$JDNumPrev $JDNum01 1;
   
$JDNumNext $JDNum01 $mDays;

/* ---------------------------------------------
   Construct calendar date strings corresponding
   to the JD numbers.
*/
   
$DatePrev Inv_JD_Num($JDNumPrev$JGAMode);
   
$DatePrev substr($DatePrev0StrLen($DatePrev)-4);

   
$DateNext Inv_JD_Num($JDNumNext$JGAMode);
   
$DateNext substr($DateNext0StrLen($DateNext)-4);

// Return CSV date strings.
   
return "$DatePrev,$DateNext";
}



?>