This is my response to The Weekly Challenge #336.
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))
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
+
, C
or D
.
+
adds the sum of previous two scores. The score C
invalidates the previous score. The score D
will double the
previous score.
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.