Equally Final
with Raku

by Arne Sommer

Equally Final with Raku

[360] Published 29. August 2025.

This is my response to The Weekly Challenge #336.

Challenge #336.1: Equal Group

You are given an array of integers.

Write a script to return true if the given array can be divided into one or more groups: each group must be of the same size as the others, with at least two members, and with all members having the same value.

Example 1:
Input: @ints = (1,1,2,2,2,2)
Output: true

Groups: (1,1), (2,2), (2,2)
Example 2:
Input: @ints = (1,1,1,2,2,2,3,3)
Output: false

Groups: (1,1,1), (2,2,2), (3,3)
Example 3:
Input: @ints = (5,5,5,5,5,5,7,7,7,7,7,7)
Output: true

Groups: (5,5,5,5,5,5), (7,7,7,7,7,7)
Example 4:
Input: @ints = (1,2,3,4)
Output: false
Example 5:
Input: @ints = (8,8,9,9,10,10,11,11)
Output: true

Groups: (8,8), (9,9), (10,10), (11,11)

First a program that gives the correct answer for the given examples, but is wrong nevertheless...

File: equal-group
#! /usr/bin/env raku

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

my $bag = @ints>>.Int.Bag;                                         # [2]
my $min = @ints.min;                                               # [3]

if $verbose
{
  say ": Bag: { $bag.raku }";
  say ": Group size (min freq): $min";
}

say so $min > 1 && all($bag.values) %% $min;                       # [4]

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

[2] Coerce all the integers to integers (absurdity intended), to get rid of the pesky IntStr type that would look ugly in the verbose mode output. Then we coerce the integers to a Bag, a hash like structure that counts duplicates.

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

[3] Get the lowest frequency of the input. This is the size of the groups.

[4] If all the frequencies are divisible (%%) by the lowest frequency (which must be 2 or higher), print 'True'. If not, print 'False'. The Booelean coercer so collapses the all Junction to a single Boolean value.

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

Running it:

$ ./equal-group 1 1 2 2 2 2
True

$ ./equal-group 1 1 1 2 2 2 3 3
False

$ ./equal-group 5 5 5 5 5 5 7 7 7 7 7 7
True

$ ./equal-group 1 2 3 4
False

$ ./equal-group 8 8 9 9 10 10 11 11
True

Looking good.

With verbose mode:

$ ./equal-group -v 1 1 2 2 2 2
: Bag: (2=>4,1=>2).Bag
: Group size (min freq): 2
True

$ ./equal-group -v 1 1 1 2 2 2 3 3
: Bag: (1=>3,3=>2,2=>3).Bag
: Group size (min freq): 2
False

$ ./equal-group -v 5 5 5 5 5 5 7 7 7 7 7 7
: Bag: (7=>6,5=>6).Bag
: Group size (min freq): 6
True

$ ./equal-group -v 1 2 3 4
: Bag: (2=>1,4=>1,1=>1,3=>1).Bag
: Group size (min freq): 1
False

$ ./equal-group -v 8 8 9 9 10 10 11 11
: Bag: (10=>2,8=>2,9=>2,11=>2).Bag
: Group size (min freq): 2
True

Let us show why this prorogram is wrong...

$ ./equal-group -v 4 4 4 4 6 6 6 6 6 6
: Bag: (4=>4,6=>6).Bag
: Group size (min freq): 4
False

This is wrong, because ((4,4), (4,4), (6,6), (6,6), (6,6)) is a perfectly valid division.

That is actually easy to fix:

File: equal-group-prime
#! /usr/bin/env raku

unit sub MAIN (*@ints where @ints.elems > 0 && all(@ints) ~~ Int,
               :v(:$verbose));


my $bag = @ints>>.Int.Bag;
my $min = $bag.values.min;

if $verbose
{
  say ": Bag: { $bag.raku }";
  say ": Group size (min freq): $min";
}

for 2 .. $min -> $i                      # [1]
{
  next unless $i.is-prime;               # [1a]

  if all($bag.values) %% $i              # [2]
  {
    say ": Group size (prime): $i";
    say 'true';                          # [2a]
    exit;                                # [2b]
  }
}

say 'false';                             # [3]

[1] Iterate over primes (is-prime) from 2 to the lowest frequency.

See docs.raku.org/routine/is-prime for more information about is-prime.

[2] Check if the frequencies are divisible by the current prime. If so, report success [2a] and be done [2b].

[3] Failing to succeed in the loop means failure. Say so.

Running it on the example that invalidated the first program works out, here with verbose mode:

$ ./equal-group-prime -v 4 4 4 4 6 6 6 6 6 6
: Bag: (6=>6,4=>4).Bag
: Group size (min freq): 4
: Group size (prime): 2
true

Note that the order of the input is irrelevant for this program, but that may not be what the challenge requires.

This example should most probably fail:

$ ./equal-group-prime -v 2 1 2 2 1 2
: Bag: (2=>4,1=>2).Bag
: Group size (min freq): 2
: Group size (prime): 2
true

Let us fix that:

File: equal-group-ordered
#! /usr/bin/env raku

