Time Balls
with Raku

by Arne Sommer

Time Balls with Raku

[333] Published 16. March 2025.

This is my response to The Weekly Challenge #312.

Challenge #312.1: Minimum Time

You are given a typewriter with lowercase english letters a to z arranged in a circle.

Typing a character takes 1 sec. You can move pointer one character clockwise or anti-clockwise.

The pointer initially points at a.

Write a script to return minimum time it takes to print the given string.

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

The pointer is at 'a' initially.
1 sec - type the letter 'a'
1 sec - move pointer clockwise to 'b'
1 sec - type the letter 'b'
1 sec - move pointer clockwise to 'c'
1 sec - type the letter 'c'
Example 2:
Input: $str = "bza"
Output: 7

The pointer is at 'a' initially.
1 sec - move pointer clockwise to 'b'
1 sec - type the letter 'b'
1 sec - move pointer anti-clockwise to 'a'
1 sec - move pointer anti-clockwise to 'z'
1 sec - type the letter 'z'
1 sec - move pointer clockwise to 'a'
1 sec - type the letter 'a'
Example 3:
Input: $str = "zjpc"
Output: 34

We can deduce from the examples that the moving of the pointer one step in either direction also takes 1 sec.

File: minimum-time
#! /usr/bin/env raku

unit sub MAIN (Str $str where $str ~~ /^<[a..z]>+$/, :v(:$verbose));  # [1]

my @chars = $str.comb;                                                # [2]
my $prev  = 'a';                                                      # [3]
my $time  = 0;                                                        # [4]

while @chars.elems                                                    # [5]
{
  my $current = @chars.shift;                                         # [6]
  my $diff    = abs( ord($prev) - ord($current) );                    # [7]

  if $diff > 13                                                       # [8]
  {
    $diff = 26 - $diff;                                               # [9]
    say ": $diff sec - Move pointer anti-clockwise from '$prev' \
      to '$current'." if $verbose;
  }
  elsif $diff && $verbose
  {
    say ": $diff sec - Move pointer clocwise from '$prev' to '$current'.";
  }

  say ": 1 sec - Type the letter '$current'." if $verbose;

  $time += ( $diff + 1 );                                            # [10]

  $prev = $current;                                                  # [11]
}

say $time;                                                           # [12]

[1] Ensure a string of lowercase lettes (a-z) only, with at least one (the +).

[2] Split the string into an array of characters with comb.

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

[3] The starting position of the pointer.

[4] The time usage, initially zero.

[5] As long as we have more characters to print,

[6] Get the next character.

[7] Get the number of steps, without the sign (if any). We do this by getting the codepoint (or ascii position) of the two letters (with ord), and subtracting those values.

[8] If the difference is more than 13, we will move anti-clockwise.

[9] The number of letters on the wheel is 26, so subtracting the difference from this value gives the number of steps in the anti-clockwise direction.

[10] Add the number of steps (1 second each) and the printing (also 1 second).

[11] Prepare for the next iteration.

[12] Print the result.

Running it:

$ ./minimum-time abc
5

$ ./minimum-time bza
7

$ ./minimum-time zjpc
34

Looking good.

With verbose mode:

$ ./minimum-time -v abc
: 1 sec - Type the letter 'a'.
: 1 sec - Move pointer clocwise from 'a' to 'b'.
: 1 sec - Type the letter 'b'.
: 1 sec - Move pointer clocwise from 'b' to 'c'.
: 1 sec - Type the letter 'c'.
5

$ ./minimum-time -v bza
: 1 sec - Move pointer clocwise from 'a' to 'b'.
: 1 sec - Type the letter 'b'.
: 2 sec - Move pointer anti-clockwise from 'b' to 'z'.
: 1 sec - Type the letter 'z'.
: 1 sec - Move pointer anti-clockwise from 'z' to 'a'.
: 1 sec - Type the letter 'a'.
7

$ ./minimum-time -v zjpc
: 1 sec - Move pointer anti-clockwise from 'a' to 'z'.
: 1 sec - Type the letter 'z'.
: 10 sec - Move pointer anti-clockwise from 'z' to 'j'.
: 1 sec - Type the letter 'j'.
: 6 sec - Move pointer clocwise from 'j' to 'p'.
: 1 sec - Type the letter 'p'.
: 13 sec - Move pointer clocwise from 'p' to 'c'.
: 1 sec - Type the letter 'c'.
34

