Perl Weekly Challenge: Week 214

Challenge 1:

Rank Score

You are given a list of scores (>=1).

Write a script to rank each score in descending order. First three will get medals i.e. G (Gold), S (Silver) and B (Bronze). Rest will just get the ranking number.

Using the standard model of giving equal scores equal rank, then advancing that number of ranks.

Example 1
Input: @scores = (1,2,4,3,5)
Output: (5,4,S,B,G)

Score 1 is the 5th rank.
Score 2 is the 4th rank.
Score 4 is the 2nd rank i.e. Silver (S).
Score 3 is the 3rd rank i.e. Bronze (B).
Score 5 is the 1st rank i.e. Gold (G).
Example 2
Input: @scores = (8,5,6,7,4)
Output: (G,4,B,S,5)

Score 8 is the 1st rank i.e. Gold (G).
Score 4 is the 4th rank.
Score 6 is the 3rd rank i.e. Bronze (B).
Score 7 is the 2nd rank i.e. Silver (S).
Score 4 is the 5th rank.
Example 3
Input: @list = (3,5,4,2)
Output: (B,G,S,4)
Example 4
Input: @scores = (2,5,2,1,7,5,1)
Output: (4,S,4,6,G,S,6)

We start by making use of, as I did last week, the .classify() method in order to find how frequently each element in @scores appears. We end up with %quantities, a hash where the keys are scores and the values are every occurrence of that score in @scores.

@scores.classify( { $_ }, :into(my %quantities;) );

That's not exactly what we want though. For instance if the score 2 appeared 4 times in @scores, .classify() would give us 2 => 2 2 2 2 whearas what we would like is 2 => 4. This line does that. There should be a way to simply combine it into one with the line above but I don't know how.

%quantities = %quantities.keys.map({ $_ => %quantities{$_}.elems; });

Next we sort @scores so that the top score is first and the bottom score is at the end; this is stored as a new array called @ordered.

my @ordered = @scores.sort({ $^b <=> $^a });

%ranks is a hash whose keys will be the ranks and the values the ordered scores that have that rank.

my %ranks;

The current rank (the top) will be 1.

my $currentRank = 1;

This is the part that challenged me the most. According to the spec, if more than one score shares the same rank the next rank is not the next consecutive number but advanced by the number of scores that shared the previous rank. To determine how much to skip we have to keep track of how many scores are equal. We already have that data in %quantities we just need to apply to each ordered score.

my $quantity = 0;

We iterate through @ordered by index.

for 0 .. @ordered.end -> $i {

If the value of $quantity is 0 it means we have arrived at a new score. If so, $quantity is set to the number of times that score occurs as recorded in %quantities.

    if $quantity == 0 {
        $quantity = %quantities{@ordered[$i]};
    }

1 is subtracted from $quantity for the current element in @ordered.

    $quantity--;

If the current rank is 1, 2, or 3, the value for this element in %ranks is set to 'G' for Gold, 'S' for Silver, or 'B' for Bronze. In all other cases, the value is set to $currentRank.

    given $currentRank {
        when 1 { %ranks{@ordered[$i]} = 'G' };
        when 2 { %ranks{@ordered[$i]} = 'S' };
        when 3 { %ranks{@ordered[$i]} = 'B' };
        default { %ranks{@ordered[$i]} = $currentRank; }
    }

if the value of $quantity is 0, we have to increase $currentRank by the number of scores that shared the current rank. I tried for sometime to combine this check with the other check for $quantity == 0 at the top of the loop but could not do it without turning the code into spaghetti.

    if $quantity == 0 {
        $currentRank += %quantities{@ordered[$i]};
    }
}

Once all the scores have been processed, we print out the ranks associated with each one in the format the spec suggests.

say q{(}, @scores.map({ %ranks{$_}; }).join(q{,}), q{)};

(Full code on Github.)

Here's the Perl version:

Perl doesn't have .classify() but it is easy enough to emulate.

my %quantities;
map { $quantities{$_}++ } @scores;

my @ordered = sort { $b <=> $a } @scores;
my %ranks;
my $currentRank = 1;
my $quantity = 0;

for my $i (0 .. scalar @ordered - 1) {
    if ($quantity == 0) {
        $quantity = $quantities{$ordered[$i]};
    }
    $quantity--;

After all this time given/when still causes a warning that the feature is experimental unless you add use experimental qw/ switch /; to the top of the script.

    given ($currentRank) {
        when (1) { $ranks{$ordered[$i]} = 'G' };
        when (2) { $ranks{$ordered[$i]} = 'S' };
        when (3) { $ranks{$ordered[$i]} = 'B' };
        default { $ranks{$ordered[$i]} = $currentRank; }
    }

    if ($quantity == 0) {
        $currentRank += $quantities{$ordered[$i]};
    }
}

