Perl Weekly Challenge: Double Your Pleasure, Double Your Luhn

Not every time is the music that the Perl Weekly Challenge inspires going to be a Broadway show tune, a banger from classic rock, or even a baroque fugue. Sometimes, it’s going to be a marketing jingle: 🎶 Double your pleasure, double your fun, with double good, double good, Doublemint Gum! 🎶

See, it’s because “Luhn” rhymes with fun. No? Fine, let’s chew on Perl Weekly Challenge 290 for a little bit.

Task 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]

Approach

Stipulations 1 and 2 just mean that $i and $j have to be different and in the range of the array. It’s the third one, that $ints[$j] is twice $ints[$i], that we need to worry about. There may be a clever way to do this, but I’m always coding these late at night, and I’m striving for “clear and concise” over “clever and elegant”. This feels like a double loop to me.

We can do some optimization, however. If we’re looking at a candidate for $i, if it’s odd, we know that value can’t be $i because it has to be twice one of the other integer values in the array, and twice any integer value has to be even. So if we loop over the array for $i, skipping odd values, and then loop over the array again for $j, skipping where $j == $i, something like this:

for $i from 0 to scalar(@ints)-1 {
  next if $ints[$i] is odd;
  for $j from 0 to scalar(@ints)-1 {
    next if $j == $i;
    next if $ints[$j] * 2 != $ints[$i];
    return true;
  }
}
return false;

Raku

