Perl Weekly Challenge: The array is rank, but the string is formatted

This week is another blog-light week; my evening for writing got eaten up by a production problem at work, so… here, listen to Signs by the Five Man Electrical Band while we whip through Perl Weekly Challenge 322.

Task 1: String Format

You are given a string and a positive integer.

Write a script to format the string, removing any dashes, in groups of size given by the integer. The first group can be smaller than the integer but should have at least one character. Groups should be separated by dashes.

Example 1

Input: $str = "ABC-D-E-F", $i = 3
Output: "ABC-DEF"

Example 2

Input: $str = "A-BC-D-E", $i = 2
Output: "A-BC-DE"

Example 3

Input: $str = "-A-B-CD-E", $i = 4
Output: "A-BCDE"

Approach

Since the first group can be smaller that the target group size, that said to me that we should process the string backwards. First, remove all the dashes. Then pull characters off the string in groups of $i characters, and put them into the beginning of a new string with a dash in front of them. Repeat until the source string is less than or equal in length to $i, and then put the remaining characters on the front of the result string with no leading dash.

Raku

I tried to make this Raku-ish by using .subst, .substr, and .substr-rw as methods on a Str object instead of their functional counterparts.

sub strFormat($str is copy, $i) {
  $str = $str.subst("-", :g);
  my $output = '';
  while ($str.chars > $i) {
    $output = "-" ~ $str.substr(*-$i) ~ $output;
    $str.substr-rw(*-$i) = '';
  }
  $output = $str ~ $output;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: $str = "ABC-D-E-F", $i = 3
Output: "ABC-DEF"

Example 2:
Input: $str = "A-BC-D-E", $i = 2
Output: "A-BC-DE"

Example 3:
Input: $str = "-A-B-CD-E", $i = 4
Output: "A-BCDE"

Example 4:
Input: $str = "-A-B-CD-E", $i = 5
Output: "ABCDE"

Perl

I remember being amazed when I learned that, in Perl, substr can be used as an lvalue.

sub strFormat($str, $i) {
  $str =~ s/\-//g;
  my $output = '';
  while (length($str) > $i) {
    $output = "-" . substr($str, -$i) . $output;
    substr($str, -$i) = '';
  }
  $output = $str . $output;
}

View the entire Perl script for this task on GitHub.

Python

Two things that slapped me in the face when I went to do this in Python:

  • Python strings are immutable, so I can’t assign an empty string to the last i characters of the string; I had to build a new string without the last i charaters
  • And I had completely overlooked that in the Raku and Perl solutions, I was taking advantage of the language family’s behavior that, in lieu of an explicit return statement, the return value of a function is the evaluated value of the last statement. However, when I first ran the Python version, I got the following:
    
    Example 1:
    Input: $str = "ABC-D-E-F", $i = 3
    Output: "None"

At this point, I should confess to how I write the solutions. I write my solution in Raku, then I copy the Raku code to a new file and convert it to Perl, then I copy the Raku code to another new file and convert it to Python, and then I copy the Python code to a new file and covert it to Elixir. By far, I need to make the most changes going from Python to Elixir, but sometimes (like now) I get brought up short by differences between the children of Larry Wall and other programming languages.

So in the Perl and Raku solutions, I could have just had the last line of the function be $str . $output and $str ~ $output, but I decided not to go back and change them.

def strFormat(strVar, i):
  strVar = strVar.replace("-", "")
  output = ''
  while (len(strVar) > i):
    output = "-" + strVar[-i:] + output
    strVar = strVar[0:-i] # Python strings are IMMUTABLE
  return strVar + output

View the entire Python script for this task on GitHub.

Elixir

Elixir, of course, always makes me re-think things because of the fundamental lack of loops. Reworking a looping solution as a recursive solution stretches me a little. I opted to convert the string into a list of graphemes immediately after removing the dashes because in Elixir it seems easier to deal with Enums than it is to deal with Strings. For instance, I can use Enum.take/2 to grab the last i characters off the list, and use the same function to get the remaining characters from the beginning of the string (like in Python, Elixir values are immutable, so I have to assign the modified list back to the same label, charlist).

There are no String functions that can be used in a guard, so passing around the string as a list allows me to use length(charlist) <= i as a guard on the termination clause for strFormat/3.

  def strFormat(charlist, i, output) when length(charlist) <= i,
    do: List.to_string(charlist) <> output

  def strFormat(charlist, i, output) do
    chunk    = Enum.take(charlist, -i) |> List.to_string
    charlist = Enum.take(charlist, length(charlist)-i)
    strFormat(charlist, i, "-" <> chunk <> output)
  end

  def strFormat(str, i) do
    str = String.replace(str, "-", "")
    strFormat(String.graphemes(str), i, "")
  end

View the entire Elixir script for this task on GitHub.


Task 2: Rank Array

You are given an array of integers.

Write a script to return an array of the ranks of each element: the lowest value has rank 1, next lowest rank 2, etc. If two elements are the same then they share the same rank.

Example 1

Input: @ints = (55, 22, 44, 33)
Output: (4, 1, 3, 2)

Example 2

Input: @ints = (10, 10, 10)
Output: (1, 1, 1)

Example 3

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

Approach

The easiest way to get the rank is to sort the list into a new array, then loop over the new array, maintain a counter, and keep a rank hash-like data structure. Start with the counter equal to 0, and then loop over the sorted array. If the current element is already in the rank hash, move on to the next element. If it isn’t, increment the counter and insert that new value in rank with the current element as the key. Once we’re done, we’ll have a hash of the ranks of each of the elements in the original array.

Raku

Again, I used things as methods as much as possible to keep it as Raku-ish as I could. But it occurs to me that using next with a postfix if statement is still very Perl.

sub rankArray(@ints) {
  my $rank = 0;
  my %rank;
  for @ints.sort() -> $i {
    next if %rank{$i}:exists;
    %rank{$i} = ++$rank;
  }
  @ints.map({ %rank{$_} });
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: @ints = (55, 22, 44, 33)
Output: (4, 1, 3, 2)

Example 2:
Input: @ints = (10, 10, 10)
Output: (1, 1, 1)

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

Perl

Besides moving the postfix sort and map methods and the :exists adverb to functions, nothing else needed to be done to convert Raku to Perl, which pretty much just proves that I think in Perl.

sub rankArray(@ints) {
  my $rank = 0;
  my %rank;
  foreach my $i (sort @ints) {
    next if exists $rank{$i};
    $rank{$i} = ++$rank;
  }
  map { $rank{$_} } @ints;
}

View the entire Perl script for this task on GitHub.

Python

Until I got to the Python solution, I hadn’t noticed that in Raku and Perl I had taken advantage of the feature that $rank and %rank are different variables, which can be confusing, so I renamed them to rankInt and rankDict. Plus I get to show off knowing Python’s one-line if syntax, even if it violates PEP 8.

def rankArray(ints):
  rankInt = 0
  rankDict = {}
  for i in sorted(ints):
    if i in rankDict: continue
    rankInt += 1
    rankDict[i] = rankInt
  return [ rankDict[i] for i in ints ]

View the entire Python script for this task on GitHub.

Elixir

Like I did in PWC 320, I’m using Enum.map_reduce/3 to loop over a list to generate data based on that list, and once again I’m getting to use a private function to make the code cleaner. One thing that keeps tripping me up, though, is remembering that even when I’m not changing anything in the function, I still need to return all the input arguments (look at lines 8 and 10).

But I do enjoy that the last evaluated value in a function is the return value of the function. Elixir doesn’t even have a return statement!

  defp makeRankMap(i, {rank, rankMap}) do
    cond do
      not Map.has_key?(rankMap, i) ->
        rank = rank + 1
        {i, { rank, Map.put_new(rankMap, i, rank)} }
      true ->
        {i, { rank, rankMap } }
    end
  end

  def rankArray(ints) do
    {_, {_, rankMap}} = Enum.map_reduce(
      Enum.sort(ints), {0, %{}}, &makeRankMap/2
    )
    Enum.map(ints, fn i -> Map.get(rankMap, i) end)
  end

View the entire Elixir script for this task on GitHub.


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

Leave a Reply