unit sub MAIN (*@ints where @ints.elems > 0 && all(@ints) ~~ Int,
               :v(:$verbose));

my @freq;                    # [1]
my $curr = @ints[0];         # [2]
my $freq = 1;                # [3]

for @ints[1 .. Inf] -> $int  # [4]
{
  if $int == $curr           # [5]
  {
    $freq++;                 # [5a]
  }
  else                       # [6]
  {
    @freq.push: $freq;       # [6a]
    $curr = $int;            # [6b]
    $freq = 1;               # [6c]
  }
}

@freq.push: $freq;           # [7]

my $min = @freq.min;         # [8]

if $verbose
{
  say ": Frequency: { @freq.join(", ") }";
  say ": Group size (min freq): $min";
}

for 2 .. $min -> $i
{
  next unless $i.is-prime;

  if all(@freq) %% $i
  {
    if $verbose
    {
      say ": Group size (prime): $i";
      say ": Grouped: { @ints>>.Int.rotor($i).raku }";  # [9]
    } 
    say 'true';
    exit;
  }
}

say 'false';

[1] We will collect the frequencies here, instead of using the former $bag.values.

[2] The first value,

[3] and the frequency of that value.

[4] Iterate over the remainder of the input.

[5] The same value as the previous one? If so, increase the frequency counter [5a].

[6] If not, add the frequency (of the previous value) to the frequency list [6a], set the new value [6b] and reset the frequency to 1 [6c].

[7] Add the very last frequency to the list.

[8] Getting the lowest frequency from a list is easy.

[9] Note the we did not record or save the actual values for posterity, just the frequencies. We can compute them on demand with rotor as done in verbose mode.

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

Running it gives the expected result:

$ ./equal-group-ordered -v 2 1 2 2 1 2
: Frequency: 1, 1, 2, 1, 1
: Group size (min freq): 1
false

$ ./equal-group-ordered -v 1 1 2 2 2 2
: Frequency: 2, 4
: Group size (min freq): 2
: Group size (prime): 2
true

Challenge #336.2: Final Score

You are given an array of scores by a team.

Write a script to find the total score of the given team. The score can be any integer, +, C or D.

The + adds the sum of previous two scores. The score C invalidates the previous score. The score D will double the previous score.

Example 1:
Input: @scores = ("5","2","C","D","+")
Output: 30

Round 1: 5
Round 2: 5 + 2
Round 3: 5 (invalidate the previous score 2)
Round 4: 5 + 10 (double the previous score 5)
Round 5: 5 + 10 + 15 (sum of previous two scores)

Total Scores: 30
Example 2:
Input: @scores = ("5","-2","4","C","D","9","+","+")
Output: 27

Round 1: 5
Round 2: 5 + (-2)
Round 3: 5 + (-2) + 4
Round 4: 5 + (-2) (invalidate the previous score 4)
Round 5: 5 + (-2) + (-4) (double the previous score -2)
Round 6: 5 + (-2) + (-4) + 9
Round 7: 5 + (-2) + (-4) + 9 + 5 (sum of previous two scores)
Round 8: 5 + (-2) + (-4) + 9 + 5 + 14 (sum of previous two scores)

Total Scores: 27
Example 3:
Input: @scores = ("7","D","D","C","+","3")
Output: 45

Round 1: 7
Round 2: 7 + 14 (double the previous score 7)
Round 3: 7 + 14 + 28 (double the previous score 14)
Round 4: 7 + 14 (invalidate the previous score 28)
Round 5: 7 + 14 + 21 (sum of previous two scores)
Round 6: 7 + 14 + 21 + 3

Total Scores: 45
Example 4:
Input: @scores = ("-5","-10","+","D","C","+")
Output: -55

Round 1: (-5)
Round 2: (-5) + (-10)
Round 3: (-5) + (-10) + (-15) (sum of previous two scores)
Round 4: (-5) + (-10) + (-15) + (-30) (double the previous score -15)
Round 5: (-5) + (-10) + (-15) (invalidate the previous score -30)
Round 6: (-5) + (-10) + (-15) + (-25) (sum of previous two scores)

Total Scores: -55
Example 5:
Input: @scores = ("3","6","+","D","C","8","+","D","-2","C","+")
Output: 128

Round  1: 3
Round  2: 3 + 6
Round  3: 3 + 6 + 9 (sum of previous two scores)
Round  4: 3 + 6 + 9 + 18 (double the previous score 9)
Round  5: 3 + 6 + 9 (invalidate the previous score 18)
Round  6: 3 + 6 + 9 + 8
Round  7: 3 + 6 + 9 + 8 + 17 (sum of previous two scores)
Round  8: 3 + 6 + 9 + 8 + 17 + 34 (double the previous score 17)
Round  9: 3 + 6 + 9 + 8 + 17 + 34 + (-2)
Round 10: 3 + 6 + 9 + 8 + 17 + 34 (invalidate the previous score -2)
Round 11: 3 + 6 + 9 + 8 + 17 + 34 + 51 (sum of previous two scores)

Total Scores: 128

File: final-score
#! /usr/bin/env raku

unit sub MAIN (*@scores where @scores.elems > 0,                      # [1]
               :v(:$verbose));

