RogerBW's Blog

Perl Weekly Challenge 37: weekdays and day lengths 11 December 2019

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';

[Buy Calendrical Calculations. at Amazon] and help support the blog.

Add A Comment

Your Name
Your Email
Your Comment

Your submission will be ignored if any field is left blank, but your email address will not be displayed. Comments will be processed through markdown.

Search
Archive
Tags 1920s 1930s 1940s 1950s 1960s 1970s 1980s 1990s 2000s 2010s 3d printing action aeronautics aikakirja anecdote animation anime army astronomy audio audio tech base commerce battletech beer boardgaming book of the week bookmonth chain of command children chronicle church of no redeeming virtues cold war comedy computing contemporary cornish smuggler cosmic encounter coup cycling dead of winter doctor who documentary drama driving drone ecchi economics espionage essen 2015 essen 2016 essen 2017 essen 2018 essen 2019 existential risk falklands war fandom fantasy film firefly first world war flash point food garmin drive gazebo geodata gin gurps gurps 101 harpoon historical history horror hugo 2014 hugo 2015 hugo 2016 hugo 2017 hugo 2018 hugo 2019 hugo-nebula reread humour in brief avoid instrumented life kickstarter learn to play leaving earth linux mecha men with beards museum mystery naval non-fiction one for the brow opera perl perl weekly challenge photography podcast politics powers prediction privacy project woolsack pyracantha quantum rail ranting raspberry pi reading reading boardgames social real life real life ranting restaurant reviews romance rpg a day rpgs science fiction scythe second world war security shipwreck simutrans smartphone south atlantic war squaddies stationery steampunk stuarts suburbia superheroes suspense television the resistance thirsty meeples thriller tin soldier torg toys trailers travel type 26 type 31 type 45 vietnam war war wargaming weather wives and sweethearts writing about writing x-wing young adult
Special All book reviews, All film reviews
Produced by aikakirja v0.1