Perl Weekly Challenge: Week 395

Challenge 1:

Complex Number

You are given two complex numbers as string.

Write a script to return the multiplication of the given complex numbers.

Using the formula: (a + bi) x (c + di) = (ac - bd) + (ad + bc)i

Example 1
Input: $num1 = "1+1i", $num2 = "1+1i"
Output: "0+2i"
Example 2
Input: $num1 = "1+(-1)i", $num2 = "1+(-1)i"
Output: "0+(-2)i"
Example 3
Input: $num1 = "0+3i", $num2 = "0+4i"
Output: "-12+0i"
Example 4
Input: $num1 = "3+4i", $num2 = "3+(-4)i"
Output: "25+0i"
Example 5
Input: $num1 = "50+(-20)i", $num2 = "-10+30i"
Output: "100+1700i"

Ugh maths! but luckily Raku has a datatype for complex numbers so this should be easy right?

sub MAIN(

The actual arguments to the script (which we get from the command-line,) are of type ComplexStr which is a dual complex number snd string.

    ComplexStr $num1, #= a complex number
    ComplexStr $num2  #= a complex number
) {

We muitiply the two complex numbers and print the result.

    say $num1 * $num2;
}

Sadly, it's never that simple. This will work for examples 1 or 3 but fail for any example where the imaginary part of a number is negative, e.g. example 2. It will work if we change the input to 1-1i.

The following function will reformat the input to something more palatable to Raku.

sub normalize(Str $num) {
    $num.match(/
        $<real> = ['-'? \d+]
        \+
        [\(? $<imaginary> = ['-'? \d+] \)?]
        i
    /);
    return ($<real>,$<imaginary> < 0 ?? '-' !! '+', $<imaginary>.abs,'i').join;
}

MAIN() will now look like this:

say normalize($num1) * normalize($num2);

It's still not quite right. This gives 0-2i for example 2 and the spec expects 0+(-2)i. This function fixes the problem.

sub denormalize(Complex $num) {
    return $num.im < 0 ?? ($num.re, "+(", $num.im, ")i").join !! $num;
}

The final form of MAIN() will look like this:

say denormalize(normalize($num1) * normalize($num2));

(Full code on Github.)

For the Perl version, not having a builtin Complex type is actually a blessing. Instead of all that formatting to and fro, we just use the method suggested in the spec.

We still need a regular expression to parse the real and imaginary parts of a complex number from the string that contains it. I store it in a variable $complex so it can be reused.

my $complex = qr/
        (?<real> \-? \d+)
        \+
        \(? (?<imaginary> \-? \d+) \)?
        i
    /msx;

Then for both $num1 and $num2 we find the real and imaginary parts and assign them to four variables $a, $b, $c and $d.

$num1 =~ $complex;
my $a = $+{real};
my $b = $+{imaginary};

$num2 =~ $complex;
my $c = $+{real};
my $d = $+{imaginary};

Now we can apply the formula from the spec.

my $real = ($a * $c - $b * $d);
my $imaginary = ($a * $d + $b * $c);

One last touch is to put parentheses around the imaginary number if it is negative.

if ($imaginary < 0) {
    $imaginary = "($imaginary)";
}

We print the result.

say join q{}, ($real, '+', $imaginary, 'i');

(Full code on Github.)

Challenge 2:

Word Squares

You are given a list of equal lengths words.

Write a script to arrange a set of words into a grid that reading across any row gives the exact same word as reading down its corresponding column.

Example 1
Input: @words = ("MA", "AM", "MY", "ME")
Output: ("MA", "AM")

    M A
    A M
Example 2
Input: @words = ("SUN", "USE", "NET", "SEE", "TEN")
Output: ("SUN", "USE", "NET")

    S U N
    U S E
    N E T
Example 3
Input: @words = ("BOAT", "TEAL", "AREA", "TSAR", "OGRE")
Output: ("BOAT", "OGRE", "AREA", "TEAL")

    B O A T
    O G R E
    A R E A
    T E A L
Example 4
Input: @words = ("CARD", "AREA", "REAR", "DART")
Output: ("CARD", "AREA", "REAR", "DART")

    C A R D
    A R E A
    R E A R
    D A R T
Example 5
Input: @words = ("HEART", "TREND", "EMBER", "RESIN", "ABUSE")
Output: ("HEART", "EMBER", "ABUSE", "RESIN", "TREND")

    H E A R T
    E M B E R
    A B U S E
    R E S I N
    T R E N D

First we need to know the size of the word square we are going to make. As it is a square the number of row and columns in the grid will be the same and equal to the number of characters in the first word (or any word really; the spec says all words are of the same length.)

my $size = @words[0].chars;

We will also need to keep track of the grid and the words used to create it.

my @grid;
my %used;

We construct the word square by doing a recursive depth-first search in search()

sub search(Str $prefix) {

If the number or rows equals the number of columns, we are done.

    if @grid.elems == $size {
        return True;
    }

Otherwise, we look through the words for candidates which have the same prefix as the rows already chosen.

    for @words.grep({ $_.starts-with($prefix) }) -> $word {

If the word was already used, skip it and move on to the next one.

        if %used{$word}:exists {
            next;
        }

Otherwise add it to the list of used words.

        @grid.push($word);
        %used{$word} = True;

The prefix of the next row must match the next column of every previously chosen row.

        my $column = @grid.elems;
        my $nextPrefix = @grid.map({ $_.substr($column, 1) }).join;

If the next prefix is suitable, we use it to recursively call search() again.

        if search($nextPrefix) {
            return True;
        }

Otherwise we have to backtrack so another word can be tried in this position. We take the current word out of the grid and remove it from the used list.

        @grid.pop;
        %used{$word}:delete;
    }

If somehow we have reached here, no word square is possible so we just retuen False.

    return False;
}

We kick of the search with an empty string.

search(q{});

Once search() is finished, we should have a proper word square in @grid. We print it out in the manner suggested by the spec.

say q{(}, @grid.map({ "\"$_\"" }).join(q{, }), q{)};

(Full code on Github.)

This is the Perl version which is almost exactly similar to Raku.

my @words = @ARGV;

my $size = length $words[0];
my @grid;
my %used;

sub search($prefix) {
    if (scalar @grid == $size) {
        return true;
    }

    for my $word (grep { /^$prefix/ } @words) {
        if (exists $used{$word}) {
            next;
        }

        push @grid, $word;
        $used{$word} = true;

        my $column = scalar @grid;
        my $nextPrefix = join q{}, map { substr $_, $column, 1 } @grid;

        if (search($nextPrefix)) {
            return true;
        }

        pop @grid;
        delete $used{$word};
    }

    return false;
}

search(q{});

say q{(}, (join q{, }, map{ "\"$_\"" } @grid), q{)};

(Full code on Github.)