Wordly Array with Raku

by Arne Sommer

Wordly Array with Raku

[98] Published 25. October 2020.

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

Challenge #083.1: Words Length

You are given a string $S with 3 or more words.

Write a script to find the length of the string except the first and last words ignoring whitespace.

Example 1
Input: $S = "The Weekly Challenge"

Output: 6
Example 2
Input: $S = "The purpose of our lives is to be happy"

Output: 23

File: words-length
#! /usr/bin/env raku

unit sub MAIN (Str $S where $S.words.elems >= 3,  # [1]
               :v($verbose));

my @words = $S.words;                             # [2]
my $s     = @words[1.. @words.end -1].join;       # [3]

say ": $s" if $verbose;

say $s.chars;                                     # [4]

[1] The where clause ensures that we get at least three words.

[2] Split the string into words, according to what words considers words...

[3] Get all the words, except the first and last, and join them together to a single "word".

[4] Print the length of that "word".

See my Common Frequency with Raku article, or docs.raku.org/routine/words for more information about words.

Running it on the examples:

$ ./words-length "The Weekly Challenge"
6

$ ./words-length "The purpose of our lives is to be happy"
23

With verbose mode:

$ ./words-length -v "The Weekly Challenge"
: 'Weekly'
6

$ ./words-length -v "The purpose of our lives is to be happy"
: 'purposeofourlivesistobe'
23

A string with too few words causes a usage error:

$ ./words-length -v "The Challenge"
Usage:
  ./words-length [-v=<Any>] <S>

Challenge #083.2: Flip Array

You are given an array @A of positive numbers.

Write a script to flip the sign of some members of the given array so that the sum of the all members is minimum non-negative.

Given an array of positive elements, you have to flip the sign of some of its elements such that the resultant sum of the elements of array should be minimum non-negative (as close to zero as possible). Return the minimum no. of elements whose sign needs to be flipped such that the resultant sum is minimum non-negative.

Example 1
Input: @A = (3, 10, 8)
Output: 1
Explanation
Flipping the sign of just one element 10 gives the result 1
i.e. (3) + (-10) + (8) = 1
Example 2
Input: @A = (12, 2, 10)
Output: 1
Explanation
Flipping the sign of just one element 12 gives the result 0
i.e. (-12) + (2) + (10) = 0

This time I'll start with the output of the verbose mode, and explain it from there.

$ ./flip-array -v 3 10 8
: Candidate [3, 10, 8] with sum 21       # [1]
: Candidate [3, 10, -8] with sum 5
: Candidate [3, -10, 8] with sum 1
: Candidate [3, -10, -8] with sum -15
: Candidate [-3, 10, 8] with sum 15
: Candidate [-3, 10, -8] with sum -1
: Candidate [-3, -10, 8] with sum -5
: Candidate [-3, -10, -8] with sum -21
: Lowest non-negative sum: 1             # [2]
: Match 0: [3, -10, 8] with 1 flip(s)    # [3]
: Flips: [1]                             # [4]
1                                        # [5]

$ ./flip-array -v 12 2 10
: Candidate [12, 2, 10] with sum 24      # [1]
: Candidate [12, 2, -10] with sum 4
: Candidate [12, -2, 10] with sum 20
: Candidate [12, -2, -10] with sum 0
: Candidate [-12, 2, 10] with sum 0
: Candidate [-12, 2, -10] with sum -20
: Candidate [-12, -2, 10] with sum -4
: Candidate [-12, -2, -10] with sum -24 
: Lowest non-negative sum: 0             # [2]
: Match 0: [12, -2, -10] with 2 flip(s)  # [3]
: Match 1: [-12, 2, 10] with 1 flip(s)
: Flips: [2, 1]                          # [4]
1                                        # [5]

[1] The first step («Candidate …») is computing all the permutaions we can get by flipping the sign on the values. Note the sum at the end of the line. We are storing the candidates in a hash, with the sum as the key.

[2] The second step («Lowest …») is computing the lowest non-negative sum.