my @processed;                                                        # [2]
my $round = 1;

for @scores -> $score                                                 # [3]
{
  given $score                                                        # [4]
  {
    when Int   { @processed.push: $score }                            # [5]
    when '+'   { @processed.push: @processed[*-1] + @processed[*-2] } # [6]
    when 'C'   { @processed.pop }                                     # [7]
    when 'D'   { @processed.push: @processed[*-1] * 2 }               # [8]
    default    { die "Illegal score $score" }                         # [9]
  }

  say ": Round { $round++ }: Value:$score -> Scores: { @processed.join(",") }"
    if $verbose;
}

say @processed.sum;                                                   # [10]

[1] A slurpy array of scores, with at least one. Note the missing input validation, as it is done in [9] instead.

[2] The processed scores, i.e. integers, are collected here.

[3] Iterate over the scores.

[4] Given (given) the current score,

See docs.raku.org/syntax/default when for more information about given/when/default.

[5] When (when) it is an integer, add it to the list of processed scores.

[6] When it is a +, add the sum of the last two (rightmost) processed scores.

[7] When it is a C remove the last value in the list of processed scores (with pop).

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

[8] When it is a C, add the double of the last value in the list of processed scores.

[9] When it is something else (default), die with a suitable error message.

[10] Print the sum of the processed scores.

Running it:

$ ./final-score 5 2 C D +
30

$ ./final-score 5 -2 4 C D 9 + +
27

$ ./final-score 7 D D C + 3
45

$ ./final-score -- -5 -10 + D C +
-55

$ ./final-score 3 6 + D C 8 + D -2 C +
128

Looking good.

With verbose mode:

 $./final-score -v 5 2 C D +
: Round 1: Value:5 -> Scores: 5
: Round 2: Value:2 -> Scores: 5,2
: Round 3: Value:C -> Scores: 5
: Round 4: Value:D -> Scores: 5,10
: Round 5: Value:+ -> Scores: 5,10,15
30

$ ./final-score -v 5 -2 4 C D 9 + +
: Round 1: Value:5 -> Scores: 5
: Round 2: Value:-2 -> Scores: 5,-2
: Round 3: Value:4 -> Scores: 5,-2,4
: Round 4: Value:C -> Scores: 5,-2
: Round 5: Value:D -> Scores: 5,-2,-4
: Round 6: Value:9 -> Scores: 5,-2,-4,9
: Round 7: Value:+ -> Scores: 5,-2,-4,9,5
: Round 8: Value:+ -> Scores: 5,-2,-4,9,5,14
 27

$ ./final-score -v 7 D D C + 3
: Round 1: Value:7 -> Scores: 7
: Round 2: Value:D -> Scores: 7,14
: Round 3: Value:D -> Scores: 7,14,28
: Round 4: Value:C -> Scores: 7,14
: Round 5: Value:+ -> Scores: 7,14,21
: Round 6: Value:3 -> Scores: 7,14,21,3
45

$ ./final-score -v -- -5 -10 + D C +
: Round 1: Value:-5 -> Scores: -5
: Round 2: Value:-10 -> Scores: -5,-10
: Round 3: Value:+ -> Scores: -5,-10,-15
: Round 4: Value:D -> Scores: -5,-10,-15,-30
: Round 5: Value:C -> Scores: -5,-10,-15
: Round 6: Value:+ -> Scores: -5,-10,-15,-25
-55

 $ ./final-score -v 3 6 + D C 8 + D -2 C +
: Round 1: Value:3 -> Scores: 3
: Round 2: Value:6 -> Scores: 3,6
: Round 3: Value:+ -> Scores: 3,6,9
: Round 4: Value:D -> Scores: 3,6,9,18
: Round 5: Value:C -> Scores: 3,6,9
: Round 6: Value:8 -> Scores: 3,6,9,8
: Round 7: Value:+ -> Scores: 3,6,9,8,17
: Round 8: Value:D -> Scores: 3,6,9,8,17,34
: Round 9: Value:-1 -> Scores: 3,6,9,8,17,34,-2
: Round 10: Value:C -> Scores: 3,6,9,8,17,34
: Round 11: Value:+ -> Scores: 3,6,9,8,17,34,51
128

The program does not check if the non-integer operations are possible, i.e. if we have one or two values to work with (in the list of processed scores). But that is ok-ish, as the program will crash all by itself if we run out of values:

$ ./final-score D 0 0 0 
Effective index out of range. Is: -1, should be in 0..^Inf
  in block  at ./final-score line 16
  in sub MAIN at ./final-score line 9
  in block <unit> at ./final-score line 1

$ ./final-score + 0 0 0
Effective index out of range. Is: -1, should be in 0..^Inf
  in block  at ./final-score line 14
  in sub MAIN at ./final-score line 9
  in block <unit> at ./final-score line 1

$ ./final-score C 0 0 0
Cannot pop from an empty Array
  in block  at ./final-score line 15
  in sub MAIN at ./final-score line 9
  in block <unit> at ./final-score line 1

Actually thrown at:
  in sub MAIN at ./final-score line 11
  in block <unit> at ./final-score line 1

And that's it.