Perl Weekly Challenge: Oh to live on Array Mountain…

Perl Weekly Challenge 355‘s tasks are “Thousand Separator” and “Mountain Array”. I barely got to thinking “What music am I going to pair with these tasks?” when my brain started playing on loop…

Oh to live on Sugar Mountain
With the barkers and the colored balloons
You can’t be twenty on Sugar Mountain
Though you’re thinking that you’re leaving there too soon
You’re leaving there too soon

So enjoy Neil Young’s Sugar Mountain while we think about the Perl Weekly Challenge.

Task 1: Thousand Separator

You are given a positive integer, $int.

Write a script to add thousand separator, , and return as string.

Example 1

Input: $int = 123
Output: "123"

Example 2

Input: $int = 1234
Output: "1,234"

Example 3

Input: $int = 1000000
Output: "1,000,000"

Example 4

Input: $int = 1
Output: "1"

Example 5

Input: $int = 12345
Output: "12,345"

Approach

In general, the trick for doing this is converting the number to a string, reversing the string, then pulling three characters off at a time and interspersing them with commas until there are three or fewer characters less, and then appending those to the result and once again reversing the strng.

Raku

In Raku, this could be a one-liner, but it’s 75 characters long, and my blog doesn’t display things longer than 65 chars very well. Besides, I can comment operation if I put it on its own line.

sub thousandSeparator($int) {
  $int.Str                # convert int to string
      .comb               # break the string into list of chars
      .reverse            # reverse the list
      .rotor(3, :partial) # return a sequence of 3 element lists
      .join(",")          # join the lists with commas
      .comb(/\S/)         # break the string back into chars
      .reverse            # reverse the list again
      .join;              # join them back into a string
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: $int = 123
Output: "123"

Example 2:
Input: $int = 1234
Output: "1,234"

Example 3:
Input: $int = 1000000
Output: "1,000,000"

Example 4:
Input: $int = 1
Output: "1"

Example 5:
Input: $int = 12345
Output: "12,345"

Perl

The easy way in Perl would be to use Number::Format::format_number, but where’s the fun in that? I’m still using List::AllUtils’ bundle_by to do the batching, which feels a little like cheating, though. But bundle_by is close to Raku’s rotor, so I’m using it.

use List::AllUtils qw( bundle_by );

sub thousandSeparator($int) {
  # flow goes from bottom to top
  scalar reverse               # reverse the string
  join ',',                    # join the strings with comma
  bundle_by { join "", @_ } 3, # return a list of 3 char strings
  reverse                      # reverse the list
  split //, $int;              # break the string into chars
}

View the entire Perl script for this task on GitHub.

Python

In Python, the cheat is itertools’ batched function. And also the Python idiom of using string[::-1] to reverse a string.

def thousand_separator(num):
  # flow goes inside to top to bottom
  return(
    ",".join( # join the list on commas
      [
        # convert num into a string, reverse it, then batch it
        # every 3 characters, then join those 3 chars to a string
        "".join(l) for l in batched(str(num)[::-1], 3)
      ]
    )[::-1] # reverse the string again
  )

View the entire Python script for this task on GitHub.

Elixir

Interestingly, Elixir winds up being the most like Raku in that we’re able to pipe information from the beginning to the end, not from bottom to top or inside out.

  def thousand_separator(int) do
    int
    |> Integer.digits # convert integer into digits
    |> Enum.reverse   # reverse the digits
    |> Enum.chunk_every(3) # return a sequence of 3 element lists
    # convert [[1, 2, 3], [4, 5]] to ["123", "45"]
    |> Enum.map(fn l -> Enum.join(l,"") end)
    |> Enum.join(",") # join the sequence on commas
    |> String.reverse # reverse the whole string
  end

View the entire Elixir script for this task on GitHub.


Task 2: Mountain Array

You are given an array of integers, @ints.

Write a script to return true if the given array is a valid mountain array.

An array is mountain if and only if:
1) arr.length >= 3
and
2) There exists some i with 0 < i < arr.length - 1 such that:
arr[0] < arr[1]     < ... < arr[i - 1] < arr[i]
arr[i] > arr[i + 1] > ... > arr[arr.length - 1]

Example 1

Input: @ints = (1, 2, 3, 4, 5)
Output: false

Example 2

Input: @ints = (0, 2, 4, 6, 4, 2, 0)
Output: true

Example 3

Input: @ints = (5, 4, 3, 2, 1)
Output: false

Example 4

Input: @ints = (1, 3, 5, 5, 4, 2)
Output: false

Example 5

Input: @ints = (1, 3, 2)
Output: true

Approach

Basically, this is just checking conditions. If the length of the array is greater than or equal to 3, and the integers in the array increase to a single value, then decrease until the end of the array, it’s a mountain. Example 4 fails because the array increases to a pair of equal values (I guess it’s a plateau array). The list [1, 5, 4, 3, 2, 1] is a mountain where the peak is the second value.

Raku

