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. ["As an Amazon Associate, I earn from qualifying purchases."]

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.

Search
Archive
Tags 1920s 1930s 1940s 1950s 1960s 1970s 1980s 1990s 2000s 2010s 2300ad 3d printing action advent of code aeronautics aikakirja anecdote animation anime army astronomy audio audio tech base commerce battletech bayern beer boardgaming book of the week bookmonth chain of command children chris chronicle church of no redeeming virtues cold war comedy computing contemporary cornish smuggler cosmic encounter coup covid-19 crime crystal cthulhu eternal cycling dead of winter doctor who documentary drama driving drone ecchi economics en garde espionage essen 2015 essen 2016 essen 2017 essen 2018 essen 2019 essen 2022 essen 2023 essen 2024 existential risk falklands war fandom fanfic fantasy feminism film firefly first world war flash point flight simulation food garmin drive gazebo genesys geocaching geodata gin gkp gurps gurps 101 gus harpoon historical history horror hugo 2014 hugo 2015 hugo 2016 hugo 2017 hugo 2018 hugo 2019 hugo 2020 hugo 2021 hugo 2022 hugo 2023 hugo 2024 hugo-nebula reread in brief avoid instrumented life javascript julian simpson julie enfield kickstarter kotlin learn to play leaving earth linux liquor lovecraftiana lua mecha men with beards mpd museum music mystery naval noir non-fiction one for the brow opera parody paul temple perl perl weekly challenge photography podcast politics postscript powers prediction privacy project woolsack pyracantha python quantum rail raku ranting raspberry pi reading reading boardgames social real life restaurant reviews romance rpg a day rpgs ruby rust scala science fiction scythe second world war security shipwreck simutrans smartphone south atlantic war squaddies stationery steampunk stuarts suburbia superheroes suspense television the resistance the weekly challenge 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