Perl Weekly Challenge: You Got to Know When to $HOLD Them

This week’s Perl Weekly Challenge task 2 is about poker hands, so the musical theme this week has to be Kenny Rogers’ The Gambler.

So ante up and let’s deal out for Perl Weekly Challenge 291.

Task 1: Middle Index

You are given an array of integers, @ints.

Write a script to find the leftmost middle index (MI) i.e. the smallest amongst all the possible ones.

A middle index is an index where ints[0] + ints[1] + … + ints[MI-1] == ints[MI+1] + ints[MI+2] + … + ints[ints.length-1].

If MI == 0, the left side sum is considered to be 0. Similarly,
if MI == ints.length - 1, the right side sum is considered to be 0.

Return the leftmost MI that satisfies the condition, or -1 if there is no such index.

Example 1

Input: @ints = (2, 3, -1, 8, 4)
Output: 3

The sum of the numbers before index 3 is: 2 + 3 + -1 = 4
The sum of the numbers after index 3 is: 4 = 4

Example 2

Input: @ints = (1, -1, 4)
Output: 2

The sum of the numbers before index 2 is: 1 + -1 = 0
The sum of the numbers after index 2 is: 0

Example 3

Input: @ints = (2, 5)
Output: -1

There is no valid MI.

Approach

This is a fairly straightforward problem. Loop over the indices, find the sum of numbers before the index (which I’m calling leftSum) and the sum of numbers after the index (which I’m calling rightSum), and compare. If they’re equal, return the index. If you run out of indices, return -1.

Raku

