Odd Valleys
with Raku

by Arne Sommer

Odd Valleys with Raku

[222] Published 4. February 2023.

This is my response to The Weekly Challenge #202.

Challenge #202.1: Consecutive Odds

You are given an array of integers.

Write a script to print 1 if there are THREE consecutive odds in the given array otherwise print 0.

Example 1:
Input: @array = (1,5,3,6)
Output: 1
Example 2:
Input: @array = (2,6,3,5)
Output: 0
Example 3:
Input: @array = (1,2,3,4)
Output: 0
Example 4:
Input: @array = (2,3,5,7)
Output: 1

I choose to interpretet «three consecutive» as «at least three consecutive», and not as «exactly three consecutive».

File: consecutive-odds
#! /usr/bin/env raku

unit sub MAIN (*@array where @array.elems
                         && all(@array) ~~ /^\-?<[0..9]>*$/,  # [1]
               :v(:$verbose));

my $consecutive = 0;         # [2]
my $start_index = 0;         # [3]

for ^@array.elems -> $i      # [4]
{
  if (@array[$i] %% 2)       # [5]
  {
    $consecutive = 0;        # [5a]
    $start_index++;          # [3a]
  }
  else                       # [6]
  {
    $consecutive++;          # [6a]
  }

  if ($consecutive == 3)     # [7]
  {
    say ":Consecutives: [{ @array[$start_index .. $start_index+2].join(",") \
      }] starting at undex $start_index" if $verbose;  # [3b]
    say 1;                   # [7a]
    exit;                    # [7b]
  }
}

say 0;                       # [8]

[1] Alow negative integers as well (with an optional minus sign at the beginning of each value). Note that a negative integer as the very first argument will screw up the argument passing, so you should not do that.

[2] The number of consecutive odd values, initially none.

[3] The start index of where we found a match, set in [3a] and used by the verbose output in [3b] only. The algoritm as such does not need this variable.

[4] Iterate over the indices of the array.

[5] Do we have an even number (i.e. one that is divisible by 2), courtesy of the divisibility operator %%. If so reset the count of odd values [5a].

See docs.raku.org/routine/%% for more information about the Divisibility Operator %%.

[6] If not (i.e. it is odd), increase the count of consecutive odd values [6a].

[7] Do we have three consecutive odd values? if so, print «1» and exit.

[8] We have failed to find three consecutive odd values, if we get here. Print «0».

Running it gives the expected result:

$ ./consecutive-odds 1 5 3 6
1

$ ./consecutive-odds 2 6 3 5
0

$ ./consecutive-odds 1 2 3 4
0

$ ./consecutive-odds 2 3 5 7
1

Running it with verbose mode:

$ ./consecutive-odds -v 1 5 3 6
:Consecutives: [1,5,3] starting at undex 0
1

$ ./consecutive-odds -v 2 6 5 3
0

$ ./consecutive-odds -v 1 2 3 4
0

$ ./consecutive-odds -v 2 3 5 7
:Consecutives: [3,5,7] starting at undex 1
1

5 consecutive odd values are ok:

$ ./consecutive-odds -v 0 1 3 5 7 9
:Consecutives: [1,3,5] starting at undex 1
1

Challenge #202.2: Widest Valley

Given a profile as a list of altitudes, return the leftmost widest valley. A valley is defined as a subarray of the profile consisting of two parts: the first part is non-increasing and the second part is non-decreasing. Either part can be empty.

Example 1:
Input: 1, 5, 5, 2, 8
Output: 5, 5, 2, 8
Example 2:
Input: 2, 6, 8, 5
Output: 2, 6, 8
Example 3:
Input: 9, 8, 13, 13, 2, 2, 15, 17
Output: 13, 13, 2, 2, 15, 17
Example 4:
Input: 2, 1, 2, 1, 3
Output: 2, 1, 2
Example 5:
Input: 1, 3, 3, 2, 1, 2, 3, 3, 2
Output: 3, 3, 2, 1, 2, 3, 3

This first version is slighly more rich of lines than strictly necessary. I'll get back to that later.

File: widest-valley
#! /usr/bin/env raku

unit sub MAIN (*@array where @array.elems && all(@array) ~~ /^<[0..9]>*$/,
               :v(:$verbose));                  # [0]

my @valleys;                                    # [1]