Challenge #312.2: Balls and Boxes

There are $n balls of mixed colors: red, blue or green. They are all distributed in 10 boxes labelled 0-9.

You are given a string describing the location of balls.

Write a script to find the number of boxes containing all three colors. Return 0 if none found.

Example 1:
Input: $str = "G0B1R2R0B0"
Output: 1

The given string describes there are 5 balls as below:
Box 0: Green(G0), Red(R0), Blue(B0) => 3 balls
Box 1: Blue(B1) => 1 ball
Box 2: Red(R2) => 1 ball
Example 2:
Input: $str = "G1R3R6B3G6B1B6R1G3"
Output: 3

The given string describes there are 9 balls as below:
Box 1: Red(R1), Blue(B1), Green(G1) => 3 balls
Box 3: Red(R3), Blue(B3), Green(G3) => 3 balls
Box 6: Red(R6), Blue(B6), Green(G6) => 3 balls
Example 3:
Input: $str = "B3B2G1B3"
Output: 0

Box 1: Green(G1) => 1 ball
Box 2: Blue(B2)  => 1 ball
Box 3: Blue(B3)  => 2 balls
File: balls-and-boxes
#! /usr/bin/env raku

subset RGB where /^(<[RGB]><[0..9]>)+$/;          # [1]

unit sub MAIN (RGB $str is copy, :v(:$verbose));  # [1a]

my %boxes;                                        # [2]

while $str.chars                                  # [3]
{
  my $ball   = $str.substr(0,2);                  # [4]
  my $colour = $str.substr(0,1);                  # [4a]
  my $box    = $str.substr(1,1);                  # [4b]
  $str       = $str.substr(2);                    # [5]

  %boxes{$box}{$colour}++;                        # [6]

  say ":Ball $ball (Colour $colour, Box $box)" if $verbose;
}


say %boxes>>.elems.grep( *.value == 3 ).elems;   # [7]

[1] A custom type for the two-character box specifications (with subset), with at least one box. And apply it to the input [1a].

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

[2] We will build up the boxes here, as a two dimentional hash. The first level is the box id (number 0-9) and the second is the colour identifier.

[3] As long as we have more balls to process.

[4] Get the ball, the first two characters, with substr. This one is used by verbose mode only, so can be skipped. Get the colour [4a] and the box number [4b].

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

[5] Get rid of the first two characters, i.e. the ball that we have just processed.

[6] Add the new ball to the hash.

The %boxes hash looks like this, for the first example:

[7] For each box (%boxes>>) use elems to get the number of colours they contain. (Non-existing colours will not be present in the hash.) Then use grep to only keep the boxes with three elements (different colours). And finally we count them with elems and print the result.

There is more than one way to skin this cat (or hash). Here is a very convoluted one:

%boxes.map({ + %boxes{ .key }.elems == 3}).sum.say;

Running it:

$ ./balls-and-boxes G0B1R2R0B0
1

$ ./balls-and-boxes G1R3R6B3G6B1B6R1G3
3

$ ./balls-and-boxes B3B2G1B3
0

Looking good.

With verbose mode:

$ ./balls-and-boxes -v G0B1R2R0B0
:Ball G0 (Colour G, Box 0)
:Ball B1 (Colour B, Box 1)
:Ball R2 (Colour R, Box 2)
:Ball R0 (Colour R, Box 0)
:Ball B0 (Colour B, Box 0)
1

$ ./balls-and-boxes -v G1R3R6B3G6B1B6R1G3
:Ball G1 (Colour G, Box 1)
:Ball R3 (Colour R, Box 3)
:Ball R6 (Colour R, Box 6)
:Ball B3 (Colour B, Box 3)
:Ball G6 (Colour G, Box 6)
:Ball B1 (Colour B, Box 1)
:Ball B6 (Colour B, Box 6)
:Ball R1 (Colour R, Box 1)
:Ball G3 (Colour G, Box 3)
3

$ ./balls-and-boxes -v B3B2G1B3
:Ball B3 (Colour B, Box 3)
:Ball B2 (Colour B, Box 2)
:Ball G1 (Colour G, Box 1)
:Ball B3 (Colour B, Box 3)
0

And that's it.