sub middleIndex(@ints) {
  for 0 .. @ints.end -> $i {
    my $leftSum =
      $i == 0 ?? 0 !! [+] @ints[0 .. $i - 1];
    my $rightSum =
      $i == @ints.end ?? 0 !! [+] @ints[$i + 1 .. *-1];
    return $i if $leftSum == $rightSum;
  }
  return -1;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: @arr = (2, 3, -1, 8, 4)
Output: 3

Example 2:
Input: @arr = (1, -1, 4)
Output: 2

Example 3:
Input: @arr = (2, 5)
Output: -1

Example 4:
Input: @arr = (0, 5)
Output: 1

Perl

The thing that caught me was I thought I could do a slice @ints[$i + 1 .. -1], and then I realized that I couldn’t.

sub middleIndex(@ints) {
  foreach my $i ( 0 .. $#ints ) {
    my $leftSum =
      $i == 0 ? 0 : sum @ints[0 .. $i - 1];
    my $rightSum =
      $i == $#ints ? 0 : sum @ints[$i + 1 .. $#ints];
    return $i if $leftSum == $rightSum;
  }
  return -1;
}

View the entire Perl script for this task on GitHub.

Python

Python wins here for being able to have the same verbosity in the fewest lines, but most of that is the lack of braces; it’s the same number of statements.

def middleIndex(ints):
  for i in range(len(ints)):
    leftSum  = 0 if i == 0         else sum(ints[0:i])
    rightSum = 0 if i == len(ints) else sum(ints[i+1:])
    if leftSum == rightSum: return i
  return -1

View the entire Python script for this task on GitHub.

Elixir

Elixir, on the other hand, is a bit more verbose, but it’s still pretty cool. Instead of variables, I’ve made leftSum and rightSum functions with guards to handle the end cases. I’m finding that dealing with lists is easier than I originally thought with functions like Enum.slice/2 and Enum.at/3.

  def leftSum(_ints, i) when i == 0, do: 0
  def leftSum(ints, i) do
    ints |> Enum.slice(0..i-1) |> Enum.sum
  end

  def rightSum(ints, i) when i >= length(ints), do: 0
  def rightSum(ints, i) do
    ints |> Enum.slice(i+1..-1//1) |> Enum.sum
  end

  def middleIndex(ints, i) when i >= length(ints), do: -1
  def middleIndex(ints, i) do
    if leftSum(ints, i) == rightSum(ints, i) do
      i
    else
      middleIndex(ints, i+1)
    end
  end

  def middleIndex(ints) do
    middleIndex(ints, 0)
  end

View the entire Elixir script for this task on GitHub.

Task 2: Poker Hand Rankings

A draw poker hand consists of 5 cards, drawn from a pack of 52: no jokers, no wild cards. An ace can rank either high or low.

Write a script to determine the following three things:

1. How many different 5-card hands can be dealt?
2. How many different hands of each of the 10 ranks can be dealt?
   See here for descriptions of the 10 ranks of Poker hands:
   https://en.wikipedia.org/wiki/List_of_poker_hands#Hand-ranking_categories
3. Check the ten numbers you get in step 2 by adding them together
   and showing that they're equal to the number you get in step 1.

Approach

This problem, on the other hand, does not lend itself well to an iterative approach. At first I excitedly tried it, and it took several minutes to generate the 2,598,960 different possible hands in poker, and another half hour or more to classify them into the 10 possible hand rankings.

So we don’t want to do this by actually generating the hands. We want to do the math.

So this is going to boil down to calculating how many possible combinations of the different hands there are. I had to look up what the formula was for a binomial coefficient, and it’s

This will give us the number of possible combinations of k unordered elements from a set of n elements. For the total number of poker hands, thats n = 52, and k = 5, so comb(52,5) = 52!/(5! * 47!) => 8.065817517094389e67 / (120 * 2.586232415111682e59) => 2,598,960.

But how many of each hand are there? Let’s start at the top, and work our way down.

Royal Flush

This hand is a straight flush of the highest rank, 10-A. There’s only one of these per suit, and there are four suits (♣️, ♦️, ♥️, ♠️), so this is 4.

Straight Flush

Since this is a flush, we can start our calculations with just the cards in single suit, which is 13. A straight is 5 cards increasing monotonically, so the possibilities are A-5, 2-6, 3-7, 4-8, 5-9, 6-10, 7-J, 8-Q, 9-K, and 10-A, but the 10-A combination is a Royal Flush, so per suit, there are 9 straight flushes per suit. To calculate this, it’s the number of cards in a suit (13), plus 1 because the Ace can be used at either end of a straight, minus 1 because when the Ace is used at the high end it’s a Royal Flush, minus 4 (because the straight consumes 5 cards), and then multiply the result by the number of suits: comb(13 – 4, 1) * comb(4, 1) = 36.

Four of a Kind

There are 13 cards in a suit, so there are 13 possible combinations for the four cards of the same kind. But how many combinations of the fifth card are there? Well, if we take four cards out of the deck of 52, the fifth card could be any of the remaining 48. So it should be comb(13, 1) * comb(48, 1) = 624.

Full House

Here we start getting into more calculation for our combinations. We need to figure out how many combinations of three-of-a-kind and two-of-a-kind are in a deck. There are 13 possible ranks we could pick for the three-of-a-kind, and once we pick that, we need to pick three suits for that rank. We can now pick a rank for our two-of-a-kind from the remaining 12 ranks, and we can pick two suits for that rank. So… comb(13, 1) * comb(4, 3) * comb(12, 1) * comb(4, 2) = 13 * 4 * 12 * 6 = 3,744.

Flush

This is five cards of the same suit, but those cards can’t be a straight. But we know there are 10 straights per suit, so we can just subtract that . So if we look at how many combinations of the 13 cards in a suit we can make, we get comb(13, 5)-10 times four suits comb(4, 1). (comb(13, 5) – 10 )* comb(4, 1) = 5,108.

Straight

So to start a straight, there’s only 10 ranks (A-10), so let’s start with comb(10, 1), Then we need to pick a suit for that card, comb(4, 1). Once we’ve picked the starting card, there’s only 4 possibilities for the next card, and 4 possibilities for the card after that, and 4 possibilities for the card after that, and four possibilities for the final card. So we have comb(10, 1) * 4^5 flushes, and then we subtract out the 40 straight flushes for 10,200.

Three of a Kind

This starts out like the full house: there are 13 possible ranks we could pick for the three-of-a-kind, and once we pick that, we need to pick three suits for that rank. Then we need to pick our remaining two cards out of the 48 cards that aren’t the rank of the three-of-a-kind, and we subtract from the total the number of full houses, for 54,912.

Two Pair

Again, this starts out like the full house: there are 13 possible ranks we could pick for each of the two-of-a-kind, and once we pick that, we need to pick two suits for each of those ranks. Then we pick our remaining card out of the 44 cards that aren’t the ranks of the 2 twos-of-a-kind.

One Pair

We have 13 possible ranks we could pick for the pair, and two suits for the pair. Then we pick our remaining 3 cards out of the 12 remaining ranks, and pick 3 suits for each.

High Card

Finally we have our crap hand. 13 possible ranks for each of our five cards, but 10 of those combinations make straights, so (comb(13,5) – 10). Then we need to pick suits for each of those cards, but 4 of those combinations will yield a flush, so (comb(4,1) ** 5 – 4).

Raku

I wasn’t sure if Raku had a built-in factorial function, so I searched, and I came across Andrew Shitov’s wonderful page of about 20 different ways to do it in Raku. I picked the one that used a multi because it looked the most like Elixir, with guards and multiple function definitions.

#!/usr/bin/env raku
use v6;

# factorial calculation
multi sub factorial($n where $n < 2)  { 1 }
multi sub factorial($n) { $n * factorial($n - 1) }

# combinations 
sub combinations($n, $k) {
  factorial($n) / ( factorial($k) * factorial($n - $k) )
}

sub format-number($n) {
  my $str = $n.Str;
  my @parts;
  while ($str.chars > 3) {
    @parts.unshift($str.substr(*-3,*-0));
    $str = $str.chop(3);
  }
  @parts.unshift($str);
  return @parts.join(",");
}

say "1. How many different 5-card hands can be dealt?";
my $possible_hands = combinations(52, 5);
say format-number $possible_hands;
say "";

say "2. How many different hands of each of the 10 ranks can be dealt?";

my $royalFlushes    = combinations(4, 1);
my $straightFlushes = combinations(9, 1) * combinations(4, 1);
my $fourOfAKinds    = combinations(13, 1) * combinations(48, 1);
my $fullHouses      = combinations(13, 1) * combinations(4, 3) 
                    * combinations(12, 1) * combinations(4, 2);
my $flushes         = (combinations(13, 5) - 10)
                    * combinations(4, 1);
my $straights       = combinations(10, 1) * (4 ** 5)
                    - $straightFlushes - $royalFlushes;     
my $threeOfAKinds   = combinations(13, 1) * combinations(4, 3) 
                    * combinations(48, 2) - $fullHouses;
my $twoPair         = combinations(13, 2) * (combinations(4, 2) ** 2)
                    * combinations(44, 1);
my $onePair         = combinations(13, 1) * combinations(4, 2)
                    * combinations(12, 3) * combinations(4, 1) ** 3;
my $highCard        = (combinations(13, 5) - 10)
                    * (combinations(4, 1) ** 5 - 4);

printf "Royal Flush:     %9s\n", format-number $royalFlushes;
printf "Straight Flush:  %9s\n", format-number $straightFlushes;
printf "Four of a Kind:  %9s\n", format-number $fourOfAKinds;
printf "Full House:      %9s\n", format-number $fullHouses;
printf "Flush:           %9s\n", format-number $flushes;
printf "Straight:        %9s\n", format-number $straights;
printf "Three of a Kind: %9s\n", format-number $threeOfAKinds;
printf "Two Pair:        %9s\n", format-number $twoPair;
printf "One Pair:        %9s\n", format-number $onePair;
printf "High Card:       %9s\n", format-number $highCard;
say    "--------------------------";
printf "Total:           %9s\n", format-number 
  $royalFlushes + $straightFlushes + $fourOfAKinds +
  $fullHouses + $flushes + $straights + $threeOfAKinds +
  $twoPair + $onePair + $highCard;

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
1. How many different 5-card hands can be dealt?
2,598,960

2. How many different hands of each of the 10 ranks can be dealt?
Royal Flush:             4
Straight Flush:         36
Four of a Kind:        624
Full House:          3,744
Flush:               5,108
Straight:           10,200
Three of a Kind:    54,912
Two Pair:          123,552
One Pair:        1,098,240
High Card:       1,302,540
--------------------------
Total:           2,598,960

Perl

For the factorial in Perl I went with a much more Perlish implementation. I am happy that I was able to use the 4-argument version of substr for the number formatting, though.

#!/usr/bin/env perl
use v5.40;

# factorial calculation
sub factorial($n) {
  return 1 if $n < 2;
  return $n * factorial($n - 1);
}

# combinations 
sub combinations($n, $k) {
  factorial($n) / ( factorial($k) * factorial($n - $k) )
}

sub format_number($n) {
  my $str = "$n";
  my @parts;
  while (length($str) > 3) {
    unshift @parts, substr($str, -3, 3, "");
  }
  unshift @parts, $str;
  return join(",", @parts);
}

say "1. How many different 5-card hands can be dealt?";
my $possible_hands = combinations(52, 5);
say format_number $possible_hands;
say "";

say "2. How many different hands of each of the 10 ranks can be dealt?";

my $royalFlushes    = combinations(4, 1);
my $straightFlushes = combinations(9, 1) * combinations(4, 1);
my $fourOfAKinds    = combinations(13, 1) * combinations(48, 1);
my $fullHouses      = combinations(13, 1) * combinations(4, 3) 
                    * combinations(12, 1) * combinations(4, 2);
my $flushes         = (combinations(13, 5) - 10)
                    * combinations(4, 1);
my $straights       = combinations(10, 1) * (4 ** 5)
                    - $straightFlushes - $royalFlushes;     
my $threeOfAKinds   = combinations(13, 1) * combinations(4, 3) 
                    * combinations(48, 2) - $fullHouses;
my $twoPair         = combinations(13, 2) * (combinations(4, 2) ** 2)
                    * combinations(44, 1);
my $onePair         = combinations(13, 1) * combinations(4, 2)
                    * combinations(12, 3) * combinations(4, 1) ** 3;
my $highCard        = (combinations(13, 5) - 10)
                    * (combinations(4, 1) ** 5 - 4);

printf "Royal Flush:     %9s\n", format_number $royalFlushes;
printf "Straight Flush:  %9s\n", format_number $straightFlushes;
printf "Four of a Kind:  %9s\n", format_number $fourOfAKinds;
printf "Full House:      %9s\n", format_number $fullHouses;
printf "Flush:           %9s\n", format_number $flushes;
printf "Straight:        %9s\n", format_number $straights;
printf "Three of a Kind: %9s\n", format_number $threeOfAKinds;
printf "Two Pair:        %9s\n", format_number $twoPair;
printf "One Pair:        %9s\n", format_number $onePair;
printf "High Card:       %9s\n", format_number $highCard;
say    "--------------------------";
printf "Total:           %9s\n", format_number 
  $royalFlushes + $straightFlushes + $fourOfAKinds +
  $fullHouses + $flushes + $straights + $threeOfAKinds +
  $twoPair + $onePair + $highCard;

View the entire Perl script for this task on GitHub.

Python

#!/usr/bin/env python

# factorial calculation
def factorial(n):
  if n < 2: return 1
  return n * factorial(n - 1)

# combinations 
def combinations(n, k):
  return int(factorial(n) / ( factorial(k) * factorial(n - k) ))

def format_number(n):
  s = str(n)
  parts = []
  while (len(s) > 3):
    parts.insert(0, s[-3:])
    s = s[:-3]
  parts.insert(0, s)
  return ",".join(parts)

print("1. How many different 5-card hands can be dealt?")
possible_hands = combinations(52, 5)
print(format_number(possible_hands) + "\n")

print("2. How many different hands of each of the 10 ranks can be dealt?")

royalFlushes    = combinations(4, 1)
straightFlushes = combinations(9, 1) * combinations(4, 1)
fourOfAKinds    = combinations(13, 1) * combinations(48, 1)
fullHouses      = combinations(13, 1) * combinations(4, 3) \
                * combinations(12, 1) * combinations(4, 2)
flushes         = (combinations(13, 5) - 10) \
                * combinations(4, 1)
straights       = combinations(10, 1) * (4 ** 5) \
                - straightFlushes - royalFlushes
threeOfAKinds   = combinations(13, 1) * combinations(4, 3) \
                * combinations(48, 2) - fullHouses
twoPair         = combinations(13, 2) * (combinations(4, 2) ** 2) \
                * combinations(44, 1)
onePair         = combinations(13, 1) * combinations(4, 2) \
                * combinations(12, 3) * combinations(4, 1) ** 3
highCard        = (combinations(13, 5) - 10) \
                * (combinations(4, 1) ** 5 - 4)

print(f"Royal Flush:     { format_number(royalFlushes) : >9}")
print(f"Straight Flush:  { format_number(straightFlushes) : >9}")
print(f"Four of a Kind:  { format_number(fourOfAKinds) : >9}")
print(f"Full House:      { format_number(fullHouses) : >9}")
print(f"Flush:           { format_number(flushes) : >9}")
print(f"Straight:        { format_number(straights) : >9}")
print(f"Three of a Kind: { format_number(threeOfAKinds) : >9}")
print(f"Two Pair:        { format_number(twoPair) : >9}")
print(f"One Pair:        { format_number(onePair) : >9}")
print(f"High Card:       { format_number(highCard) : >9}")
print("--------------------------")
total = format_number(
  royalFlushes + straightFlushes + fourOfAKinds +
  fullHouses + flushes + straights + threeOfAKinds +
  twoPair + onePair + highCard
)
print(f"Total:           { total : >9}")

View the entire Python script for this task on GitHub.

Elixir

#!/usr/bin/env elixir

defmodule PWC do
# factorial calculation
  def factorial(n) when n < 2, do: 1
  def factorial(n), do: n * factorial(n - 1)

  def combinations(n, k) do
    trunc(factorial(n) / ( factorial(k) * factorial(n - k) ))
  end

  def format_number(n, parts) when n == "" do
    Enum.join(parts, ",")
  end
  def format_number(n, parts) do
    parts = [String.slice(n, -3..-1) | parts]
    format_number(String.slice(n,0..-4//1), parts)
  end
  def format_number(n) do
    format_number(Integer.to_string(n), [])
  end

  def pad9(s) do
    String.pad_leading(s, 9)
  end

  def solution() do
    IO.puts("1. How many different 5-card hands can be dealt?")
    possible_hands = combinations(52, 5)
    IO.puts("#{format_number(possible_hands)}\n")

    IO.puts("2. How many different hands of each of the 10 ranks can be dealt?")

    royalFlushes    = combinations(4, 1)
    straightFlushes = combinations(9, 1) * combinations(4, 1)
    fourOfAKinds    = combinations(13, 1) * combinations(48, 1)
    fullHouses      = combinations(13, 1) * combinations(4, 3)
                    * combinations(12, 1) * combinations(4, 2)
    flushes         = (combinations(13, 5) - 10)
                    * combinations(4, 1)
    straights       = combinations(10, 1) * (4 ** 5) - straightFlushes - royalFlushes
    threeOfAKinds   = combinations(13, 1) * combinations(4, 3)
                    * combinations(48, 2) - fullHouses
    twoPair         = combinations(13, 2) * (combinations(4, 2) ** 2)
                    * combinations(44, 1)
    onePair         = combinations(13, 1) * combinations(4, 2)
                    * combinations(12, 3) * combinations(4, 1) ** 3
    highCard        = (combinations(13, 5) - 10)
                    * (combinations(4, 1) ** 5 - 4)

    total = royalFlushes + straightFlushes + fourOfAKinds +
            fullHouses + flushes + straights + threeOfAKinds +
            twoPair + onePair + highCard

    IO.puts("Royal Flush:     #{ pad9(format_number(royalFlushes)) }")
    IO.puts("Straight Flush:  #{ pad9(format_number(straightFlushes)) }")
    IO.puts("Four of a Kind:  #{ pad9(format_number(fourOfAKinds)) }")
    IO.puts("Full House:      #{ pad9(format_number(fullHouses)) }")
    IO.puts("Flush:           #{ pad9(format_number(flushes)) }")
    IO.puts("Straight:        #{ pad9(format_number(straights)) }")
    IO.puts("Three of a Kind: #{ pad9(format_number(threeOfAKinds)) }")
    IO.puts("Two Pair:        #{ pad9(format_number(twoPair)) }")
    IO.puts("One Pair:        #{ pad9(format_number(onePair)) }")
    IO.puts("High Card:       #{ pad9(format_number(highCard)) }")
    IO.puts("--------------------------")
    IO.puts("Total:           #{ pad9(format_number(total)) }")
  end
end

PWC.solution()

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-291/packy-anderson