The Good Shuffle
with Raku

by Arne Sommer

The Good Shuffle with Raku

[374] Published 6. December 2025.

This is my response to The Weekly Challenge #350.

London Perl and Raku Workshop 2025

I attended the London Perl and Raku Workshop 2025 last weekend, and it was a success. Except that I caught a cold during the stay.

Our own (weekly challenger) Mohammad Anwar deserves a special mention, for the publication of his first book - Design Patterns in Modern Perl.

And he has been doing the challenges for 350 weeks. Quite a challenge in itself.

Challenge #350.1: Good Substrings

You are given a string.

Write a script to return the number of good substrings of length three in the given string.

A string is good if there are no repeated characters.

Example 1:
Input: $str = "abcaefg"
Output: 5

Good substrings of length 3: abc, bca, cae, aef and efg
Example 2:
Input: $str = "xyzzabc"
Output: 3

Good substrings of length 3: "xyz", "zab" and "abc"
Example 3:
Input: $str = "aababc"
Output: 1

Good substrings of length 3: "abc"
Example 4:
Input: $str = "qwerty"
Output: 4

Good substrings of length 3: "qwe", "wer", "ert" and "rty"
Example 5:
Input: $str = "zzzaaa"
Output: 0
File: good-substrings
#! /usr/bin/env raku

unit sub MAIN ($str where $str.chars > 0, :v(:$verbose));  # [1]

my $good = 0;                                              # [2]

for 0 .. $str.chars - 3 -> $i                              # [3]
{
  my $substring =  $str.substr($i, 3);                     # [4]
  my $is-good = $substring.comb.Bag.elems == 3;            # [5]
  $good++ if $is-good;                                     # [6]

  say ": Candidate: '$substring' { $is-good ?? "is good ($good)" !! "" }"
    if $verbose;

}

say $good;                                                 # [7]

[1] A string with at least one character.

[2] The number of good substrings we encounter.

[3] Iterate over the starting positions (index) of all the substrings with length 3.

[4] Get the substring.

[5] Turn the substring into individual characters and that array into a Bag, a hash-like structure that counts the frequencies. The string is good if we have three distinct characters (keys in the hash).

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

[6] Count it, if it is good.

[7] Print the result.

Running it:

$ ./good-substrings qabcaefg
5

$ ./good-substrings xyzzabc
3

$ ./good-substrings aababc
1

$ ./good-substrings qwerty
4

$ ./good-substrings zzzaaa

0

Looking good.

With verbose mode:

$ ./good-substrings -v abcaefg
: Candidate: 'abc' is good (1)
: Candidate: 'bca' is good (2)
: Candidate: 'cae' is good (3)
: Candidate: 'aef' is good (4)
: Candidate: 'efg' is good (5)
5

$ ./good-substrings -v xyzzabc
: Candidate: 'xyz' is good (1)
: Candidate: 'yzz' 
: Candidate: 'zza' 
: Candidate: 'zab' is good (2)
: Candidate: 'abc' is good (3)
3

$ ./good-substrings -v aababc
: Candidate: 'aab' 
: Candidate: 'aba' 
: Candidate: 'bab' 
: Candidate: 'abc' is good (1)
1

$ ./good-substrings -v qwerty
: Candidate: 'qwe' is good (1)
: Candidate: 'wer' is good (2)
: Candidate: 'ert' is good (3)
: Candidate: 'rty' is good (4)
4

$ ./good-substrings -v zzzaaa
: Candidate: 'zzz' 
: Candidate: 'zza' 
: Candidate: 'zaa' 
: Candidate: 'aaa' 
0

Challenge #350.2: Shuffle Pairs

If two integers A <= B have the same digits but in different orders, we say that they belong to the same shuffle pair if and only if there is an integer k such that B = A * k where k is called the witness of the pair.

For example, 1359 and 9513 belong to the same shuffle pair, because 1359 * 7 = 9513.

Interestingly, some integers belong to several different shuffle pairs. For example, 123876 forms one shuffle pair with 371628, and another with 867132, as 123876 * 3 = 371628, and 123876 * 7 = 867132.

Write a function that for a given $from, $to, and $count returns the number of integers $i in the range $from <= $i <= $to that belong to at least $count different shuffle pairs.

PS: Inspired by a conversation between Mark Dominus and Simon Tatham at Mastodon.

Example 1:
Input: $from = 1, $to = 1000, $count = 1
Output: 0

There are no shuffle pairs with elements less than 1000.
Example 2:
Input: $from = 1500, $to = 2500, $count = 1
Output: 3

There are 3 integers between 1500 and 2500 that belong to shuffle pairs.

1782, the other element is 7128 (witness 4)
2178, the other element is 8712 (witness 4)
2475, the other element is 7425 (witness 3)
Example 3:
Input: $from = 1_000_000, $to = 1_500_000, $count = 5
Output: 2

There are 2 integers in the given range that belong to 5 different
shuffle pairs.

1428570 pairs with 2857140, 4285710, 5714280, 7142850, and 8571420
1429857 pairs with 2859714, 4289571, 5719428, 7149285, and 8579142

The witnesses are 2, 3, 4, 5, and 6 for both the integers.
Example 4:
Input: $from = 13_427_000, $to = 14_100_000, $count = 2
Output: 11

6 integers in the given range belong to 3 different shuffle pairs,
5 integers belong to 2 different ones.
Example 5:
Input: $from = 1030, $to = 1130, $count = 1
Output: 2

There are 2 integers between 1020 and 1120 that belong to at least one
shuffle pair:
1035, the other element is 3105 (witness k = 3)
1089, the other element is 9801 (witness k = 9)
File: shuffle-pairs
#! /usr/bin/env raku