for ^@array.elems -> $start                     # [2]
{
  my @c = @array[$start..Inf].clone;            # [3]

  say ":Starting at offset $start; values [ { @c.join(",") } ]" if $verbose;

  my @current = (@c.shift.Int,);               # [4]

  say ":First: @current[0]" if $verbose;

  my $non-inc = True;                          # [5]

  while (@c.elems)                             # [6]
  {
    my $curr = @c.shift.Int;                   # [7]
    
    if $non-inc                                # [8]
    {
      if $curr <= @current.tail                # [8a]
      {
        ;                                      # [8b]
      }
      else
      {
        $non-inc = False;                      # [8c]
      }
    }
    else                                       # [9]
    {
      if $curr >= @current.tail                # [9a]
      {
        ;                                      # [9b]
      }
      else
      {
        @valleys.push: @current.clone;        # [10]
	@current = ();                        # [10a]
        last;                                 # [10b]
      }
    }
    say ":Add: $curr ({ $non-inc ?? "!inc" !! "!desc" })" if $verbose;
    @current.push: $curr;                     # [11]
  }
  @valleys.push: @current if @current.elems;  # [12]
}

say ":Valleys: { @valleys.raku; }" if $verbose;
say ":Widest: { @valleys>>.elems.max }" if $verbose;

say @valleys.grep({ $_.elems == @valleys>>.elems.max }).first.join(", ");
                                              # [13]

[0] At least one element, and they must all be non-negative integers.

[1] All the possible valleys will end up here.

[2] Iterate over the indices (of the array), from left to right.

[3] Get a copy (with clone) of the original array, starting at the given index (from [2]). A copy, as we are changing it (in [4] and [7]).

See docs.raku.org/routine/clone for more information about the clone method.

[4] Get the first value, placed into the array used to build up the current valley. There are two interesting features in play here. The first is the use of .Int to coerce the value from the IntStr type, courtesy of the command line, to a real (not in the matematical sense, but you get the idea) integer. The second is the trailing comma after the value. The comma is there to turn the single value into a list, which is what we want. The parens are just for grouping in Raku (as opposed to Perl where they will genereate a list).

[5] We are initially in the first part of the valley (the non-increasing part).

[6] As long as we have more elements to parse.

[7] Get the next one.

[8] If we are in the first part of the valley, and the next value is non-increasing [8a], all is well [8b] (as in we are still in the first part of the valley). If not, we have reached the second part of the valley [8c]; the non-decreasing part. In both cases, the next value will be added to the valley in [11].

[9] We are in the second part. If the next value is non-decreasing [9a], all is well [9b] (and the current value will be added to the valley in [11]).

[10] If not, we are done and add the valley (or rather, a copy of it) to the list of valleys, then we reset the current valley [9b] (ready for the next iteration of [2]), and exit the inner loop (with last) [9c] as adding more values to the currently illegal valley will not make it legal.

[11] Add the current value to the current valley.

[12] After going throgh the values of the array, add the resulting valley to the list of results - if it is non-empty.

[13] We are looking for the leftmost (or first) widest valley. We start with the widest, which is the one with the higest number of elements. We get that size with >>.elems (which gets the size of each valley) and then .max to get the highest one. Then we use grep to get only the valleys with that exact width. This is a list of all (1 or more) valleys with that size. Then we use first to get the first (leftmost) one. And finally we print that list (i.e. valley) separated by commas.

Running it:

$ ./widest-valley 1 5 5 2 8
5, 5, 2, 8

$ ./widest-valley 2 6 8 5
2, 6, 8

$ ./widest-valley 9 8 13 13 2 2 15 17
13, 13, 2, 2, 15, 17

$ ./widest-valley 2 1 2 1 3
2, 1, 2

$ ./widest-valley 1 3 3 2 1 2 3 3 2
3, 3, 2, 1, 2, 3, 3

Looking good.

With verbose mode, just to show off:

$ ./widest-valley -v 1 5 5 2 8
:Starting at offset 0; values [ 1,5,5,2,8 ]
:First: 1
:Add: 5 (!desc)
:Add: 5 (!desc)
:Starting at offset 1; values [ 5,5,2,8 ]
:First: 5
:Add: 5 (!inc)
:Add: 2 (!inc)
:Add: 8 (!desc)
:Starting at offset 2; values [ 5,2,8 ]
:First: 5
:Add: 2 (!inc)
:Add: 8 (!desc)
:Starting at offset 3; values [ 2,8 ]
:First: 2
:Add: 8 (!desc)
:Starting at offset 4; values [ 8 ]
:First: 8
:Valleys: [[1, 5, 5], [5, 5, 2, 8], [5, 2, 8], [2, 8], [8]]
:Widest: 4
5, 5, 2, 8