sub doubleExist(@ints) {
  for 0 .. @ints.end -> $i {
    next unless @ints[$i] %% 2;
    for 0 .. @ints.end -> $j {
      next if $j == $i;
      next if @ints[$j] * 2 != @ints[$i];
      return 'true',
        "For \$i = $i, \$j = $j\n" ~
        "\$ints[\$i] = @ints[$i] => 2 * @ints[$j] => " ~
        "2 * \$ints[\$j]";
    }
  }
  return 'false';
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
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 = 0, $j = 1
$ints[$i] = 2 => 2 * 1 => 2 * $ints[$j]

Note that my solution finds an alternate solution to example 3.

Perl

sub doubleExist(@ints) {
  foreach my $i ( 0 .. $#ints ) {
    next unless $ints[$i] % 2 == 0;
    foreach my $j ( 0 .. $#ints ) {
      next if $j == $i;
      next if $ints[$j] * 2 != $ints[$i];
      return 'true',
        "For \$i = $i, \$j = $j\n" .
        "\$ints[\$i] = $ints[$i] => 2 * $ints[$j] => " .
        "2 * \$ints[\$j]";
    }
  }
  return 'false';
}

View the entire Perl script for this task on GitHub.

Python

def doubleExist(ints):
  for i in range(len(ints)):
    if ints[i] % 2 != 0:
      continue
    for j in range(len(ints)):
      if j == i:
        continue
      if ints[j] * 2 != ints[i]:
        continue
      explain = (
        f"For $i = {i}, $j = {j}\n" +
        f"$ints[$i] = {ints[i]} => 2 * {ints[j]} => " +
        "2 * $ints[$j]"
      )
      return 'true', explain
  return 'false', False

View the entire Python script for this task on GitHub.

Elixir

Because Elixir is a functional language, if I want to be able to exit out of a loop early, I need to do the loop via recursion. Also, remember that because Integer.is_even/1 and Integer.is_odd/1 are defined as macros so they can be used as guards, we have to require the module before we can use the functions.

  require Integer # so we can use is_odd/1 as function

  def doubleExist(ints) do
    iLoop(ints, 0)
  end

  def iLoop(ints, i) when i >= length(ints), do: {false, false}
  def iLoop(ints, i) do
    iVal = Enum.at(ints, i)
    # we can't use Enum functions in a guard,
    # so we need this test here
    {answer, explain} = if Integer.is_odd(iVal) do
      {false, false}
    else
      # start the j loop
      jLoop(ints, i, 0)
    end
    if answer do
      # found an answer, return it
      {answer, explain}
    else
      # go to the next possible i value
      iLoop(ints, i + 1)
    end
  end

  def jLoop(ints, _, j) when j >= length(ints), do: {false, false}
  # skip when i == j
  def jLoop(ints, i, j) when i == j, do: jLoop(ints, i, j + 1)
  def jLoop(ints, i, j) do
    iVal = Enum.at(ints, i)
    jVal = Enum.at(ints, j)
    if jVal * 2 == iVal do
      # we found an answer!
      {
        true,
        "For $i = #{i}, $j = #{j}\n" <>
        "$ints[$i] = #{iVal} => 2 * #{jVal} => " <>
        "2 * $ints[$j]"
      }
    else
      # go to the next possible j value
      jLoop(ints, i, j + 1)
    end
  end

View the entire Elixir script for this task on GitHub.


Task 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

Approach

One of the first things I did was look at the Reddit post, and then I looked at the Wikipedia post for the Luhn Algorithm, but I stopped reading when I got to the heading “Pseudocode implementation” (because I don’t want Wikipedia to do my work for me). One thing I noticed is that really what we’re doing is alternating between multiplying the digits by 2 and by 1, and then summing all the digits. So, another way to do example 1 is to just run through the multiplications and generate the digits 149182143188141, and then add those up to get 56.

Raku

sub luhnCheck($num) {
  # just the numeric digits of the number
  my @digits = $num.comb.grep( /\d/ );
  # the last digit is the payload/checksum
  my $payload = @digits.pop;

  my $to_be_summed;
  my $multiplier = 2; # we'll alternate between 2 & 1

  # pull the last digit off the number
  for @digits.reverse -> $digit {
    $to_be_summed ~= $digit.Int * $multiplier;
    # alternate the multiplier
    $multiplier = ($multiplier == 2) ?? 1 !! 2;
  }
  # add up the digits
  my $sum = [+] $to_be_summed.comb;
  # add the payload back in
  $sum += $payload;
  # is it a multiple of 10
  return $sum %% 10;
}

View the entire Raku script for this task on GitHub.

$ $ raku/ch-2.raku
Example 1:
Input: "17893729974"
Output: True

Example 2:
Input: "4137 8947 1175 5904"
Output: True

Example 3:
Input: "4137 8974 1175 5904"
Output: False

Perl

use List::AllUtils qw( sum );

sub luhnCheck($num) {
  # just the numeric digits of the number
  my @digits = grep { /\d/ } split //, $num;
  # the last digit is the payload/checksum
  my $payload = pop @digits;

  my $to_be_summed;
  my $multiplier = 2; # we'll alternate between 2 & 1
  
  # pull the last digit off the number
  foreach my $digit (reverse @digits) {
    $to_be_summed .= $digit * $multiplier;
    # alternate the multiplier
    $multiplier = ($multiplier == 2) ? 1 : 2;
  }
  # add up the digits
  my $sum = sum(split //, $to_be_summed);
  # add the payload back in
  $sum += $payload;
  # is it a multiple of 10
  return $sum % 10 == 0 ? 'true' : 'false';
}

View the entire Perl script for this task on GitHub.

Python

def luhnCheck(num):
  # just the numeric digits of the number
  digits = [ d for d in num if d.isnumeric() ]
  # the last digit is the payload/checksum
  payload = digits.pop()

  to_be_summed = ''
  multiplier = 2 # we'll alternate between 2 & 1

  digits.reverse() # reverse the digits
  # pull the last digit off the number
  for digit in digits:
    to_be_summed += str(int(digit) * multiplier)
    # alternate the multiplier
    multiplier = 1 if multiplier == 2 else 2

  # add up the digits
  digitSum = sum([ int(d) for d in to_be_summed ])
  # add the payload back in
  digitSum += int(payload)
  # is it a multiple of 10
  return digitSum % 10 == 0

View the entire Python script for this task on GitHub.

Elixir

For this solution, I stopped pulling the payload off the end of the number and just started my alternation of the multiplier at 1

  def sumDigits(num) do
    {_, data} = num
    |> Integer.to_string # turn number into string
    |> String.codepoints # break into chars
    |> Enum.map(fn d -> String.to_integer(d) end) # convert to int
    |> Enum.map_reduce(%{ sum: 0 }, fn digit, data ->
      { digit, Map.put(data, :sum, digit + data[:sum])}
    end)
    data[:sum]
  end

  def luhnCheck?(num) do
    digits = num
    |> String.codepoints # break into chars
    |> Enum.filter(fn d -> Regex.match?(~r/\d/, d) end) # just numeric
    |> Enum.map(fn d -> String.to_integer(d) end) # convert to int
    |> Enum.reverse # reverse the digits

    data = %{
      multiplier: 1, # we'll alternate between 2 & 1
      sum: 0 # let's add the numbers as we process them
    }
    alternate = [0, 2, 1] # trick for alternating between 2 & 1
    {_, data} =
      Enum.map_reduce(digits, data, fn digit, data ->
        multiplier = data[:multiplier]
        product = digit * multiplier
        data = data
        |> Map.put(:multiplier, Enum.at(alternate, multiplier)) # alternate the multiplier
        |> Map.put(:sum, data[:sum] + sumDigits(product))
        { product, data }
      end)
    Kernel.rem(data[:sum], 10) == 0
  end

View the entire Elixir script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-290/packy-anderson