Perl Weekly Challenge 038: Date Finder and Word Game

Date Finder

Create a script to accept a 7 digits number, where the first number can only be 1 or 2. The second and third digits can be anything 0-9. The fourth and fifth digits corresponds to the month i.e. 01, 02, 03…, 11, 12. And the last 2 digits respresents the days in the month i.e. 01, 02, 03…, 29, 30, 31. Your script should validate if the given number is valid as per the rule and then convert into human readable format date.

RULES

  1. If 1st digit is 1, then prepend 20 otherwise 19 to the 2nd and 3rd digits to make it 4-digits year.
  2. The 4th and 5th digits together should be a valid month.
  3. The 6th and 7th digits together should be a valid day for the above month.

For example, the given number is 2230120, it should print 1923-01-20.

As we’ve done several times, we’ll use the core module Time::Piece to handle dates.

#!/usr/bin/perl
use warnings;
use strict;

use Time::Piece;

sub validate {
    my ($number) = @_;

First, we’ll check the length of the input string.

    die 'Invalid length' unless length $number == 7;

Next, let’s extract the year. Unfortunately, Perl doesn’t have a case statement, but we can use a hash instead:

    my $date = {  1 => 20, 2 => 19 }->{ substr $number, 0, 1 }
        or die 'Invalid year';

The remaining part of the date can be copied literally.

    $date .= substr $number, 1;

If the module can’t parse the date string now, the input was definitely invalid.

    my $tp = eval { 'Time::Piece'->strptime($date, '%Y%m%d') };

Unfortunately, Time::Piece doesn’t complain when you give it a date like 2019-02-30: in fact, it builds the object corresponding to March the 2nd. To catch these “corrections” Time::Piece makes, let’s also check the object’s month corresponds to the months of the input:

    die 'Invalid date' unless $tp && $tp->mon == substr $number, 3, 2;

If we survived up to here, the input was a valid date, therefore we should return it.

    return $tp->ymd
}

As usually, here are some tests to verify our solution is correct.

use Test::More;
use Test::Exception;

throws_ok { validate('123456') }   qr/Invalid length/, 'too short';
throws_ok { validate('12345678') } qr/Invalid length/, 'too long';

throws_ok { validate('3234567') } qr/Invalid year/, 'year';

throws_ok { validate('2xxxxxx') } qr/Invalid date/, 'xxx';
throws_ok { validate('1031301') } qr/Invalid date/, 'Undecember 1 2003';
throws_ok { validate('1030229') } qr/Invalid date/, 'Feb 29 2003';
throws_ok { validate('1040230') } qr/Invalid date/, 'Feb 30 2004';
throws_ok { validate('1040431') } qr/Invalid date/, 'Apr 31 2004';

is validate('1040229'), '2004-02-29', 'Feb 29 2004';
is validate('2801231'), '1980-12-31', 'Dec 31 1980';
is validate('2230120'), '1923-01-20', 'the example';

done_testing();

Word Game

Lets assume we have tiles as listed below, with an alphabet (A…Z) printed on them. Each tile has a value, e.g. A (1 point), B (4 points) etc. You are allowed to draw 7 tiles from the lot randomly. Then try to form a word using the 7 tiles with maximum points altogether. You don’t have to use all the 7 tiles to make a word. You should try to use as many tiles as possible to get the maximum points.

Let’s start with the initialisation. The %tile hash contains the number of tiles of the given type and their point value. We use enum to declare two constants, NUMBER and POINTS, that correspond to the positions in the inner arrays.

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

use enum qw( NUMBER POINTS );
use List::Util qw{ shuffle sum };

my $DICT = '/usr/share/dict/british';

my %tile = (
    A => [ 8,  1 ], G => [ 3,  1 ], I => [ 5, 1 ], S => [ 7, 1 ],
    U => [ 5,  1 ], X => [ 2,  1 ], Z => [ 5, 1 ], E => [ 9, 2 ],
    J => [ 3,  2 ], L => [ 3,  2 ], R => [ 3, 2 ], V => [ 3, 2 ],
    Y => [ 5,  2 ], F => [ 3,  3 ], D => [ 3, 3 ], P => [ 5, 3 ],
    W => [ 5,  3 ], B => [ 5,  4 ], N => [ 4, 4 ], T => [ 5, 5 ],
    O => [ 3,  5 ], H => [ 3,  5 ], M => [ 4, 5 ], C => [ 4, 5 ],
    K => [ 2, 10 ], Q => [ 2, 10 ]);

my @all = shuffle(map +($_) x $tile{$_}[NUMBER], keys %tile);

The last line populates the @all array with a randomly shuffled tiles. How is it done? We start from the keys, each key is mapped to itself repeated its NUMBER of times. Shuffling is done by List::Util::shuffle.

We can now draw 7 tiles: just select the first 7 from the shuffled ones.

my %draw;
++$draw{$_} for @all[0 .. 6];
say "Drawn: @all[0 .. 6]";

We represent the drawn tiles as a hash, you’ll see later what advantages it has over using an array of possibly repeated letters.

Let’s find what words we can build from the tiles. We’ll read all the possible English words in our dictionary (your system might provide a different file with slightly different words, fix the path if needed) sequentially and for each one, we’ll find out whether it could be built from the drawn tiles. The hash representation of %draw will make the code simple and clean:

open my $in, '<', $DICT or die $!;
my @max = (0, "");
WORD: while (<$in>) {
    chomp;

    my @chars = split //, uc;
    my %seen;
    $draw{$_} && $draw{$_} >= ++$seen{$_}
        or next WORD
        for @chars;

Finally, we’ll count the score of the word and store the maximum if reached. If there’s more than one such a word, we’ll store all of them:

    my $score = sum(map $tile{$_}[POINTS], @chars);
    @max = ($score) if $score >  $max[0];
    push @max, $_   if $score >= $max[0];
}
say "@max";

What is the maximal possible score? You can try to modify the code to find out. My dictionary gave me two words that scored 41 points: “Okhotsk” and “tokomak” (I didn’t know you can spell “tokamak” this way).

Leave a comment

About E. Choroba

user-pic I blog about Perl.