$ ./widest-valley -v 2 6 8 5
:Starting at offset 0; values [ 2,6,8,5 ]
:First: 2
:Add: 6 (!desc)
:Add: 8 (!desc)
:Starting at offset 1; values [ 6,8,5 ]
:First: 6
:Add: 8 (!desc)
:Starting at offset 2; values [ 8,5 ]
:First: 8
:Add: 5 (!inc)
:Starting at offset 3; values [ 5 ]
:First: 5
:Valleys: [[2, 6, 8], [6, 8], [8, 5], [5]]
:Widest: 3
2, 6, 8

$ ./widest-valley -v 9 8 13 13 2 2 15 17
:Starting at offset 0; values [ 9,8,13,13,2,2,15,17 ]
:First: 9
:Add: 8 (!inc)
:Add: 13 (!desc)
:Add: 13 (!desc)
:Starting at offset 1; values [ 8,13,13,2,2,15,17 ]
:First: 8
:Add: 13 (!desc)
:Add: 13 (!desc)
:Starting at offset 2; values [ 13,13,2,2,15,17 ]
:First: 13
:Add: 13 (!inc)
:Add: 2 (!inc)
:Add: 2 (!inc)
:Add: 15 (!desc)
:Add: 17 (!desc)
:Starting at offset 3; values [ 13,2,2,15,17 ]
:First: 13
:Add: 2 (!inc)
:Add: 2 (!inc)
:Add: 15 (!desc)
:Add: 17 (!desc)
:Starting at offset 4; values [ 2,2,15,17 ]
:First: 2
:Add: 2 (!inc)
:Add: 15 (!desc)
:Add: 17 (!desc)
:Starting at offset 5; values [ 2,15,17 ]
:First: 2
:Add: 15 (!desc)
:Add: 17 (!desc)
:Starting at offset 6; values [ 15,17 ]
:First: 15
:Add: 17 (!desc)
:Starting at offset 7; values [ 17 ]
:First: 17
:Valleys: [[9, 8, 13, 13], [8, 13, 13], [13, 13, 2, 2, 15, 17], \
   [13, 2, 2, 15, 17], [2, 2, 15, 17], [2, 15, 17], [15, 17], [17]]
:Widest: 6
13, 13, 2, 2, 15, 17

$ ./widest-valley -v 2 1 2 1 3
:Starting at offset 0; values [ 2,1,2,1,3 ]
:First: 2
:Add: 1 (!inc)
:Add: 2 (!desc)
:Starting at offset 1; values [ 1,2,1,3 ]
:First: 1
:Add: 2 (!desc)
:Starting at offset 2; values [ 2,1,3 ]
:First: 2
:Add: 1 (!inc)
:Add: 3 (!desc)
:Starting at offset 3; values [ 1,3 ]
:First: 1
:Add: 3 (!desc)
:Starting at offset 4; values [ 3 ]
:First: 3
:Valleys: [[2, 1, 2], [1, 2], [2, 1, 3], [1, 3], [3]]
:Widest: 3
2, 1, 2

$ ./widest-valley -v 1 3 3 2 1 2 3 3 2
:Starting at offset 0; values [ 1,3,3,2,1,2,3,3,2 ]
:First: 1
:Add: 3 (!desc)
:Add: 3 (!desc)
:Starting at offset 1; values [ 3,3,2,1,2,3,3,2 ]
:First: 3
:Add: 3 (!inc)
:Add: 2 (!inc)
:Add: 1 (!inc)
:Add: 2 (!desc)
:Add: 3 (!desc)
:Add: 3 (!desc)
:Starting at offset 2; values [ 3,2,1,2,3,3,2 ]
:First: 3
:Add: 2 (!inc)
:Add: 1 (!inc)
:Add: 2 (!desc)
:Add: 3 (!desc)
:Add: 3 (!desc)
:Starting at offset 3; values [ 2,1,2,3,3,2 ]
:First: 2
:Add: 1 (!inc)
:Add: 2 (!desc)
:Add: 3 (!desc)
:Add: 3 (!desc)
:Starting at offset 4; values [ 1,2,3,3,2 ]
:First: 1
:Add: 2 (!desc)
:Add: 3 (!desc)
:Add: 3 (!desc)
:Starting at offset 5; values [ 2,3,3,2 ]
:First: 2
:Add: 3 (!desc)
:Add: 3 (!desc)
:Starting at offset 6; values [ 3,3,2 ]
:First: 3
:Add: 3 (!inc)
:Add: 2 (!inc)
:Starting at offset 7; values [ 3,2 ]
:First: 3
:Add: 2 (!inc)
:Starting at offset 8; values [ 2 ]
:First: 2
:Valleys: [[1, 3, 3], [3, 3, 2, 1, 2, 3, 3], [3, 2, 1, 2, 3, 3], \
  [2, 1, 2, 3, 3], [1, 2, 3, 3], [2, 3, 3], [3, 3, 2], [3, 2], [2]]
