Perl Weekly Challenge 132: Mirror Dates and Hash Join

These are some answers for Week 133 of the Perl Weekly Challenge organized by Mohammad S. Anwar.

Spoiler Alert: This weekly challenge deadline is due in a few days from now (on October 3, 2021 at 23:59). This blog post offers some solutions to this challenge, please don’t read on if you intend to complete the challenge on your own.

Task 1: Mirror Dates

You are given a date (yyyy/mm/dd).

Assuming, the given date is your date of birth. Write a script to find the mirror dates of the given date.

Dave Cross has built cool site that does something similar.

Assuming today is 2021/09/22.

Example 1:

Input: 2021/09/18
Output: 2021/09/14, 2021/09/26

On the date you were born, someone who was your current age, would have been born on 2021/09/14.
Someone born today will be your current age on 2021/09/26.

Example 2:

Input: 1975/10/10
Output: 1929/10/27, 2067/09/05

On the date you were born, someone who was your current age, would have been born on 1929/10/27.
Someone born today will be your current age on 2067/09/05.

Example 3:

Input: 1967/02/14
Output: 1912/07/08, 2076/04/30

On the date you were born, someone who was your current age, would have been born on 1912/07/08.
Someone born today will be your current age on 2076/04/30.

Even though I did the task a few days later, I’ll also assume today is 2021/09/22 in order to be able to compare my results with the examples provided in the task description.

Mirror Dates in Raku

Raku has rich built-in classes and/or roles (types) for dealing with dates. Here we only need to use the Date built-in data type. Date subtraction correctly computes the number of days between two dates, leading to very simple code:

use v6;

my $today =  Date.new("2021-09-22");
for "2021-09-18", "1975-10-10", "1967-02-14" -> $test {
    my $input = Date.new($test);
    my $time-diff = $today - $input;
    say "Mirror dates for $input are: ", 
        $input - $time-diff, " and ", $today + $time-diff;
}

This program displays the following output:

$ raku ./mirror-dates.raku
Mirror dates for 2021-09-18 are: 2021-09-14 and 2021-09-26
Mirror dates for 1975-10-10 are: 1929-10-27 and 2067-09-05
Mirror dates for 1967-02-14 are: 1912-07-08 and 2076-04-30

Mirror Dates in Perl

As usual, I don’t want, for a programming challenge, to use external modules. So I’ll be using only the core Time::Local module. This leads to some complexities compare to the Raku version. In particular, the program has a compute_time function to transform a date string into a standard Posix time stamp, and a convert_date function to perform the reciprocal conversion (time stamp to string). Besides that, the Perl program works the same way as the Raku program.

#!/usr/bin/perl
use strict;
use warnings;
use feature qw/say/;
use Time::Local;

sub compute_time {
    my ($y, $mo, $d) = split /-/, shift;
    my ($h, $mi, $s) = (0, 0, 0);
    $mo -= 1; # months are 0-indexed
    # $y -= 1900; # timegm works better with real year
    # say $y;
    return timegm($h, $mi, $s, $d, $mo, $y);
} 
sub convert_date {
    my $time_stamp = shift;
    my ($d, $m, $y) = (gmtime $time_stamp)[3, 4, 5];
    $d = sprintf "%02d", $d;
    $m = sprintf "%02d", $m+1;
    $y += 1900;
    return "$y-$m-$d";
}

my $today =  compute_time("2021-09-22");  # timestamp for the date arbitrarily chosen as "today"
for my $test ("2021-09-18", "1975-10-10", "1967-02-14") {
    my $input = compute_time ($test);
    my $time_diff = $today - $input;
    my @output = ($input - $time_diff, $today + $time_diff);
    say "Mirror dates for $test are: ", convert_date($output[0]), " and ", convert_date($output[1]);
}

This program displays the following output:

$ perl ./mirror-dates.pl
Mirror dates for 2021-09-18 are: 2021-09-14 and 2021-09-26
Mirror dates for 1975-10-10 are: 1929-10-27 and 2067-09-05
Mirror dates for 1967-02-14 are: 1912-07-08 and 2076-04-30

Task2: Hash Join

