Luhn's Existence
with Raku

by Arne Sommer

Luhn's Existence with Raku

[311] Published 10. October 2024.

This is my response to The Weekly Challenge #290.

Challenge #290.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]

We do not actually need to consider the indices, just the values. So let us do just that.

The value approach may run into problems if we allow 0 (zero). The given examples do not have zero (nor negative values for that matter) in them, but I'll have a go at it anyway. Later on...

File: double-exist
#! /usr/bin/env raku

unit sub MAIN (*@ints where all(@ints) ~~ Int && @ints.elems > 0);  # [1]

my @double = @ints.map: * * 2;                                      # [2]

say so ( any(@ints) == any(@double) );                              # [3]

[1] A slurpy array of integers, with at least one value.

[2] Get a version of the input, whith all the values doubled. The first * is a reference to the current value, and the second one is the multiplication operator.

[3] Do any of the input values have the double version in the modified array? This is the third rule. Note the so Booelan coercer to reduce the result to a single Boolean value.

See docs.raku.org/routine/any for more information about any.

See docs.raku.org/routine/so for more information about so.

Running it:

$ ./double-exist 6 2 3 3
True

0$ ./double-exist 3 1 4 13
False

$ ./double-exist 2 1 4 2
True

Looking good.

Zero and negative values are ok(ish):

$ ./double-exist 1 -1 
False

$ ./double-exist 1 0 -1 -2
True

$ ./double-exist 1 0 0 -1 
True

0$ ./double-exist 1 0 -1 
True

The last one is wrong, as the zero is a single value and the two indices are equal. This breaks the first rule.

Let us fix that:

File: double-exist-zero
#! /usr/bin/env raku

unit sub MAIN (*@ints where all(@ints) ~~ Int && @ints.elems > 0);


if @ints.grep(* == 0).elems > 1            # [1]
{
  say True;                                # [1a]
}
else
{
  my @unique = @ints.unique.grep: * != 0;  # [2]
  my @double = @unique.map: * * 2;

  say so ( any(@unique) == any(@double) );
}

[1] Do we have at least 2 zeros? Success if we do.

[2] ) The double any construct is a double loop in disguise. We can optimize this by getting rid of duplicates (with unique) before the comparison. This will speed up the program noticeably if the input array is very big.

See docs.raku.org/routine/unique for more information about unique.

We have indeed solved the problem:

$ ./double-exist-zero 1 0 -1
False

$ ./double-exist-zero 1 0 0 -1
True

$ ./double-exist-zero 1 0 0 0 -1
True

This program is not so compact, but it does give the correct result. That should count for something...

Challenge #290.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

Why treat the payload explicitly? We can leave it as a part of the number, and just check the result against mod 10.

File: luhns-algorithm
#! /usr/bin/env raku

unit sub MAIN ($str, :v($verbose));                    # [1]

my @digits = $str.flip.comb.grep: * ~~ /<[0..9]>/;     # [2]

say ": Digits (reverse): @digits[]" if $verbose;

my $total = 0;                                         # [3]

@digits.push(0) unless @digits.elems %% 2;             # [4]

for @digits -> $a, $b                                  # [5]
{
  my $sum = $b * 2; $sum = $sum.comb.sum if $sum > 9;  # [6]

  say ": $a = $a" if $verbose;
  say ": $b * 2 = { $b * 2 }, total = $sum" if $verbose;

  $total += $a;                                        # [7]
  $total += $sum;                                      # [8]
}

say ! so $total mod 10;                                # [9]

[1] Just a string; no constraints. (Or; no strings attached.)

[2] Reverse the string (with flip), get an array with the individual characters (with comb) and keep the digits only (with grep).

See docs.raku.org/routine/flip for more information about flip.

[3] The total sum will end up here.

[4] We are going to iterate over two and two values at a time, so add a trailing zero (which will not impact the total) if we have an odd number of elements. This will prevent an execution error.

[5] Iterate over two and two elements at a time.

[6] The second one we double, and reduce to a single digit if larger than 9.

[7] Add the first digit.

[8] And the doubled second one.

[9] We modula operation will return zero on success. We coerce this to a Boolean value (with so), and negate it (with !).

Running it:

$ ./luhns-algorithm 17893729974
True

$ ./luhns-algorithm "4137 8947 1175 5904"
True

$ ./luhns-algorithm "4137 8974 1175 5904"
False

Looking good.

With verbose mode:

$ ./luhns-algorithm -v 17893729974
: Digits (reverse): 4 7 9 9 2 7 3 9 8 7 1
: 4 = 4
: 7 * 2 = 14, total = 5
: 9 = 9
: 9 * 2 = 18, total = 9
: 2 = 2
: 7 * 2 = 14, total = 5
: 3 = 3
: 9 * 2 = 18, total = 9
: 8 = 8
: 7 * 2 = 14, total = 5
: 1 = 1
: 0 * 2 = 0, total = 0
True

$ ./luhns-algorithm -v "4137 8947 1175 5904"
: Digits (reverse): 4 0 9 5 5 7 1 1 7 4 9 8 7 3 1 4
: 4 = 4
: 0 * 2 = 0, total = 0
: 9 = 9
: 5 * 2 = 10, total = 1
: 5 = 5
: 7 * 2 = 14, total = 5
: 1 = 1
: 1 * 2 = 2, total = 2
: 7 = 7
: 4 * 2 = 8, total = 8
: 9 = 9
: 8 * 2 = 16, total = 7
: 7 = 7
: 3 * 2 = 6, total = 6
: 1 = 1
: 4 * 2 = 8, total = 8
True

$ ./luhns-algorithm -v "4137 8974 1175 5904"
: Digits (reverse): 4 0 9 5 5 7 1 1 4 7 9 8 7 3 1 4
: 4 = 4
: 0 * 2 = 0, total = 0
: 9 = 9
: 5 * 2 = 10, total = 1
: 5 = 5
: 7 * 2 = 14, total = 5
: 1 = 1
: 1 * 2 = 2, total = 2
: 4 = 4
: 7 * 2 = 14, total = 5
: 9 = 9
: 8 * 2 = 16, total = 7
: 7 = 7
: 3 * 2 = 6, total = 6
: 1 = 1
: 4 * 2 = 8, total = 8
False

Note the elegant syntax for iterating over more than one element at a time (in [5]). I could have done this with gather/take, and that would have removed the ugly trailing zero hack.

So, here it is, for comparison.

File: luhns-algorithm-gather
#! /usr/bin/env raku

unit sub MAIN ($str, :v($verbose));

my @digits = $str.flip.comb.grep: * ~~ /<[0..9]>/;

say ": Digits (reverse): @digits[]" if $verbose;

my $seq := gather
{
  my $index = 0;
  for @digits -> $sum is copy;  # [1] 
  {
    unless $index++ %% 2 { $sum *= 2; $sum = $sum.comb.sum if $sum > 9; }
    take $sum;
  }
}

my $total = $seq.sum;

say ! so $total mod 10;

[1] Iterating gives a read only value. We want to change it (locally), so we add is copy.

Running it gives the expected result:

$ ./luhns-algorithm-gather 17893729974
True

$ ./luhns-algorithm-gather "4137 8947 1175 5904"
True

$ ./luhns-algorithm-gather "4137 8974 1175 5904"
False

And that's it.