Perl Weekly Challenge 132: Mirror Dates

by Abigail

Challenge

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 a 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.

Discussion

The first thing we need to ask when using dates, in particular dates in the past is Which calendar?. The Gregorian Calendar seems an obvious choice, but only if one is naïeve. The Gregorian calendar starts on different dates, depending on the country. While the calendar was introduced in 1582 in some countries, in other countries, it was introduced less than 100 years ago.

To make the challenge simpler for us, we will be assuming the Proleptic Gregorian calendar. This basically means we are assuming the Gregorian calendar has always existed. We will also be assuming the year 0 existed.

Solution

We could use one of the gazillion date modules from CPAN. However, we will not. Instead, of the two given dates ("today", and the input date), we will calculate the Julian Day Number. Given those, we can easily calculate the Julian Day Numbers of the target dates, which we then convert back to (proleptic) Gregorian dates.

We will use the formulas on Wikipedia to convert dates to and from Julian Day Numbers.

It should be noted that division needs to be done as integer division, with results rounded towards 0. That is, 5 / 2 == 2, and -5 / 2 == -2.

For the calculations, we fix "today" as 2021/09/22 (using a fixed date means our tests are deterministic — and if we fix a date, we may as well use the date in the given examples.)

We also be reading dates from standard input, one date per line, and for each input date, we output a line with the two target dates.

Perl

First, we create a sub which takes a year, month, and date and returns a Julian Day Number:

sub g2j ($Y, $M, $D) {
    use integer;
    (1461 * ($Y + 4800 + ($M - 14) / 12)) / 4  +
    (367 * ($M - 2 - 12 * (($M - 14) / 12))) / 12 -
    (3 * (($Y + 4900 + ($M - 14) / 12) / 100)) / 4 + $D - 32075
}

Note the directive use integer;, which causes Perl to do division the way C does division between integers.

We also need a sub which takes a Julian Day Number, and returns the date:

sub j2g ($J) {
    use integer;
    my $e = 4 * ($J + 1401 + (((4 * $J + 274277) / 146097) * 3) / 4 - 38) + 3;
    my $D =  ((5 * (($e % 1461) / 4) + 2) % 153) / 5 + 1;
    my $M = (((5 * (($e % 1461) / 4) + 2) / 153 + 2) % 12) + 1;
    my $Y = ($e / 1461) - 4716 + (12 + 2 - $M) / 12;
    ($Y, $M, $D)
}

Now we calculate todays Julian Day Number:

my @TODAY = (2021, 9, 22);
my $julian_today = g2j @TODAY;

Read a date (from $_), and calculate its Julian Day Number:

my ($Y, $M, $D) = /[0-9]+/g;
my $julian_then = g2j $Y, $M, $D;

The target Julian Day Numbers are now 2 * $julian_then - $julian_today and 2 * $julian_today - $julian_then. Which means we can output the result as:

printf "%04d/%02d/%02d, %04d/%02d/%02d\n",
         j2g (2 * $julian_then  - $julian_today),
         j2g (2 * $julian_today - $julian_then);

Find the full program on GitHub.

AWK

The AWK solution is very similar to the Perl solution, but there are a few differences. First difference is that AWK doesn't do integer division. But it does have a int function which rounds towards 0. This makes the g2j function as follows:

function g2j (Y, M, D) {
    return (int ((1461 * (Y +    4800 + int ((M - 14) / 12))) /   4) +        \
            int ((367 * (M - 2 -   12 * int ((M - 14) / 12))) /  12) -        \
            int ((3 * int (((Y + 4900 + int ((M - 14) / 12))  / 100))) / 4) + \
            D - 32075)
}

Second difference is that AWK cannot return arrays or list from functions. So, we will return a formatted date from j2g:

function j2g (J) {
    e  = 4 * (J + 1401 + int (int ((4 * J + 274277) / 146097) * 3 / 4) - 38) + 3
    D =   int (((5 * (int ((e % 1461) / 4)) + 2) % 153) / 5) + 1
    M = ((int  ((5 * (int ((e % 1461) / 4)) + 2) / 153) + 2) % 12) + 1
    Y = int (e / 1461) - 4716 + int ((12 + 2 - M) / 12)
    return sprintf ("%04d/%02d/%02d", Y, M, D)
}

To set up things, we will set the FS variable to /; that way, AWK will split the input for us, giving use the input year, month and date in the variables $1, $2, and $3:

BEGIN {
    FS = "/"
    TODAY_Y = 2021
    TODAY_M =    9
    TODAY_D =   22

    julian_today = g2j(TODAY_Y, TODAY_M, TODAY_D)
}

The main loop is now simple:

{
    julian_then = g2j($1, $2, $3)
    print j2g(2 * julian_then  - julian_today) ", " \
          j2g(2 * julian_today - julian_then)
}

Find the full program on GitHub.

Bash