[3] In the third step we iterate over the candidates with the number of flips from the previous step, computing the number of flips on each one.

[4] The fourth step gives us a list of the number of flips.

[5] Print the lowest number (of flips).

File: flip-array (partial)
#! /usr/bin/env raku

subset Positive of Numeric where * > 0;                            # [11]

unit sub MAIN (*@A where @A.elems > 0 && all(@A) ~~ Positive,      # [11]
               :v($verbose));

my %sum;                                                           # [12]

do_it( (), @A);                                                    # [13]

my $lowest-sum = %sum.keys.grep( * >= 0).min;                      # [14]

say ": Lowest non-negative sum: $lowest-sum" if $verbose;

my @flip;                                                          # [15]

for @(%sum{$lowest-sum}) -> @candidate                             # [16]
{
  state $index = 0;                                                # [17]
  @flip[$index] = @candidate.grep( * < 0).elems;                   # [18]
  say ": Match $index: [{ @candidate.join(", ") }] with @flip[$index] flip(s)"
    if $verbose;

  $index++;                                                        # [17a]
}

say ": Flips: [{ @flip.join(", ") }]" if $verbose;

say @flip.min;                                                     # [19]

[11] Ensure that all the elements are positive. The challenge calls them numbers, so we support all positive numeric values. (Even if the examples only use integers.)

[12] We are going to store the permutuations we get when we flip the sign of none to all the values here.

[13] This recursive procedure builds up all the permutatations, and places them in %sum.

[14] Get the lowest number (key in the hash), after we have removed those that are negative.

[15] We are going to store the number of flips for the candidates here.

[16] For the given key, take the value - as an array - and iterate over it.

[17] I could have declared the variable before the loop, but a state variable ensures that it is only available inside the loop.

[18] Count the number of flips, which is the sum of all the negative numbers.

[19] The smallest number (of flips) is the answer.

See docs.raku.org/syntax/state more information about state.

File: flip-array (the rest)
sub do_it (@left, @right is copy)          # [21]
{
  my $current = @right.shift;              # [22]

  unless defined $current                  # [23]
  {
    say ": Candidate [{ @left.join(", ") }] with sum {@left.sum }" if $verbose;
    
    %sum{@left.sum}.push: @left;           # [24]
    return;                                # [24a]
  }

  do_it((@left,  $current).flat, @right);  # [25]
  do_it((@left, -$current).flat, @right);  # [25a]
}

[21] The recursive procedure. Note the is copy on the second parameter, so that we can change it (the local copy). The first list is the numbers we have flipped, and the second is the one that we haven't flipped yet.

[22] Get the next (unflipped) number.

[23] If we are done ([22] failed),

[24] • add the list to the candidate list and we are done with this recursion [24a].

[25] Add the current value, unflipped and flipped [25a] and go on recursively on both.

See docs.raku.org/type/Signature#index-entry-trait_is_copy more information about is copy.

Running it (without verbose mode) gives the expected result:

$ ./flip-array 3 10 8
1

$ ./flip-array 12 2 10
1

Non-Integers work out fine. Even rational numbers like «1/3»:

./flip-array -v 1 2/3 1/3
: Candidate [1, 2/3, 1/3] with sum 2
: Candidate [1, 2/3, 1/3] with sum 1.333333
: Candidate [1, 2/3, 1/3] with sum 0.666667
: Candidate [1, 2/3, 1/3] with sum 0
: Candidate [-1, 2/3, 1/3] with sum 0
: Candidate [-1, 2/3, 1/3] with sum -0.666667
: Candidate [-1, 2/3, 1/3] with sum -1.333333
: Candidate [-1, 2/3, 1/3] with sum -2
: Lowest non-negative sum: 0
: Match 0: [1, 2/3, 1/3] with 2 flip(s)
: Match 1: [-1, 2/3, 1/3] with 1 flip(s)
: Flips: [2, 1]
1

Except for a very obvious problem. The sign is not shwown for the rational numbers. The sum and the number of flips are calculated correctly, so this is only a presentation error.

And that's it.