Weekly Challenge: Perl has classes now 👍
Weekly Challenge 334
Each week Mohammad S. Anwar sends out The Weekly Challenge, a chance for all of us to come up with solutions to two weekly tasks. My solutions are written in Python first, and then converted to Perl. It’s a great way for us all to practice some coding.
Task 1: Range Sum
Task
You are given a list integers and pair of indices..
Write a script to return the sum of integers between the given indices (inclusive).
My solution
This is relatively straight forward, so doesn’t require too much explanation. For the input from the command line, I take the x
and y
values from the last two numbers, and the rest is the list of integers.
I first check that the x
and y
values are valid (between 0
and one less than the length of the integers) and that x
is less than or equal to y
.
I then use the sum
function and list slicing to get the sum of the integers. Since the second value in Python is the stop value, I add one to achieve the desired result.
def range_sum(ints: list, x: int, y: int) -> int:
if x < 0 or x >= len(ints):
raise ValueError(f"x must be between 0 and len(ints) - 1, got x")
if y < 0 or y >= len(ints):
raise ValueError(f"y must be between 0 and len(ints) - 1, got y")
if x > y:
raise ValueError(
f"x must be less than or equal to y, got x=x, y=y")
return sum(ints[x:y + 1])
The Perl solution follows the same logic, slightly different syntax
use List::Util 'sum';
sub main (@ints)
my $y = pop @ints; # Last element is y
my $x = pop @ints; # Second last element is x
if ( $x < 0 or $x > $#ints )
die "x must be between 0 and " . $#ints . ", got $x\n";
if ( $y < 0 or $y > $#ints )
die "y must be between 0 and " . $#ints . ", got $y\n";
if ( $x > $y )
die "x must be less than or equal to y, got x=$x, y=$y\n";
return sum( @ints[ $x .. $y ] );
Examples
$ ./ch-1.py -2 0 3 -5 2 -1 0 2
1
$ ./ch-1.py 1 -2 3 -4 5 1 3
-3
$ ./ch-1.py 1 0 2 -1 3 3 4
2
$ ./ch-1.py -5 4 -3 2 -1 0 0 3
-2
$ ./ch-1.py -1 0 2 -3 -2 1 0 2
1
Task 2:
Task
You are given current location as two integers: x
and y
. You are also given a list of points on the grid.
A point is considered valid if it shares either the same x-coordinate
or the same y-coordinate
as the current location.
Write a script to return the index of the valid point that has the smallest Manhattan distance to the current location. If multiple valid points are tied for the smallest distance, return the one with the lowest index. If no valid points exist, return -1
.
The Manhattan distance between two points (x1, y1)
and (x2, y2)
is calculated as: |x1 - x2| + |y1 - y2|
.
My solution
For input from the command line, I take the first two values as x
and y
and the remaining values as the x
and y
for each point.
Python 3.7 introduced dataclasses which are a special class that are designed to hold data. For this task, I create a Point
class that hold values for x
and y
. I also create a magic method for subtracting two points to get the Manhattan distance.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
def __sub__(self, other: 'Point') -> int:
return abs(self.x - other.x) + abs(self.y - other.y)
This then makes the function a lot more straight forward. I convert x
and y
into a Point
object called starting_point
. I also convert the points_list
into a list of Point
objects called points
.
I set the value min_distance
to None
, and min_index
to -1
.
I then iterate through the points
. If they don’t share the x
and y
with the starting_point
, I skip it. I calculate the Manhattan distance in a variable called distance
. If this is less than the current min_distance
value, I update the min_distance
and min_index
values.
def shortest_index(x: int, y: int, points_list: list[list[int]]) -> int:
starting_point = Point(x, y)
points = [Point(*point) for point in points_list]
min_distance = None
min_index = -1
for index, point in enumerate(points):
if point.x != starting_point.x and point.y != starting_point.y:
continue
distance = starting_point - point
if min_distance is None or distance < min_distance:
min_distance = distance
min_index = index
return min_index
For the Perl solution, I spent a bit of time looking at the new class statement that has been introduced in Perl 5.38. Yes, Perl has had OOP class since Perl 5.4 (using the bless statement) and more recently Moose/Moo, but this task gave me time to look at native classes.
I create a class called Point
. Unsurprisingly, Copilot got a little confused and tried to tried write Moose-style classes.
use feature 'class';
no warnings 'experimental::class';
class Point
field $x : param;
field $y : param;
method x return $x;
method y return $y;
method distance_to($other)
return abs( $x - $other->x ) + abs( $y - $other->y );
The main
function follows the same logic as the Python code.
sub main (@ints)
my $x = shift @ints; # First element is x
my $y = shift @ints; # Second element is y
my @points_list = ();
for ( my $i = 0 ; $i < $#ints ; $i += 2 )
push @points_list, [ $ints[$i], $ints[ $i + 1 ] ];
my $starting_point = Point->new( x => $x, y => $y );
my @points = map Point->new( x => $_->[0], y => $_->[1] ) @points_list;
my $min_distance = undef;
my $min_index = -1;
for my $index ( 0 .. $#points )
say $min_index;
Classes are still experimental in Perl 5.38 and 5.40, and it doesn’t seem possible to overload functions like you can with the old blessed functions. But definitely a nice addition for programmers of other languages.
Examples
$ ./ch-2.py 3 4 1 2 3 1 2 4 2 3
2
$ ./ch-2.py 2 5 3 4 2 3 1 5 2 5
3
$ ./ch-2.py 1 1 2 2 3 3 4 4
-1
$ ./ch-2.py 0 0 0 1 1 0 0 2 2 0
0
$ ./ch-2.py 5 5 5 6 6 5 5 4 4 5
0