unit sub MAIN (UInt :f(:$from),                    # [1]
               UInt :t(:$to) where $to >= $from,   # [1a]
	       UInt :c(:$count),                   # [1b]
	            :v(:$verbose));

my $good = 0;                                      # [2]

for $from .. $to -> $current                       # [3]
{
  my $canonical = $current.comb.sort.join;         # [4]
  my $length    = $current.chars;                  # [5]
  my $shuffle   = 0;                               # [6]
  my @verbose;                                     # [7]
  
  for 2 .. Inf -> $multiplier                      # [8]
  {
    my $new = $current * $multiplier;              # [9]
    last if $new.chars > $length;                  # [10]
    my $canon = $new.comb.sort.join;               # [11]

    if $canonical eq $canon                        # [12]
    {
      $shuffle++;                                  # [13]
      @verbose.push: "$new (x $multiplier)";       # [14]
      if $shuffle == $count                        # [15]
      {
        say ": $current -> { @verbose.join(", ") } (count $shuffle)"
          if $verbose;
    
        $good++;                                   # [16]
	last;                                      # [17]
      }
    }
  }
}

say $good;                                         # [18]

[1] Named arguments for the three integer arguments. I use the UInt (Unsigned Int) type, to prevent negative values.

See docs.raku.org/type/UInt for more information about the UInt type.

[2] The result will end up here.

[3] Iterate over the values in the from .. to range.

[4] Get the canonical version of the current value, by sorting the digits and gluing them back together.

[5] The length of the current value, to be used in [10].

[6] The number of found shuffled variants.

[7] Use by verbose mode, to collect the shuffled values.

[8] Iterate over all posible multipliers.

[9] Get the multiplum.

[10] Exit the loop if we get too many digits.

[11] Get the canonical form of this candidate.

[12] Are the two canoinical form equal? Note string comparison, so that anyleadinbg zeros are taken into account. Not that it actually matteres here...

[13] Count the shuffled value.

[14] Set up for verbose mode.

[15] Have we reached the target? If so, be verbose if so requested.

[16] Count the value.

[17] Finish the loop, as we do not care for more shuffled pairs than the given limit.

[18] Print the result.

Running it:

$ ./shuffle-pairs -f=1 -t=1000 -c=1
0

$ ./shuffle-pairs -f=1500 -t=2500 -c=1
3

$ ./shuffle-pairs -f=1000000 -t=1500000 -c=5
2

$ ./shuffle-pairs -f=13427000 -t=14100000 -c=2
11

$ ./shuffle-pairs -f=1030 -t=1130 -c=1

Looking good.

With verbose mode:

$ ./shuffle-pairs -f=1 -t=1000 -c=1 -v
0

$ ./shuffle-pairs -f=1500 -t=2500 -c=1 -v
: 1782 -> 7128 (x 4) (count 1)
: 2178 -> 8712 (x 4) (count 1)
: 2475 -> 7425 (x 3) (count 1)
3

$ ./shuffle-pairs -f=1000000 -t=1500000 -c=5 -v
: 1428570 -> 2857140 (x 2), 4285710 (x 3), 5714280 (x 4), 7142850 (x 5), \
  8571420 (x 6) (count 5)
: 1429857 -> 2859714 (x 2), 4289571 (x 3), 5719428 (x 4), 7149285 (x 5), \
  8579142 (x 6) (count 5)
2

$ ./shuffle-pairs -f=13427000 -t=14100000 -c=2 -v
: 13428567 -> 26857134 (x 2), 53714268 (x 4) (count 2)
: 13428657 -> 26857314 (x 2), 53714628 (x 4) (count 2)
: 13567842 -> 27135684 (x 2), 54271368 (x 4) (count 2)
: 13568427 -> 27136854 (x 2), 67842135 (x 5) (count 2)
: 13874526 -> 41623578 (x 3), 83247156 (x 6) (count 2)
: 13875246 -> 41625738 (x 3), 83251476 (x 6) (count 2)
: 14002857 -> 28005714 (x 2), 42008571 (x 3) (count 2)
: 14025897 -> 28051794 (x 2), 70129485 (x 5) (count 2)
: 14028570 -> 28057140 (x 2), 42085710 (x 3) (count 2)
: 14028597 -> 28057194 (x 2), 42085791 (x 3) (count 2)
: 14029857 -> 28059714 (x 2), 42089571 (x 3) (count 2)
11

$ ./shuffle-pairs -f=1030 -t=1130 -c=1 -v
: 1035 -> 3105 (x 3) (count 1)
: 1089 -> 9801 (x 9) (count 1)
2

Som more, just for fun:

$ ./shuffle-pairs -f=13427000 -t=14100000 -c=3 -v
: 13428567 -> 26857134 (x 2), 53714268 (x 4), 67142835 (x 5) (count 3)
: 13428657 -> 26857314 (x 2), 53714628 (x 4), 67143285 (x 5) (count 3)
: 14002857 -> 28005714 (x 2), 42008571 (x 3), 70014285 (x 5) (count 3)
: 14028570 -> 28057140 (x 2), 42085710 (x 3), 70142850 (x 5) (count 3)
: 14028597 -> 28057194 (x 2), 42085791 (x 3), 70142985 (x 5) (count 3)
: 14029857 -> 28059714 (x 2), 42089571 (x 3), 70149285 (x 5) (count 3)
6

$ ./shuffle-pairs -f=13427000 -t=14100000 -c=4 -v
0

And that's it.