use strict; use warnings; use DateTime; use DateTime::Format::ISO8601; use DateTime::TimeZone; use Data::Dumper; use Try::Tiny; use Astro::MoonPhase; ###### # Calculate the moon phase of an event in time. # Required is the date (time defaults to 00:00:01) of the event # The moon-phase calculator does not need a location # since moon phase is the same across the planet (more-or-less) # with the usual antipodean side-effects # However, by specifying a timezone or even coordinates of the # event conversion of event's time to UTC is more accurate # (else UTC time is assumed) # author: bliako # date: 2021-10-07 ###### my $debug = 0; my @events = ( # { # # example event specification # "name" => "a name for reference, required", # "date" => "date of the event in YYYY-MM-DD", # "time" => "optional time as hh:mm:ss, default is 00:00:01", # #specify optional location using one of these for calculating # #epoch which uses UTC time, # #otherwise, UTC timezone will be assumed for event's time: # "timezone" => "standard timezone name, e.g. America/Havana", # "location" => "cuba", # "location" => {lon=>-81.1376, lat=>22.17927}, # }, { "name" => "Normandy Landing", "date" => "1944-06-06", "time" => "05:00:00", #"timezone" => "Europe/Paris", "location" => {lat=>49.180000, lon=>-0.370000} }, { "name" => "US Invasion of Cuba, Bay of Pigs", "date" => "1961-04-15", "time" => "05:00:00", "timezone" => "America/Havana", #"location" => {lon=>-81.1376, lat=>22.17927} }, { "name" => "Invasion of Libya", "date" => "2011-03-19", "time" => "05:00:00", "timezone" => "Africa/Tripoli", }, { "name" => "Invasion of Iraq", "date" => "2003-03-19", "time" => "05:00:00", "timezone" => "Asia/Baghdad", }, ); for my $event (@events){ my $epoch = parse_event($event); print event2str($event) . "\n"; my ( $MoonPhase, $MoonIllum, $MoonAge, $MoonDist, $MoonAng, $SunDist, $SunAng ) = phase($epoch); print "Moon age: $MoonAge days\n"; print "Moon phase: ".sprintf("%.1f", 100.0*$MoonPhase)." % of cycle (birth-to-death)\n"; print "Moon's illuminated fraction: ",sprintf("%.1f", 100.0*$MoonIllum)." % of full disc\n"; print "important moon phases around the event:\n"; my @phases = phasehunt($epoch); print " New Moon = ", scalar(localtime($phases[0])), "\n"; print " First quarter = ", scalar(localtime($phases[1])), "\n"; print " Full moon = ", scalar(localtime($phases[2])), "\n"; print " Last quarter = ", scalar(localtime($phases[3])), "\n"; print " New Moon = ", scalar(localtime($phases[4])), "\n"; print "end event.\n\n" } sub event2str { my $event = shift; if( ! exists $event->{_is_parsed} ){ warn "event has not been parsed, just dumping it..."; print Dump($event); } my $str = $event->{name} . " on ".$event->{datetime} . " (".$event->{datetime}->epoch." seconds unix-epoch)" . " timezone: ".$event->{datetime}->time_zone->name ; if( exists $event->{location} ){ if( ref($event->{location}) eq 'HASH' ){ $str .= " (lat: ".$event->{location}->{lat}.", lon: ".$event->{location}->{lon}.")" } else { $str .= "(".$event->{location}.")" } } return $str } sub parse_event { my $event = shift; if( ! exists $event->{date} ){ die "date field is missing from event." } my $datestr = $event->{date}; die "event does not have a 'name' field, please specify one, anything really." unless exists $event->{name}; my $timestr = "00:00:01"; if( exists $event->{time} ){ $timestr = $event->{time}; print "event2epoch(): setting time to '$timestr' ...\n" if $debug > 0; die "time '$timestr' is not valid, it must be in the form 'hh:mm:ss'." unless $timestr =~ /^\d{2}:\d{2}:\d{2}$/; } else { $event->{time} = $timestr } my $isostr = $datestr . 'T' . $timestr; my $dt = DateTime::Format::ISO8601->parse_datetime($isostr); die "failed to parse date '$isostr', check date and time fields." unless defined $dt; $event->{datetime} = $dt; my $tzstr = 'UTC'; if( exists $event->{timezone} ){ $tzstr = $event->{timezone}; print "event2epoch(): found a timezone via 'timezone' field as '$tzstr' (that does not mean it is valid) ...\n" if $debug > 0; } elsif( exists $event->{location} ){ my $loc = $event->{location}; if( (ref($loc) eq '') && ($loc =~ /^[a-zA-Z]$/) ){ # we have a location string my @alltzs = DateTime::TimeZone->all_names; my $tzstr; for (@alltzs){ if( $_ =~ /$loc/i ){ $tzstr = $_; last } } die "event's location can not be converted to a timezone, consider specifying the 'timezone' directly or setting 'location' coordinates with: \[lat,lon\]." unless $tzstr; print "event2epoch(): setting timezone via 'location' name to '$timestr' ...\n" if $debug > 0; } elsif( (ref($loc) eq 'HASH') && (exists $loc->{lat}) && (exists $loc->{lon}) ){ # we have a [lat,lon] array for location require Geo::Location::TimeZone; my $gltzobj = Geo::Location::TimeZone->new(); $tzstr = $gltzobj->lookup(lat => $loc->{lat}, lon => $loc->{lon}); if( ! $tzstr ){ die "timezone lookup from location coordinates lat:".$loc->{lat}.", lon:".$loc->{lon}." has failed." } print "event2epoch(): setting timezone via 'location' coordinates lat:".$loc->{lat}.", lon:".$loc->{lon}." ...\n" if $debug > 0 } } if( $tzstr ){ print "event2epoch(): deduced timezone to '$tzstr' and setting it ...\n" if $debug > 0; try { $dt->set_time_zone($tzstr) } catch { die "$_\n failed to set the timezone '$tzstr', is it valid?" } } $event->{_is_parsed} = 1; $event->{epoch} = $dt->epoch; return $event->{epoch} }