Perl Weekly Challenge: Week 379

Challenge 1:

Reverse String

You are given a string.

Write a script to reverse the given string without using standard reverse function.

Example 1
Input: $str = ""
Output: ""
Example 2
Input: $str = "reverse the given string"
Output: "gnirts nevig eht esrever"
Example 3
Input: $str = "Perl is Awesome"
Output: "emosewA si lreP"
Example 4
Input: $str = "v1.0.0-Beta!"
Output: "!ateB-0.0.1v"
Example 5
Input: $str = "racecar"
Output: "racecar"

If we can't use reverse() (or .flip() in Raku) it's not a big deal. We can achiece the same result splitting the string into individual characters and placing them into a LIFO (Last In First Out) Stack data structure. When we concatenate the contents of the stack, we will have the reversed version of the original string. While neither Perl nor Raku have a native Stack data type, we can easily emulate it in both languages with lists. In fact our solution will be a one-liner in both languages.

Here's the Raku version. It's been split up into multiple lines for clarity.

First we define our "stack".

my @a;

Next we split up the first command-line argument into individual characters with .comb() and for each character...

for @*ARGS[0].comb -> $c {

...we add it to the beginning of the list with .unshift(). This is equivalent to a "push" operation on a stack. (Both Raku and Perl have push() and pop() functions but they work on the end of a list.)

    @a.unshift($c)
};

Finally we .join() up the stack and print it out with .say().

@a.join.say

(Full code on Github.)

This is the Perl version. It is almost directly equivalent to the Raku version.

my @a;

for my $c (split//, shift) {
    unshift @a, $c
}

say join q{}, @a

(Full code on Github.)

Challenge 2:

Armstrong Number

You are given two integers, $base and $limit.

Write a script to find all Armstrong numbers in base $base that are less than $limit.

If raising each of the digits of a nonnegative integer to the power of the total number of digits, then taking the sum, equals the original number, it is an Armstrong number.

Example 1
Input: $base = 10, $limit = 1000
Output: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407)
Example 2
Input: $base = 7, $limit = 1000
Output: (0, 1, 2, 3, 4, 5, 6, 10, 25, 32, 45, 133, 134, 152, 250)
Example 3
Input: $base = 16, $limit = 1000
Output: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 342, 371, 520, 584, 645)

I produced a solution that got the correct answer for example 1 vety quickly but it did not work properly for the other examples where $base does not equal 10.

First the preliminaries:

We create a list to hold the results.

my @armstrongs;

Then we iterate through the integers from 0 to, upto but not including, $limit.

for 0 ..^ $limit -> $n {

Now the tricky bit. We have to convert $n which is a base-10 number to base-$base. This is easy to do with .base().

    my $N = $n.base($base);

And we find the length of that number with .chars.

    my $power = $N.chars;

Then as the spec directs, using .comb() and .map(), we take each digit of $N and raise it to the power of the total number of digits (i.e. $power), .sum() all these values and compare them to N to see if it is an Armstrong number. It didn't work.

After some experimentation, I figured out what I had missed. Each digit of $N has to be converted back to base-10 (for which Raku has the handy .parse-base()) and compare the sum of those numbers to "the original number" which is $n not $N. If they are equal, we can add $n to our list of Armstrong numbers.

    if $N.comb.map({ $_.parse-base($base) ** $power }).sum == $n {
        @armstrongs.push($n);
    }
}

After all that exitement, all that remains is to .join() up the Armstrong numbers with commas and spaces and print them out with .say().

@armstrongs.join(q{, }).say;

(Full code on Github.)

For Perl, we need replacements for .base(), .parse-base() and .sum()

sum() has been used in many previous challenges so was quickly cut and pasted into the script. I thought I might have to write code for the other two. It's a good thing I checked first because way back in PWC 2, there was a challenge to convert to and from base 35. I was able to generalize and modernize that.

fromBase() takes two parameters, a $number in a certain $base and returns the value of that number in base-10.

sub fromBase($number, $base) {

I think I should have called this $magnitude but I'm fuzzy on math terminology. In any case, $scale reprresents the number of digits execept units.

    my $scale = (length $number) - 1;

We need to map digits in this base to their base-10 values. For base 0 to 9 there is a one to one coresspondance but for higher bases which use letters to represent digits, the base-10 value cannot be represented by a single character. so we have a Hash that maps digts to values. The mapping is done by the second line. For base-7, the keys would be 0-6 and the values 0-6 but for Hexadecimal (base-16,) the keys would be 0-F and the values 0-15.

The way this is written, we can only deal with bases 1-36 (and 1 doesn't really count.) In professional code I would have done some parameter checking but I haven't here.

    my %digits;
    @digits{0..9, 'A'..'Z'} = 0 .. $base - 1;

Here is where we will store the result of the conversion.

    my $result;

Now we split $numberinto individual digits...

    for my $digit (split //, $number) {

...and look up its' value in %digits. I msde a small effort to do some error checking but it really should be more rigorous in professional code.

        my $base10 = $digits{uc $digit} // die "malformed base-$base number\n";

This value is multiplied by the $base, raised to the power of $scale and added to $result.

        $result += $base10 * $base ** $scale;

As we move to the next decimal place to the right, $scale is decremented.

        $scale--;
    }

Finally, the converted number in$result is returned.

    return $result;
}

toBase() also takes two parameters, a $number in base-10 and $base to convert it into. It returns tha value of that number in base-$base.

sub toBase($number, $base) {

Again we need the digits used in in $base but this time, the values assoxiated with each digit increase sequentially so we can make @digits a List and just use the index of each element as the value and the element itself as the key.

    my @digits = (0 .. 9, 'A' .. 'Z')[0 .. $base - 1];

The result is also stored as a list.

    my @result;

If $number is 0, we can skip all the other processing.

    if ($number == 0) {
        return 0;
    }

If not, while it is greater 0...

    while ($number > 0) {

Using the modulo operator % we find the remainder when dividin $number by $base. This will give the value of the lowest place digit.

        my $digit = int($number % $base);

The digit is used as a key into @digits and the value is added to the beginning of the @result.

        unshift @result, $digits[$digit];

The digit is removed from $number and the loop continues.

        $number = int($number / $base);
    }

Finally, we .join() the @result back together into a single string and return it.

    return join '', @result;
}

Now the rest of the script is just like Raku.

my @armstrongs;

for my $n (0 .. $limit - 1) {
    my $N = toBase($n, $base);
    my $power = length $N;

    if (sum(map { fromBase($_, $base) ** $power } split //, $N) == $n) {
        push @armstrongs, $n;
    }
}

say join q{, }, @armstrongs;

(Full code on Github.)