Perl Weekly Challenge: Week 290

Challenge 1:

Double Exist

You are given an array of integers, @ints.

Write a script to find if there exist two indices $i and $j such that:

1) $i != $j
2) 0 <= ($i, $j) < scalar @ints
3) $ints[$i] == 2 * $ints[$j]
Example 1
Input: @ints = (6, 2, 3, 3)
Output: true

For $i = 0, $j = 2
$ints[$i] = 6 => 2 * 3 =>  2 * $ints[$j]
Example 2
Input: @ints = (3, 1, 4, 13)
Output: false
Example 3
Input: @ints = (2, 1, 4, 2)
Output: true

For $i = 2, $j = 3
$ints[$i] = 4 => 2 * 2 =>  2 * $ints[$j]

First we need to setup storage for the result. By default, it is False.

my $result = False;

Then we get the indices of @ints with .keys() and all pairs of those indices with the .combinations(2) method.

for @ints.keys.combinations(2) -> $combo {

For convenience we define $i and $j as the two members of each pair.

    my $i = @$combo[0];
    my $j = @$combo[1];

Then we test if conditions 1 and 3 in the spec are valid. Condition 2 will always be as far as I can see so there is no reason to test that.

    if $i != $j && @ints[$i] == 2 * @ints[$j] {

If the conditions are valid, we set $result to True and stop examing combinations.

        $result = True;
        last;
    }
}

Finally, we print the result.

say $result;

(Full code on Github.)

I've been using Perl 5.38 since the latest Ubuntu LTS operating system came out in April but I haven't fully explored all the new features yet. One that I recently heard about which I will be using a lot is support for true and false values and a boolean type for variables. (see the builtin man page for details.) To use it, you need to add a line like this at the top of the script:

use builtin qw/ true false /;

Because this is still an experimental feature for now, you will need to surpress warnings about it like this:

no warnings qw/ experimental::builtin /;

Now $result is what the documentation calls a "distinguished boolean value" with the default value false.

my $result = false;

We also need to provide a replacement for Raku's .combinations() but I already had that from previous challenges.

for my $combo (combinations([keys @ints], 2)) {
    my $i = $combo->[0];
    my $j = $combo->[1];

    if ($i != $j && $ints[$i] == 2 * $ints[$j]) {
        $result = true;
        last;
    }
}

Unfortunately, the boolean support provided so far doesn't extend to output so we have to explicitly print the words "true" and "false" based on the value of $result.

say $result ? 'true' : 'false';

(Full code on Github.)

Challenge 2:

Luhn's Algorithm

You are given a string $str containing digits (and possibly other characters which can be ignored). The last digit is the payload; consider it separately. Counting from the right, double the value of the first, third, etc. of the remaining digits.

For each value now greater than 9, sum its digits.

The correct check digit is that which, added to the sum of all values, would bring the total mod 10 to zero.

Return true if and only if the payload is equal to the correct check digit.

It was originally posted on reddit.

Example 1
Input: "17893729974"
Output: true

Payload is 4.

Digits from the right:

7 * 2 = 14, sum = 5
9 = 9
9 * 2 = 18, sum = 9
2 = 2
7 * 2 = 14, sum = 5
3 = 3
9 * 2 = 18, sum = 9
8 = 8
7 * 2 = 14, sum = 5
1 = 1

Sum of all values = 56, so 4 must be added to bring the total mod 10 to zero. The payload is indeed 4.
Example 2
Input: "4137 8947 1175 5904"
Output: true
Example 3
Input: "4137 8974 1175 5904"
Output: false

The spec is fairly clear as to how to solve this though I did run into a problem due to misinterpretation.

First we split up $str into its' constituent digits. My mistake was to remove all the non-digit characters; it turns out they are needed to maintain position.

my @digits = $str.comb;

The last digit in @digits is removed and stored separately as the checksum.

my $checksum = @digits.pop;

Storage is reserved for the sum of the digits.

my $sum;

We go through all the indices of @digits one by one in reverse order.

for @digits.keys.reverse -> $k {

If the character at that index is not a digit, we skip to the next one.

    if @digits[$k] !~~ '0' .. '9' {
        next;
    }

If the index is odd, the value of the digit is doubled else it is left as is.

    my $val = $k % 2 ?? @digits[$k] * 2 !! @digits[$k];

If the value is now greater than 9, its digits are seperated with .comb() and added together with .sum(). The value is added to $sum.

    $sum += $val > 9 ?? $val.comb.sum !! $val;
}

If the remainder of $sum modulo 10 is equal to the checksum, we print True else False.

say 10 - $sum % 10 == $checksum;

(Full code on Github.)

This is the Perl version:

my @digits = split //, $str;
my $checksum = pop @digits;
my $sum;

for my $k (reverse keys @digits) {
    if ($digits[$k] !~ /\d/) {
        next;
    }
    my $val = $k % 2 ? $digits[$k] * 2 : $digits[$k];

I had to provide an implementation of sum() as Perl doesn't have it.

    $sum += $val > 9 ? sum(split //, $val) : $val;
}

Once again, we have to explicitly print "true" and "false" based on the value of the conditional.

say 10 - $sum % 10 == $checksum ? 'true' : 'false';

(Full code on Github.)