package CalDate;

require Exporter;
@ISA = ('Exporter');
@EXPORT = qw(CalDate JulDate FormatCalDate);

##############################################################################
#	CalDate - pass in a pointer to a UTInstant
#	which has its julian decimal date set.  This routine
#	will calculate and set all the other elements of the
#	hash.  As a convenience, it will also return the year,
#	an integer.
#

sub CalDate() {
	my ($date) = @_;	#	pointer to the UTInstant
	#	These locals are all integers except for $frac
	my ($jd, $ka, $kb, $kc, $kd, $ke, $ialp, $frac);
	#	Thus, to be safer, we will use int() around all calculations

	$jd = int($date->{'J_DATE'} + 0.5);	# integer julian date
	$frac = $date->{'J_DATE'} + 0.5 - $jd + 1.0e-10; # fraction of day
	$ka = int($jd);
	
	if ( $jd >= 2299161 ) {
		$ialp = int(( $jd - 1867216.25 ) / ( 36524.25 ));
		$ka = int($jd + 1 + $ialp - ( $ialp >> 2 ));
	}
	$kb = int($ka + 1524);
	$kc = int(( $kb - 122.1 ) / 365.25);
	$kd = int($kc * 365.25);
	$ke = int(( $kb - $kd ) / 30.6001);

	$date->{'DAY'} = $kb - $kd - int( $ke * 30.6001 );

	if ( $ke > 13 ) {	$date->{'MONTH'} = int($ke - 13); }
	else {		$date->{'MONTH'} = int($ke - 1); }

	if ( ($date->{'MONTH'} == 2) && ($date->{'DAY'} > 28) ) {
		$date->{'DAY'} = 29;
	}

	if ( ($date->{'MONTH'} == 2) && ($date->{'DAY'} == 29) && ($ke == 3) ){
					 $date->{'YEAR'} = int($kc - 4716);
	} elsif ( $date->{'MONTH'} > 2 ) { $date->{'YEAR'} = int($kc - 4716);
	} else {			 $date->{'YEAR'} = int($kc - 4715); }

	$date->{'D_HOUR'} = $frac * 24.0;	#  d_hour includes min, sec as fraction
	$date->{'HOUR'} = int($date->{'D_HOUR'});	#	integer hour
	$date->{'D_MINUTE'} =		#  d_minute includes sec as fraction
		( $date->{'D_HOUR'} - $date->{'HOUR'} ) * 60.0;
	$date->{'MINUTE'} = int($date->{'D_MINUTE'});	#	integer minute
	$date->{'SECOND'} =		#  float second, int() it if you want
		( $date->{'D_MINUTE'} - $date->{'MINUTE'} ) * 60.0;
	$date->{'WEEKDAY'} = int(($jd + 1) % 7);	#	day of week
	if ( $date->{'YEAR'} == (($date->{'YEAR'} >> 2) << 2) ) {
		$date->{'DAY_OF_YEAR'} =
			int( ( 275 * $date->{'MONTH'} ) / 9)
			- int(($date->{'MONTH'} + 9) / 12)
			+ $date->{'DAY'} - 30;
	} else {
		$date->{'DAY_OF_YEAR'} =
			int( ( 275 * $date->{'MONTH'}) / 9)
			- ((($date->{'MONTH'} + 9) / 12) << 1)
			+ $date->{'DAY'} - 30;
	}
	return( $date->{'YEAR'} );
}	#	end of	 long CalDate( date )

############################################################################
#
#	JulDate - computes the julian decimal date from
#		the gregorian (or Julian) calendar date.
#	for astronomical purposes, The Gregorian calendar reform occurred
#	on 15 Oct. 1582.  This is 05 Oct 1582 by the julian calendar.
#
#	Single argument input: pointer to a hash that is a UTInstant
#		The elements that must be set are:
#		YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
#		(if they have not been set, they appear as zero here)
#
#	Output result will be the setting of the UTInstant elements:
#		J_DATE, D_HOUR, D_MINUTE, WEEKDAY, DAY_OF_YEAR
#		and for convenience, the J_DATE value will be returned.
#
#	Reference: Astronomial formulae for calculators, meeus, p 23
#	from fortran program by F. Espenak - April 1982 Page 276,
#	50 Year canon of solar eclipses: 1986-2035
#

