Rotating Leader with Raku and Perl

by Arne Sommer

Rotating Leader with Raku and Perl

[92] Published 18. September 2020.

This is my response to the Perl Weekly Challenge #078.

Challenge #078.1: Leader Element

You are given an array @A containing distinct integers.

Write a script to find all leader elements in the array @A. Print (0) if none found.

An element is leader if it is greater than all the elements to its right side.

Example 1
Input: @A = (9, 10, 7, 5, 6, 1)
Output: (10, 7, 6, 1)


Example 2
Input: @A = (3, 4, 5)
Output: (5)
File: leader-element
#! /usr/bin/env raku

unit sub MAIN (*@A where @A.elems && all(@A) ~~ Int);  # [1]

my @B;

while (@A)                                             # [2a]
{
  my $a = @A.shift;                                    # [2]
  @B.push: $a if $a > all(@A);                         # [3]
}

say '(' ~ @B.join(', ')  ~ ')';                        # [4]

[1] We cannot slap a type constraint on a slurpy argument, so we use a where clause instead. We insist on at least one element, as slurpies accept none as well, and they must all be integers using an all junction.

[2] Remove one element at a time from the front of the array, until we are done.

[3] Add the removed element to the result, if it is higher than what is left in the array

[4] Print it.

See docs.raku.org/routine/all for more information about all, and docs.raku.org/type/Junction for more information about Junctions.

Running it with the values given in the challenge:

$ ./leader-element 9 10 7 5 6 1
(10, 7, 6, 1)

$ ./leader-element 3 4 5
(5)

The last element in @A is always shown, so we get this:

./leader-element 91
(91)

./leader-element 0
(0)

This shows two things. First that the «Print (0) if none found» part of the description does not make sense. Except if we allow an empty array. Which I have chosen not to. Second that we can get that (0) from a perfectly legal input array.

A Perl Version

This is pretty much a straight forward translation from the Raku version:

File: leader-element-perl
#! /usr/bin/env perl

use Perl6::Junction 'all';
use feature 'say';

my @A = @ARGV;

die "Please specify at least one element" unless @A;

die "Integers only" unless all(@A) == qr/^\d+$/;

my @B;

while (@A)
{
  my $a = shift @A;
  push(@B, $a) if $a > all(@A);  
}

say '(' . join(', ', @B)  . ')';

It works exactly like the Raku version:

$ ./leader-element-perl 9 10 7 5 6 1
(10, 7, 6, 1)

$ ./leader-element-perl 3 4 5
(5)

$ ./leader-element-perl 91
(91)

$ ./leader-element-perl 0
(0)

Challenge #078.2: Left Rotation

You are given array @A containing positive numbers and @B containing one or more indices from the array @A.

Write a script to left rotate @A so that the number at the first index of @B becomes the first element in the array. Similary, left rotate @A again so that the number at the second index of @B becomes the first element in the array.

Example 1
Input:
    @A = (10 20 30 40 50)
    @B = (3 4)
Explanation
) We left rotate the 3rd index element (40) in the @A to make it 0th
   index member in the array.
        [40 50 10 20 30]

b) We left rotate the 4th index element (50) in the @A to make it 0th
    index member in the array.
        [50 10 20 30 40]

Output:
    [40 50 10 20 30]
    [50 10 20 30 40]

Example 2
Input:
    @A = (7 4 2 6 3)
    @B = (1 3 4)
Explanation
a) We left rotate the 1st index element (4) in the @A to make it 0th
    index member in the array.
        [4 2 6 3 7]

b) We left rotate the 3rd index element (6) in the @A to make it 0th
    index member in the array.
        [6 3 7 4 2]

c) We left rotate the 4th index element (3) in the @A to make it 0th
    index member in the array.
        [3 7 4 2 6]

Output:
    [4 2 6 3 7]
    [6 3 7 4 2]
    [3 7 4 2 6]

I'll start with a very short program, as it is funny to do so after the rather elaborate explanation:

File: left-rotation-1
#! /usr/bin/env raku

my @A = (10, 20, 30, 40, 50);
my @B = (3, 4);

@B.map({ @A.rotate($_).say });   # [1]