I’ll confess that I was thinking about how I wanted to do this in Elixir when I wrote the Raku solution; hence the use of recursion and multi.

multi mountainArray($up, @ints) {
  my $first = @ints.shift();

  # must be strictly greater than or strictly less than
  return 'false' if $first == @ints[0];

  # the penultimate element > last element
  if (@ints.elems == 1) {
    return 'true' if $first > @ints[0];
    return 'false';
  }

  if (!$up) { # we're going back down
    # so elements from here on out must be >
    return 'false' if $first < @ints[0];
    return mountainArray(0, @ints);
  }
  else { # we've been coming up
    # $first is the peak, head back down
    return mountainArray(0, @ints) if $first > @ints[0];

    # keep going up
    return mountainArray(1, @ints);
  }
}

multi mountainArray(@ints) {
  # arr.length >= 3
  return 'false' unless @ints.elems >= 3;

  # first element < second element
  my $first = @ints.shift();
  return 'false' unless $first < @ints[0];

  # check the rest of the array
  return mountainArray(1, @ints);
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: @ints = (1, 2, 3, 4, 5)
Output: false

Example 2:
Input: @ints = (0, 2, 4, 6, 4, 2, 0)
Output: true

Example 3:
Input: @ints = (5, 4, 3, 2, 1)
Output: false

Example 4:
Input: @ints = (1, 3, 5, 5, 4, 2)
Output: false

Example 5:
Input: @ints = (1, 3, 2)
Output: true

Example 6:
Input: @ints = (1, 5, 4, 3, 2, 1)
Output: true

Elixir

Since I had the Elixir solution on my mind when I was writing the Raku solution, I tackled that next. One thing I did here that I haven’t before is passing around Elixir atoms, which are Elixir constants whose values are their own name. This way, I could call the parameter direction and pass it the atoms :up and :down. Interestingly enough, all the library functions I’m calling are from Kernel, so I don’t need to prefix those calls with Kernel.

def mountain_array(_, [first | ints]) when length(ints) == 1 do
  cond do
    first > hd(ints) -> "true"
    true -> "false"
  end
end

def mountain_array(direction, [first | ints]) do
  cond do
    # must be strictly greater than or strictly less than
    first == hd(ints) -> "false"

    # if we're going back down, elements
    # from here on out must be >
    direction == :down and first < hd(ints) -> "false"
    direction == :down -> mountain_array(direction, ints)

    # first is the peak, head back down
    first > hd(ints) -> mountain_array(:down, ints)
    true -> mountain_array(direction, ints)
  end
end

def mountain_array([first | ints]) do
  cond do
    # arr.length >= 3 (we pulled the firt elem off already)
    length(ints) < 2 -> "false"

    # first element < second element
    first > hd(ints) -> "false"

    # check the rest of the array
    true -> mountain_array(:up, ints)
  end
end

View the entire Elixir script for this task on GitHub.

Perl

The Perl solution is pretty much exactly the Raku solution, only I needed to rename the two-parameter version of mountainArray.

sub mountainArray2($up, @ints) {
  my $first = shift @ints;

  # must be strictly greater than or strictly less than
  return 'false' if $first == $ints[0];

  # the penultimate element > last element
  if (@ints == 1) {
    return 'true' if $first > $ints[0];
    return 'false';
  }

  if (!$up) { # we're going back down
    # so elements from here on out must be >
    return 'false' if $first < $ints[0];
    return mountainArray2(0, @ints);
  }
  else { # we've been coming up
    # $first is the peak, head back down
    return mountainArray2(0, @ints) if $first > $ints[0];

    # keep going up
    return mountainArray2(1, @ints);
  }
}

sub mountainArray(@ints) {
  # arr.length >= 3
  return 'false' unless @ints >= 3;

  # first element < second element
  my $first = shift @ints;
  return 'false' unless $first < $ints[0];

  # check the rest of the array
  return mountainArray2(1, @ints);
}

View the entire Perl script for this task on GitHub.

Python

And the Python version is much the same.

def mountain_array2(up, nums):
  first = nums.pop(0)

  # must be strictly greater than or strictly less than
  if first == nums[0]:
    return "false"

  # the penultimate element > last element
  if len(nums) == 1:
    if first > nums[0]:
      return "true"
    return "false"

  if not up: # we're going back down
    if first < nums[0]:
      return "false"
    return mountain_array2(0, nums)
  else: # we've been coming up
    if first > nums[0]:
      # first is the peak, head back down
      return mountain_array2(0, nums)
    # keep going up
    return mountain_array2(1, nums)

def mountain_array(nums):
  # arr.length >= 3
  if len(nums) < 3:
    return "false"

  # first element < second element
  first = nums.pop(0)
  if first > nums[0]:
    return "false"

  # check the rest of the array
  return mountain_array2(1, nums)

View the entire Python script for this task on GitHub.


Here’s all my solutions in GItHub: https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-355/packy-anderson

Leave a Reply