sub JulDate() {
	my ($date) = @_;	#	pointer to the UTInstant
	#	These locals are all integers except for $frac and $gyr
	my ($iy0, $im0, $ia, $ib, $jd, $frac, $gyr);
	#	Thus, to be safer, we will use int() around all calculations
	#		for the integers

#	the fraction of a day

	$frac = ($date->{'HOUR'}/ 24.0)
		+ ($date->{'MINUTE'} / 1440.0)
		+ ($date->{'SECOND'} / 86400.0);

#	convert the cal date to format YYYY.MMDDdd
	$gyr = $date->{'YEAR'}
		+ (0.01 * $date->{'MONTH'})
		+ (0.0001 * $date->{'DAY'})
		+ (0.0001 * $frac) + 1.0e-9;

#	conversion factors
	if ( $date->{'MONTH'} <= 2 ) {
		$iy0 = int($date->{'YEAR'} - 1);
		$im0 = int($date->{'MONTH'} + 12);
	} else {
		$iy0 = int($date->{'YEAR'});
		$im0 = int($date->{'MONTH'});
	}

	$ia = int($iy0 / 100);
	$ib = int(2 - $ia + ($ia >> 2));
#	calculate julian date
	if ( $date->{'YEAR'} <= 0 ) {
		$jd = int( (365.25 * $iy0) - 0.75 )
			+ int( (30.6001 * ($im0 + 1) ) )
			+ int( $date->{'DAY'} + 1720994 );
	} else {
		$jd = int(365.25 * $iy0)
			+ int( 30.6001 * ($im0 + 1) )
			+ int( $date->{'DAY'} + 1720994 );
	}
	if ( $gyr >= 1582.1015 ) {	#	on or after 15 October 1582
		$jd += $ib;
	}
	$date->{'J_DATE'} = $jd + $frac + 0.5;
	$jd = int( $date->{'J_DATE'} + 0.5 );
	$date->{'WEEKDAY'} = int( ($jd + 1) % 7 );
	return( $date->{'J_DATE'} );
}	#	end of	JulDate(

############################################################################
#	FormatCalDate - pass in a pointer to a UTInstant
#	Will format and return a string in standard format:
#	YYYY/MM/DD HH:MM:SS
sub FormatCalDate() {
	my ($t) = @_;	#	pointer to the UTInstant
	return( sprintf "%d/%02d/%02d %02d:%02d:%02d",
		$t->{'YEAR'}, $t->{'MONTH'}, $t->{'DAY'}, $t->{'HOUR'},
		$t->{'MINUTE'}, int($t->{'SECOND'} + 0.5));
}

1;

=head1 NAME

CalDate	-	Calculate calendar date from Julian date

JulDate	-	Calculate Julian date from calendar date

FormatCalDate	-	Return string in ISO 8601 format: YYYY/MM/DD hh:mm:ss
			http://www.cl.cam.ac.uk/~mgk25/iso-time.html

=head1 SYNOPSIS

    use CalDate;

    my %Date;
    $Date{'J_DATE'} = 2452589.105822;
    my $U=\%Date;		# $U is a pointer to %Date
    &CalDate($U);
    printf "%s\n",  &FormatCalDate($U);

    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
    $Date{'YEAR'} = $year + 1900;
    $Date{'MONTH'} = $mon + 1;
    $Date{'DAY'} = $mday;
    $Date{'HOUR'} = $hour;
    $Date{'MINUTE'} = $min;
    $Date{'SECOND'} = $sec;
    &JulDate($U);
    printf "%s = JD: %.6f\n",  &FormatCalDate($U), $Date{'J_DATE'};


=head1 DESCRIPTION

=cut
###########################################################################
#
#	The fundamental structure is a UTInstant
#	Its elements will be the keys in a hash and they are:
#
#	J_DATE	float range (0 to the limit of perl installation)
#		J_DATE is the Julian decimal date
#	YEAR	integer range dependent upon machine implementation of perl
#		-4712 lower limit to upper limit size of integer
#	MONTH	integer range (1..12)
#	DAY	integer range (1..31)
#	HOUR	integer range (0..23)
#	MINUTE	integer range (0..61)
#	SECOND	float range (0..60) use int() if you want to consider it an int
#	D_HOUR	float range (0..23.999...) includes min and second as fraction
#	D_MINUTE	float range (0..60.9999) includes second as fraction
#	WEEKDAY	integer range (0..6)
#	DAY_OF_YEAR	integer range (1..366)
#