[1] rotate gives a rotated copy, leaving the original array unchanged. So the next iteration (so to speak) of the map loop starts with the same @A. The values in the @B array are the number of steps to rotate, so we can use them directly, in the map.

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

Running it:

$ ./left-rotation-1
(40 50 10 20 30)
(50 10 20 30 40)

And a second program, for the second example:

File: left-rotation-2
#! /usr/bin/env raku

my @A = (7, 4, 2, 6, 3);
my @B = (1, 3, 4);

@B.map({ @A.rotate($_).say });

Running that one as well:

$ ./left-rotation-2
(4 2 6 3 7)
(6 3 7 4 2)
(3 7 4 2 6)

The challenge specified the values in the arrays (the input) without separating commas. That is probably an omission, as they are there in the first challenge. The output is without commas, but with the default parens instead of square brackets. I'll get back to that.

A more flexible approach, where the user can specify the two arrays, is certainly better:

File: left-rotation
#! /usr/bin/env raku

unit sub MAIN (Str $A, Str $B);                                       # [1]

my @A = $A.words;
my @B = $B.words;

die '@A must be positive numbers only' unless all(@A) > 0;            # [1]
die '@B must be legal indices only'   unless 0 <= all(@B) <= @A.end;  # [2]

@B.map({ say '[' ~ @A.rotate($_).join(' ') ~ ']' });                  # [3]

[1] Two arrays on the command line does not work. But we can use quotes to group them, and get the individual values with words afterwards.

[2] Ensure that all the elements in @A are positive. Any non-numeric values will cause a runtime exception, which is ok.

[3] Ensure that all the elements in @B are legal indices. Non-integers will work (be truncated), which is ok-ish.

[4] The challenge wanted square brackets on the output, so here they are.

Running it:

$ ./left-rotation "10 20 30 40 50" "3 4"
[40 50 10 20 30]
[50 10 20 30 40]

$ ./left-rotation "7 4 2 6 3" "1 3 4"
[4 2 6 3 7]
[6 3 7 4 2]
[3 7 4 2 6]

$ ./left-rotation "7 4 2 6 3" "1 3.14 4"
[4 2 6 3 7]
[6 3 7 4 2]
[3 7 4 2 6]

$ ./left-rotation "7 4 2 6 A" "1 3 4"
Cannot convert string to number: base-10 number must begin with valid digits
  or '.' in ...

$ ./left-rotation "10 20 30 40 50" "1 2 3 4 5"
@B must be legal indices only in ...

A Perl Version

Another straight forward translation from the Raku version:

File: left-rotation-perl
#! /usr/bin/env perl

use feature 'say';
use feature 'signatures';
use Perl6::Junction 'all';

no warnings qw(experimental::signatures);


my @A = split(" ", $ARGV[0]); 
my @B = split(" ", $ARGV[1]);  

die 'Specify @A' unless @A;
die 'Specify @B' unless @B;

die '@A must be positive numbers only' unless all(@A) > 0;
die '@B must be legal indices only'    unless 0 <= all(@B) && all(@B) <= $#A;

map { say '[' . join(' ', do_rotate($_, @A)) . ']' } @B;

sub do_rotate ($length, @array)    # [1]
{
  push(@array, shift @array) for 1..$length;
    
  return @array;
}

[1] I needed a rotate function that leaves the original array unchanged, so had to write this procedure.

It gives the same output as the Raku version:

$ ./left-rotation-perl "10 20 30 40 50" "3 4"
[40 50 10 20 30]
[50 10 20 30 40]

$ ./left-rotation-perl "7 4 2 6 3" "1 3 4"
[4 2 6 3 7]
[6 3 7 4 2]
[3 7 4 2 6]

$ ./left-rotation-perl "7 4 2 6 3" "1 3.14 4"
[4 2 6 3 7]
[6 3 7 4 2]
[3 7 4 2 6]

$ ./left-rotation-perl "10 20 30 40 50" "1 2 3 4 5"
@B must be legal indices only at ./left-rotation-perl line 16.

$ ./left-rotation-perl "7 4 2 6 A" "1 3 4"
@A must be positive numbers only at ...

And that's it.