Bash does do integer division (it doesn't deal at all with floating point numbers), but to doesn't return values from functions. So we have to use global variables to return values. It also uses numbered variables as the function parameters. This leads to:

function g2j () {
    local Y=$1
    local M=$2
    local D=$3
    J=$(( ((1461 * (Y + 4800  +  (M - 14) / 12))  /   4   +
           (367 * (M - 2 - 12 * ((M - 14) / 12))) /  12   -
           (3 * ((Y + 4900    +  (M - 14) / 12)   / 100)) / 4 + D - 32075) ))
}

and

function j2g () {
    local J=$1
    local e=$(( 4 * (J + 1401 +
             (((4 * J + 274277) / 146097) * 3) / 4 - 38) + 3 ))
    D=$((  ((5 * ((e % 1461) / 4) + 2) % 153) / 5 + 1 ))
    M=$(( (((5 * ((e % 1461) / 4) + 2) / 153 + 2) % 12) + 1 ))
    Y=$((         (e / 1461) - 4716 + (12 + 2 - M) / 12 ))
}

Setting up the Julian Day Number for today shows how we use the output values. Note also that we don't use commas to separate arguments.

g2j 2021 9 22; julian_today=$J

We can use the same trick as in AWK to have the input automatically split; the variable IFS plays the role of FS in AWK. But there is a catch. An input like 08 or 09 is interpreted as an (illegal) octal number when used in arithmetic. So we have to strip off any leading 0s. We do this like this: ${d/#0/}, which takes the value in $d, and replaces the leading 0 with an empty string.

This results in the following main loop:

IFS="/"
while read y m d
do   g2j ${y/#0/} ${m/#0/} ${d/#0/}; julian_then=$J
     j2g $(( 2 * julian_then - julian_today ))
     printf "%04d/%02d/%02d, " $Y $M $D
     j2g $(( 2 * julian_today - julian_then ))
     printf "%04d/%02d/%02d\n" $Y $M $D
done

Find the full program on GitHub.

C

The g2j function in C is very similar to the one in Perl. Division in C between integer types is integer division:

typedef unsigned short date_type;

long g2j (date_type Y, date_type M, date_type D) {
    return ((1461 * (Y + 4800  +  (M - 14) / 12))  /   4   +
            (367 * (M - 2 - 12 * ((M - 14) / 12))) /  12   -
            (3 * ((Y + 4900    +  (M - 14) / 12)   / 100)) / 4 + D - 32075);
}

For the j2g function, we take an additional parameter: an appropriate sized array in which we put the date component parts:

# define idx_Y 0
# define idx_M 1
# define idx_D 2

void j2g (long J, date_type * date) {
    long e = 4 * (J + 1401 + (((4 * J + 274277) / 146097) * 3) / 4 - 38) + 3;
    date [idx_D] =  ((5 * ((e % 1461) / 4) + 2) % 153) / 5 + 1;
    date [idx_M] = (((5 * ((e % 1461) / 4) + 2) / 153 + 2) % 12) + 1;
    date [idx_Y] =         (e / 1461) - 4716 + (12 + 2 - date [idx_M]) / 12;
}

In the main program, we have to allocate memory for the date components of the two dates, and we calculate the Julian Day Number of "today":

unsigned short TODAY [] = {2021, 9, 22};

int main (void) {
    date_type Y, M, D;
    date_type * date_early;
    date_type * date_late;

    if ((date_early = (date_type *) malloc (3 * sizeof (date_type))) == NULL) {
        perror ("Malloc failed");
        exit (1);
    }
    if ((date_late  = (date_type *) malloc (3 * sizeof (date_type))) == NULL) {
        perror ("Malloc failed");
        exit (1);
    }

    long julian_today = g2j (TODAY [idx_Y], TODAY [idx_M], TODAY [idx_D]);

We use scanf to parse the input. The %hu format indicates we are reading an unsigned short. This gives use the following main loop:

while (scanf ("%hu/%hu/%hu", &Y, &M, &D) == 3) {
    long julian_then = g2j (Y, M, D);
    j2g (2 * julian_then  - julian_today, date_early);
    j2g (2 * julian_today - julian_then,  date_late);
    printf ("%04d/%02d/%02d, %04d/%02d/%02d\n",  
             date_early [idx_Y], date_early [idx_M], date_early [idx_D],
             date_late  [idx_Y], date_late  [idx_M], date_late  [idx_D]);
}

At the end, we free the allocated memory:

free (date_early);
free (date_late);

Find the full program on GitHub.

Lua

Nieuwer version of Lua (nieuwer than the version I run), has // to do integer division. Lua also doesn't have an int function, so we roll our own:

function int (x)
    if x > 0 then
        return math . floor (x)
    else
        return math . ceil (x)
    end
end

math . floor rounds toward \(- \infty\), and math . ceil rounds towards \(\infty\). Since we want to round towards 0, we check whether the value we want to round is positive or not, and use the appropriate rounding function.

This leads to

function g2j (Y, M, D)
    return (int ((1461 * (Y +    4800 + int ((M - 14) / 12))) /   4) +
            int ((367 * (M - 2 -   12 * int ((M - 14) / 12))) /  12) -
            int ((3 * int (((Y + 4900 + int ((M - 14) / 12))  / 100))) / 4) +
            D - 32075)
end

and

function j2g (J)
    local e  = 4 * (J + 1401 +
                    int (int ((4 * J + 274277) / 146097) * 3 / 4) - 38) + 3
    local D =   int (((5 * (int ((e % 1461) / 4)) + 2) % 153) / 5) + 1
    local M = ((int  ((5 * (int ((e % 1461) / 4)) + 2) / 153) + 2) % 12) + 1
    local Y = int (e / 1461) - 4716 + int ((12 + 2 - M) / 12)
    return Y, M, D
end

The rest of the program looks like:

local julian_today  = g2j (2021, 9, 22)
local output_format = "%04d/%02d/%02d, %04d/%02d/%02d\n"

for line in io . lines () do
    local _, _, Y, M, D = line : find ("([0-9]+)/([0-9]+)/([0-9]+)")
    local julian_then = g2j (Y, M, D)
    local Y1, M1, D1 = j2g (2 * julian_then  - julian_today)
    local Y2, M2, D2 = j2g (2 * julian_today - julian_then)

    io . write (output_format : format (Y1, M1, D1, Y2, M2, D2))
end

Find the full program on GitHub.

Node.js

In Node.js, we don't have integer division either, nor does it have and int function. So we use the same trick as we used in the Lua solution (which we don't copy here).

The tricky thing about the Node.js solution is the lack of a usable printf functionality. So, we have to create our own.

First, a method to pre-pad numbers with 0s till the required length:

function pad (num, l) {
    let out = num
    while (out . length < l) {
        out = "0" + out
    }
    return (out)
}

Then we can create pretty print function, which takes two dates (as arrays), and formats them:

function pp (d1, d2) {
    let e1 = d1 . map (x => x . toString ())
    let e2 = d2 . map (x => x . toString ())
    console . log ("%s/%s/%s, %s/%s/%s",
                       pad (e1 [0], 4), pad (e1 [1], 2), pad (e1 [2], 2),
                       pad (e2 [0], 4), pad (e2 [1], 2), pad (e2 [2], 2))
}

This leads to the following main program:

let julian_today = g2j (2021, 9, 22)

  require ('readline')
. createInterface ({input: process . stdin})
. on              ('line', line => {
    let [Y, M, D] = line . split ('/') . map (x => +x)
    let julian_then = g2j (Y, M, D)
    pp (j2g (2 * julian_then  - julian_today),
        j2g (2 * julian_today - julian_then))
})

Find the full program on GitHub.

Python

Python has // to do integer division, but this rounds towards \(-\infty\), which makes it useless for our purposes. Luckily, its int method rounds towards 0, so we can use that, giving us a similar g2j and j2g as we have seen before.

This gives us the following main program:

for line in fileinput . input ():
    Y, M, D     = map (lambda x: int (x), line . strip () . split ("/"))
    julian_then = g2j (Y, M, D)
    Y1, M1, D1  = j2g (2 * julian_then  - julian_today)
    Y2, M2, D2  = j2g (2 * julian_today - julian_then)
    print ('{:04d}/{:02d}/{:02d}, {:04d}/{:02d}/{:02d}' .
              format (Y1, M1, D1, Y2, M2, D2))

Find the full program on GitHub.

Ruby

Ruby uses integer division when divising integers, but it rounds towards \(-\infty\). We therefore create a div function which does integer division with rounding towards 0:

def div (x, y)
    return (x . to_f / y) . to_i
end

In this method, we convert one of the arguments to a floating point number (using to_f), then, after division, we use to_i to force the result to be an integer. to_i rounds towards 0.

This gives us the following g2j:

def g2j (y, m, d)
    return (div(1461 * (y +    4800 + div(m - 14, 12)),   4) +
            div( 367 * (m - 2 -   12 * div(m - 14, 12)),  12) -
            div(   3 * div(y +  4900 + div(m - 14, 12), 100), 4) + d - 32075)
end

and j2g:

def j2g (j)
    e = 4 * (j + 1401 + div(div(4 * j + 274277, 146097) * 3, 4) - 38) + 3;
    d =   div((5 * div(e % 1461, 4) + 2) % 153, 5) + 1;
    m = ((div( 5 * div(e % 1461, 4) + 2, 153) + 2) % 12) + 1;
    y =            div(e,  1461) - 4716 + div(12 + 2 - m, 12);
    return y, m, d
end

The main program then becomes:

julian_today = g2j 2021, 9, 22

ARGF . each_line do
    | line |
    y, m, d = line . strip . split("/") . map {|x| x . to_i}
    julian_then = g2j y, m, d
    puts "%04d/%02d/%02d, %04d/%02d/%02d\n" %
         [j2g(2 * julian_then  - julian_today),
          j2g(2 * julian_today - julian_then)] . flatten
end

Find the full program on GitHub.


Please leave any comments as a GitHub issue.