say q{(}, ( join q{,}, map { $ranks{$_}; } @scores ), q{)};

(Full code on Github.)

Challenge 2:

Collect Points

You are given a list of numbers.

You will perform a series of removal operations. For each operation, you remove from the list N (one or more) equal and consecutive numbers, and add to your score N × N.

Determine the maximum possible score.

Example 1
Input: @numbers = (2,4,3,3,3,4,5,4,2)
Output: 23

We see three 3's next to each other so let us remove that first and collect 3 x 3 points.
So now the list is (2,4,4,5,4,2).
Let us now remove 5 so that all 4's can be next to each other and collect 1 x 1 point.
So now the list is (2,4,4,4,2).
Time to remove three 4's and collect 3 x 3 points.
Now the list is (2,2).
Finally remove both 2's and collect 2 x 2 points.
So the total points collected is 9 + 1 + 9 + 4 => 23.
Example 2
Input: @numbers = (1,2,2,2,2,1)
Output: 20

Remove four 2's first and collect 4 x 4 points.
Now the list is (1,1).
Finally remove the two 1's and collect 2 x 2 points.
So the total points collected is 16 + 4 => 20.
Example 3
Input: @numbers = (1)
Output: 1
Example 4
Input: @numbers = (2,2,2,1,1,2,2,2)
Output: 40

Remove two 1's = 2 x 2 points.
Now the list is (2,2,2,2,2,2).
Then reomove six 2's = 6 x 6 points.

The MAIN() subroutine in Raku just looks like this:

say findHighest(@numbers);

Of course findHighest() is not quite so simple.

sub findHighest(*@numbers) {

We set up a variable to store the highest score.

    my $highestScore = 0;

Then we traverse through @numbers by index.

    for 0 .. @numbers.end -> $i {

Each time, we remove a consecutive run of numbers starting from index $i.

        my $results = removeConsecutive($i, @numbers);

This is removeConsecutive().

sub removeConsecutive($i, *@numbers) {

The number we are looking for a run of is @numbers[$i]. We store it in the variable $current.

    my $current = @numbers[$i];

The length of the run (if there is one) will be stored in $quantity.

    my $quantity = 0;

Then from $i to the end of the list (I could have used .end() here; I don't remember why I didn't.)

    for $i ..^ @numbers.elems -> $n {

If the number is not the same as current we have reached the end of the run so we can break out of the loop. if @numbers[$n] != $current { last;

If not, $quantity is increased by 1.

        } else {
            $quantity++;
        }
    }

Whatever length run of same numbers we found is removed from @numbers.

    @numbers.splice($i, $quantity);

The score for this run is $quantity squared. That and the new, possibly shorter @numbers is returned as a Pair. Why not a list as in the Perl version (see below)? For some reason I was not able to get it to work properly. @numbers was getting corrupted and I didn't understand why. This on the other hand works.

    return $quantity * $quantity => @numbers;
}

I feel I could possibly have made this function simpler but I lacked the time to investigate further.

Back to findHighest()...

The return value from removeConsecutive() is $results which, as I mentioned previously, is a Pair. So here we assign the two parts to variables.

        my $points = $results.key;
        my @newNumbers = $results.value;

$points is added to the return value of a recursive call to findHighest(). When it is finished recursing all the way, we will have the total score for this permutation of @numbers.

        my $score = $points + findHighest(@newNumbers);

if the score is greater than the current highest score, it becomes the new highest score.

        if ($score > $highestScore) {
            $highestScore = $score;
        }
    }

When processing of all permutations of @numbers is finished, we return the highest score.

    return $highestScore;
}

(Full code on Github.)

This is the Perl version of the findHighest() and removeConsecutive() functions.

sub findHighest {
    my @numbers = @_;
    my $highestScore = 0;
    for my $i (0 .. scalar @numbers - 1) {

        my ($points, @rest) = removeConsecutive($i, @numbers);
        my $score = $points + findHighest(@rest);
        if ($score > $highestScore) {
            $highestScore = $score;
        }
    }
    return $highestScore;
}

sub removeConsecutive {
    my $i = shift;
    my @numbers = @_;

    my $current = $numbers[$i];
    my $quantity = 0;

    for my $n ($i .. scalar @numbers - 1) {
        if ($numbers[$n] != $current) {
            last;
        } else {
            $quantity++;
        }
    }
    splice @numbers, $i, $quantity;

removeConsecutive() returns multiple values as a list as it should.

    return ($quantity * $quantity, @numbers);
}

(Full code on Github.)