Write a script to implement Hash Join algorithm as suggested by https://en.wikipedia.org/wiki/Hash_join#Classic_hash_join.

  1. For each tuple r in the build input R 1.1 Add r to the in-memory hash table 1.2 If the size of the hash table equals the maximum in-memory size: 1.2.1 Scan the probe input S, and add matching join tuples to the output relation 1.2.2 Reset the hash table, and continue scanning the build input R
  2. Do a final scan of the probe input S and add the resulting join tuples to the output relation

Example:

Input:

    @player_ages = (
        [20, "Alex"  ],
        [28, "Joe"   ],
        [38, "Mike"  ],
        [18, "Alex"  ],
        [25, "David" ],
        [18, "Simon" ],
    );

    @player_names = (
        ["Alex", "Stewart"],
        ["Joe",  "Root"   ],
        ["Mike", "Gatting"],
        ["Joe",  "Blog"   ],
        ["Alex", "Jones"  ],
        ["Simon","Duane"  ],
    );

Output:

    Based on index = 1 of @players_age and index = 0 of @players_name.

    20, "Alex",  "Stewart"
    20, "Alex",  "Jones"
    18, "Alex",  "Stewart"
    18, "Alex",  "Jones"
    28, "Joe",   "Root"
    28, "Joe",   "Blog"
    38, "Mike",  "Gatting"
    18, "Simon", "Duane"

I don’t quite understand the relationship between hash join algorithm as described in the Wikipedia article and the example provided. So, I’ll simply try to replicate the behavior described in the example.

Hash Join in Raku

The input is the two arrays of arrays (AoA), @player_ages and @player_names, provided in the task description example. We load @player_names into the %names hash of arrays (HoA). Then we loop over the other input array, @player_ages and look up the hash to complete the lines, which we eventually print out.

use v6;

my @player_ages = 
    [20, "Alex"  ],
    [28, "Joe"   ],
    [38, "Mike"  ],
    [18, "Alex"  ],
    [25, "David" ],
    [18, "Simon" ];

my @player_names = 
    ["Alex", "Stewart"],
    ["Joe",  "Root"   ],
    ["Mike", "Gatting"],
    ["Joe",  "Blog"   ],
    ["Alex", "Jones"  ],
    ["Simon","Duane"  ];

my %names;
for @player_names -> $name {
    push %names{$name[0]}, $name[1];
}
for @player_ages -> $pl_age {
    my ($age, $first_name) = $pl_age;
    next unless %names{$first_name}:exists;
    # say "$age  $first_name";
    for %names{$first_name}[] -> $name {
        say "$age $first_name $name";
    }
}

This script displays the following output:

raku ./hash-join.raku
20 Alex Stewart
20 Alex Jones
28 Joe Root
28 Joe Blog
38 Mike Gatting
18 Alex Stewart
18 Alex Jones
18 Simon Duane

Hash Join in Perl

The Perl version is essentially a port to Perl of the Raku version above. Please refer to the explanations above if needed.

#!/usr/bin/perl
use strict;
use warnings;
use feature qw/say/;
use Time::Local;

my @player_names = (
    ["Alex", "Stewart"],
    ["Joe",  "Root"   ],
    ["Mike", "Gatting"],
    ["Joe",  "Blog"   ],
    ["Alex", "Jones"  ],
    ["Simon","Duane"  ],
    );

my %names;
for my $name (@player_names) {
    push @{$names{$name->[0]}}, $name->[1];
}
for my $pl_age (@player_ages) {
    my ($age, $first_name) = @$pl_age;
    next unless exists $names{$first_name};
    for my $name (@{$names{$first_name}}) {
        say "$age $first_name $name";
    }
}

This program displays the following output:

$ perl  ./hash-join.pl
20 Alex Stewart
20 Alex Jones
28 Joe Root
28 Joe Blog
38 Mike Gatting
18 Alex Stewart
18 Alex Jones
18 Simon Duane

Wrapping up

The next week Perl Weekly Challenge will start soon. If you want to participate in this challenge, please check https://perlweeklychallenge.org/ and make sure you answer the challenge before 23:59 BST (British summer time) on October 10, 2021. And, please, also spread the word about the Perl Weekly Challenge if you can.

Leave a comment

About laurent_r

user-pic I am the author of the "Think Perl 6" book (O'Reilly, 2017) and I blog about the Perl 5 and Raku programming languages.