:Widest: 7
3, 3, 2, 1, 2, 3, 3

Let us have a go at a plain, a couple of slopes, and a mountain to top it all off:

$ ./widest-valley -v 1 1 1
:Starting at offset 0; values [ 1,1,1 ]
:First: 1
:Add: 1 (!inc)
:Add: 1 (!inc)
:Starting at offset 1; values [ 1,1 ]
:First: 1
:Add: 1 (!inc)
:Starting at offset 2; values [ 1 ]
:First: 1
:Valleys: [[1, 1, 1], [1, 1], [1]]
:Widest: 3
1, 1, 1

$ ./widest-valley -v 1 2 3
:Starting at offset 0; values [ 1,2,3 ]
:First: 1
:Add: 2 (!desc)
:Add: 3 (!desc)
:Starting at offset 1; values [ 2,3 ]
:First: 2
:Add: 3 (!desc)
:Starting at offset 2; values [ 3 ]
:First: 3
:Valleys: [[1, 2, 3], [2, 3], [3]]
:Widest: 3
1, 2, 3

$ ./widest-valley -v 3 2 1
:Starting at offset 0; values [ 3,2,1 ]
:First: 3
:Add: 2 (!inc)
:Add: 1 (!inc)
:Starting at offset 1; values [ 2,1 ]
:First: 2
:Add: 1 (!inc)
:Starting at offset 2; values [ 1 ]
:First: 1
:Valleys: [[3, 2, 1], [2, 1], [1]]
:Widest: 3
3, 2, 1

$ ./widest-valley -v 1 2 3 2 1
:Starting at offset 0; values [ 1,2,3,2,1 ]
:First: 1
:Add: 2 (!desc)
:Add: 3 (!desc)
:Starting at offset 1; values [ 2,3,2,1 ]
:First: 2
:Add: 3 (!desc)
:Starting at offset 2; values [ 3,2,1 ]
:First: 3
:Add: 2 (!inc)
:Add: 1 (!inc)
:Starting at offset 3; values [ 2,1 ]
:First: 2
:Add: 1 (!inc)
:Starting at offset 4; values [ 1 ]
:First: 1
:Valleys: [[1, 2, 3], [2, 3], [3, 2, 1], [2, 1], [1]]
:Widest: 3
1, 2, 3

Then a shorter version, with the replaced part highlighted in green:

File: widest-valley-shorter
#! /usr/bin/env raku

unit sub MAIN (*@array where @array.elems && all(@array) ~~ /^<[0..9]>*$/,
               :v(:$verbose));

my @valleys;

for ^@array.elems -> $start
{
  my @c = @array[$start..Inf].clone;

  say ":Starting at offset $start; values [ { @c.join(",") } ]" if $verbose;

  my @current = (@c.shift.Int,);

  say ":First: @current[0]" if $verbose;

  my $non-inc = True;

  while (@c.elems)
  {
    my $curr = @c.shift.Int;
    
    if $non-inc && $curr > @current.tail
    {
      $non-inc = False;
    }
    elsif ! $non-inc && $curr < @current.tail
    {
      @valleys.push: @current.clone;
      @current = ();
      last;
    }

    say ":Add: $curr ({ $non-inc ?? "!inc" !! "!desc" })" if $verbose;
    @current.push: $curr;
  }
  @valleys.push: @current if @current.elems;
}

say ":Valleys: { @valleys.raku; }" if $verbose;
say ":Widest: { @valleys>>.elems.max }" if $verbose;

say @valleys.grep({ $_.elems == @valleys>>.elems.max }).first.join(", ");

The result of running this version is as expected:

$ ./widest-valley-shorter 1 5 5 2 8
5, 5, 2, 8

$ ./widest-valley-shorter 2 6 8 5
2, 6, 8

$ ./widest-valley-shorter 9 8 13 13 2 2 15 17
13, 13, 2, 2, 15, 17

$ ./widest-valley-shorter 2 1 2 1 3
2, 1, 2

$ ./widest-valley-shorter 1 3 3 2 1 2 3 3 2
3, 3, 2, 1, 2, 3, 3

And that's it.