The Common Index
with Raku and Perl

by Arne Sommer

The Common Index with Raku and Perl

[202] Published 18. September 2022.

This is my response to The Weekly Challenge #182.

Challenge #182.1: Max Index

You are given a list of integers.

Write a script to find the index of the first biggest number in the list.

Example:
Input: @n = (5, 2, 9, 1, 7, 6)
Output: 2 (as 3rd element in the list is the biggest number)


Input: @n = (4, 2, 3, 1, 5, 0)
Output: 4 (as 5th element in the list is the biggest number)
File: max-index
#! /usr/bin/env raku

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

my $max = @n.max;                                          # [2]

for ^@n.elems -> $index                                    # [3]
{
  if @n[$index] == $max                                    # [4]
  {
    say $index;                                            # [5]
    last;                                                  # [5a]
  }
}

[1] Ensure that all the elements (of which there are one or more) are integers.

[2] Get the biggest number (with max).

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

[3] Iterate over the indices in the array.

[4] Do we have the highest number at this position in the array?

[5] If so, print the index and exit the loop [5b].

Running it:

$ ./max-index 5 2 9 1 7 6
2

$ ./max-index 4 2 3 1 5 0
4

Looking good.

A Perl Version

Let us do this slightly differently, with a single pass of the array. (The Raku version had on average one and a half pass; the first one courtesy of max).

Note that I have not implemented a check on input in the version; neither the type nor the number of them.

File: max-index-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';

my @n = @ARGV;          # [1]

my $max   = $n[0];      # [2]
my $index = 0;          # [3]

for my $i (1 .. $#n)    # [4]
{
  if ($n[$i] > $max)    # [5]
  {
    $max   = $n[$i];    # [6]
    $index = $i;        # [6a]
  }
}

say $index;             # [7]

[1] Get (copy) the first value in the array.

[2] The maximum value, initialised to the first value.

[3] The index of the maximum value, also initialised to the first value.

[4] Iterate over the indices in the array, excluding the first one (as it has been done already).

[5] Is the new value higfher than the recorded maximum value?

[6] If so, update the maximum value and the index [6a].

[7] Print the index.

Running it gives the same result as the Raku version:

$ ./max-index-perl 5 2 9 1 7 6
2

$ ./max-index-perl 4 2 3 1 5 0
4

Challenge #182.2: Common Path

Given a list of absolute Linux file paths, determine the deepest path to the directory that contains all of them.

Example:
Input:
    /a/b/c/1/x.pl
    /a/b/c/d/e/2/x.pl
    /a/b/c/d/3/x.pl
    /a/b/c/4/x.pl
    /a/b/c/d/5/x.pl

Ouput:
    /a/b/c

It does not really matter that this is file paths, right? They are just strings. Let us start programming before we have time to think about the soundness of this assumption...

File: common-path-first
#! /usr/bin/env raku

unit sub MAIN ($file where $file.IO.f && $file.IO.r = "paths.txt");  # [1]

my @paths = $file.IO.lines;                                          # [2]

my $first = @paths.shift;                                            # [3]

while @paths                                                         # [4]
{
  my $next = @paths.shift;                                           # [5]

  $first = common($first, $next);                                    # [6]
}

say $first;                                                          # [7]

sub common ($a, $b)                                                  # [6a]
{
  my $length = min($a.chars, $b.chars);                              # [8]

  for ^$length -> $index                                             # [9]
  {
    return $a.substr(0, $index)
      unless $a.substr($index,1) eq $b.substr($index,1);             # [10]
  }

  return $a.substr(0, $length);                                      # [11]
}

[1] Ensure that the filename is a file (.f), and that we can read it (.r). Note the default value.

[2] Read the paths from the file.

[3] Get the first path.

[4] As long as there are paths left,

[5] Get the next one.

[6] Set the first one to the common value of the first and second.

[7] Print the result.

[8] Get the minimum length, i.e. the breakout point for the comparison.

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

[9] Iterate over the indices, up to the breakout point.

[10] Return the string up to the previous character, if the current ones differ.

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

[11] They do not differ (up to the breakout point). Return the string up to that point.

Running it:

$ ./common-path-first
/a/b/c/

Looking sort of good. The trailing / should not be there, according to the challenge.

But...

We have a bigger problem.

Let me show it with another set of paths:

File: paths2.txt
/a/b/ssssssss
/a/b/ssxxxxxxx.pl
/a/b/sssssss.pl
/a/b/sssss.pl
/a/b/sssssss.pl

Running the program with this set:

$ ./common-path-first paths2.txt
/a/b/ss

The trailing «ss» is part of the filename, and not the path. The challenge wanted the path.

This is actually easy to fix:

File: common-path (changes only)
$first ~~ /(.*)\//; say $0.Str;  # [1]

[1] Up to the last /. Note the .Str to stringify the result, which is a Match object. Match objects stringify with funny brackets, e.g. 「/a/b/c」.

Running it:

$ ./common-path
/a/b/c

$ ./common-path paths2.txt
/a/b

Perl

This is a straight forward translation of the Raku version.

File: common-path-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use File::Slurp;
use feature 'signatures';

no warnings 'experimental::signatures';

my $file  = shift(@ARGV) || "paths.txt";
my @paths = read_file($file);
my $first = shift(@paths);

while (@paths)
{
  my $next = shift(@paths);

  $first = common($first, $next);
}

$first =~ /(.*)\//; say $1;                                        # [1]

sub common ($a, $b)
{
  my $length = length($a) < length($b) ? length($a) : length($b);  # [2]

  for my $index (0 .. $length -1)
  {
    return substr($a, 0, $index)
      unless substr($a, $index, 1) eq substr($b, $index, 1);
  }

  return substr($a, 0, $length);
}

[1] The first match is $1 in Perl (as opposed to $0 in Raku).

[2] I could have used the «max» routine from «List::Util» as I have done several times before, but doing it by hand is ok - as we only have two values.

Running it gives the same result as the Raku version:

$ ./common-path-perl 
/a/b/c

$ ./common-path paths2.txt
/a/b

And that's it.