While I was watching the moon at a place away from civilisation I wondered what phase was the moon on certain historical events. I am not into astrology. I was curious to see how military planners make use of the moon and its effect on illumination and the tides. Invasions primarily...
Thankfully Perl and the wonderful playground of CPAN provide all the tools one needs for an application which calculates the moon phase given date and, optionally, time and timezone or location. Thank you Astro::MoonPhase and DateTime and Geo::Location::TimeZone. All is needed is the date. Time defaults to the beginning of the day (00:00:01) and the timezone defaults to UTC. If the event's time is critical then supply it along with a timezone to make the conversion to unix-epoch-seconds (assumes UTC) accurate. In starting this I was not aware that Moon phase for a specific time is more-or-less the same for anywhere on the surface of our planet, irrespective of standpoint. With the caveat that our antipodean fellows will see it, well ... , antipodeanly.
It looks that some military invasions took advantage of the full moon (invasion of Libya/2011, invasion of Iraq/2003 which, btw, both happened on the same day 8 years apart) while others planned for a "dark" moon (Invasion of Bay of Pigs, Cuba/1961). Normandy Landing/1944 (D-Day) planners chose a full moon (in the output: illuminated fraction: 99.4 % of full disc and also Moon age: 14.0637595011504 days which is just about half the moon cycle, i.e. full-moon) because the way it affected the tide, although the illumination would have been unwanted.
The driver script follows. The input is provided in-script by an event hash which specifies a name and date, optional are time, timezone, location coordinates. Enjoy:
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 cycl
+e (birth-to-death)\n";
print "Moon's illuminated fraction: ",sprintf("%.1f", 100.0*$MoonI
+llum)." % 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: ".$eve
+nt->{location}->{lon}.")"
} else { $str .= "(".$event->{location}.")" }
}
return $str
}
sub parse_event {
my $event = shift;
if( ! exists $event->{date} ){ die "date field is missing from eve
+nt." }
my $datestr = $event->{date};
die "event does not have a 'name' field, please specify one, anyth
+ing 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' coo
+rdinates 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}) && (exi
+sts $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 coordin
+ates lat:".$loc->{lat}.", lon:".$loc->{lon}." has failed." }
print "event2epoch(): setting timezone via 'location' coor
+dinates 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}
}