I’ve been doing the Perl Weekly
Challenges. Last week's was about
counting weekdays and working out total daylight.
Write a script to calculate the total number of weekdays (Mon-Fri)
in each month of the year 2019.
There are obviously lots of ways to do this, so I thought I'd indulge
myself. In Perl5, I just iterated through the year, working out for
each day whether it was a weekday and if so adding it to that month's
total. It's also a good excuse to use a C-style for
loop rather than
the foreach
that I find much more common in my day-to-day Perl.
use Time::Local;
use POSIX qw(strftime);
my $y=2019;
my $s=timegm(0,0,12,1,0,$y);
my $e=timegm(0,0,12,1,0,$y+1);
my @o;
my @m;
Loop from the first day of the year, terminating just before the first
day of next year.
for (my $t=$s;$t<$e;$t+=86400) {
my @d=gmtime($t);
The only bits we care about are the month (element 4) and the day of
the week (element 6). Yes, this system has Sunday as day 0. Work out
the short name of the month while we're at it, if we don't already
have one.
if ($d[6]>0 && $d[6]<6) {
$o[$d[4]]++;
unless (defined $m[$d[4]]) {
$m[$d[4]]=strftime('%b',@d);
}
}
}
foreach my $i (0..$#o) {
print "$m[$i]: $o[$i] days\n";
}
For Perl6, rather than just copy the Perl5 program and bash it until
it worked, I took a completely different approach. I do like Perl6's
Date object (particularly the fact that it exists separately from
DateTime, so that when I only care about day-level resolution I don't
need to muck about with faking a time of day as I did above).
my $y=2019;
for (1..12) -> $m {
It would be really handy just to be able to say "the first day of
month 13" and then step back one to get the last day of month 12, but
no, that's an "error" apparently. Pathetic humans and their calendars.
(Actually, I love calendrical calculations, and I recommend the book
of that name by Reingold and Dershowitz to anyone who does too. Even
if it does use LISP for its code examples. Linked at the bottom of
this post.)
my $mm=$m+1;
my $yy=$y;
if ($mm>12) {
$mm-=12;
$yy++;
}
my $d=Date.new($yy,$mm,1).earlier(:1day);
So now we have $d set to the last day of the month, and can work back
from it, counting down until we hit the 28th. (The total number of
weekdays in days 1-28 of any month is always 20, so we don't bother to
calculate those individually.)
my $wd=20;
while ($d.day>28) {
if ($d.day-of-week < 6) {
$wd++;
}
$d=$d.earlier(:1day);
}
There doesn't appear to be any direct access to strftime in Perl6, so
we just dump month numbers instead of names.
say "$m: $wd days";
}
Another approach would be to work out the day number and day of week
of the last day of the month, then calculate from that how many
weekdays must fall in the span (29 .. last-day)
.
Write a script to find out the DayLight gain/loss in the month of
December 2019 as compared to November 2019 in the city of London.
You can find out sunrise and sunset data for November
2019
and December
2019
for London.
I can, but I don't have to, because I've already written my handy
astronomical
calendar and as a
result dug into the fiddliness that is the Earth-Centered Inertial
coordinate model. Astro::Coord::ECI
is mostly intended for tracking
artificial satellites, but it's the only module I've found that will
let me readily calculate moon rise and set times (not that I need them
for this example).
Latitude, longitude and altitude of London are taken from the
Wikipedia page.
use Astro::Coord::ECI;
use Astro::Coord::ECI::Sun;
use Astro::Coord::ECI::Utils qw{deg2rad};
use Time::Local;
my $year=2019;
my ($lat,$lon,$alt)=(deg2rad(51.507222),deg2rad(-0.1275),11);
my @dtime;
my $sun=Astro::Coord::ECI::Sun->new;
foreach my $month (11,12) {
my $ts=timelocal(0,0,0,1,$month-1,$year);
Once more, it would be really handy just to be able to say "the first
day of month 13".
my $te;
{
my $mm=$month+1;
my $yy=$year;
if ($mm>12) {
$mm-=12;
$yy++;
}
$te=timelocal(0,0,0,1,$mm-1,$yy);
}
my $sta=Astro::Coord::ECI->
universal($ts)->
geodetic($lat,$lon,$alt);
my $ds=0;
my $ls=0;
$ls is the latest sunrise time. Note that we assume midnight is always
dark; if this calculation were being done for somewhere significantly
east or west of London, there might be partial daylight spanning
midnight at the start or end of the month, which would need additional
code to check for those cases; but the specification was for London,
and one of the things I'm trying to do here is get the code working
quickly.
Then it's just a matter of iterating through the almanac that the
module will generate for us; "horizon" events are sunrise/sunset. Add
up the total day lengths for each month, then finally compare the two
months.
foreach my $event ($sun->almanac($sta,$ts,$te)) {
if ($event->[1] eq 'horizon') {
my $t=localtime($event->[0]);
if ($event->[2] == 1) {
$ls=$event->[0];
} else {
if ($ls) {
$ds+=$event->[0]-$ls;
}
}
}
}
push @dtime,$ds;
}
print 'delta ',$dtime[1]-$dtime[0]," s\n";
For Perl6 the ECI module isn't available, so I did use those pages
from timeanddate.com; for convenience, I saved them to HTML files and
found a parser that converts HTML into XML documents.
use Gumbo;
my @dtime;
for ('2019-11-london.html','2019-12-london.html') -> $file {
my $dlt=0;
my $fh=open :r,$file;
my $text='';
for $fh.lines {
$text ~= $_;
}
close $fh;
my $xml=parse-html($text);
There's only one table in the document, so jump straight to that.
my $tab=$xml.root.elements(:TAG<table>, :RECURSE)[0];
Then iterate through each row, picking the ones with twelve elements
since those are the ones with data in them. Element 2 is the day
length (hours:minutes:seconds), so grab that and parse it. This is
obviously not at all robust against changes in the site design.
for $tab.elements(:TAG<tr>, :RECURSE) -> $tr {
my @td=$tr.elements(:TAG<td>, :RECURSE);
if (@td.elems==12) {
my $dl=@td[2].nodes[0].text;
$dl ~~ /(\d+) ':' (\d+) ':' (\d+)/;
$dlt+=$0*3600+$1*60+$2;
}
}
push @dtime,$dlt;
}
say 'delta ',@dtime[1]-@dtime[0],' s';
Comments on this post are now closed. If you have particular grounds for adding a late comment, comment on a more recent post quoting the URL of this one.