Perl Weekly Challenge: Word Reverse String Sort

I’m posting really late this week because this was a special week for my wife and I: 19 years ago this past Wednesday, I managed to get her alone during intermission of the production of Guys and Dolls we were in, and I asked her out on a date. I guess the date went well, because 19 years later we’re still together.

To celebrate, we went into Manhattan and went to see two musicals in one day: a matinee of Wicked, one of the musicals my wife was in love with when we met, and an evening performance of a new musical my wife wanted to see: Back to the Future. For today’s musical accompaniment, I’m presenting “For The Dreamers“, one of the new songs from the show.

But now that’s over, and my wife is off at rehearsal for the show she’s doing right now, so let’s sort some strings and reverse some words for Perl Weekly Challenge 278!

Task 1: Sort String

You are given a shuffle string, $str.

Write a script to return the sorted string.

A string is shuffled by appending word position to each word.

Example 1

Input: $str = "and2 Raku3 cousins5 Perl1 are4"
Output: "Perl and Raku are cousins"

Example 2

Input: $str = "guest6 Python1 most4 the3 popular5 is2 language7"
Output: "Python is the most popular guest language"

Example 3

Input: $str = "Challenge3 The1 Weekly2"
Output: "The Weekly Challenge"

Approach

I knew immediately that I wanted to use a regular expression to grab the number from the back end of each word. I also knew that I wanted to make sure I allowed for multiple characters in the word position so we weren’t limited to lists of 9 or fewer words. And when I was sorting the words, make sure I sort numerically by the word position, so I don’t get words 1, 10, and 2 in that order.

Raku

I had to look up how to do named captures in Raku, because I haven’t really used them. I also decided to learn if there was a way to sort the values of a Raku hash by the keys of the hash, and I discovered there was! I just needed to add a .Int to force the values to be integers so they’d sort numerically, then grab the values and join them with spaces. All in all, I’m very satisfied with line 10.

sub sortString($str) {
  my %words;
  for $str.comb(/\S+/) -> $w {
    $w ~~ / $<word> = (\D+) $<order> = (\d+) /;
    %words{~$<order>} = ~$<word>;
  }
  return %words.sort(*.key.Int)>>.values.join(q{ });
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: $str = "and2 Raku3 cousins5 Perl1 are4"
Output: "Perl and Raku are cousins"

Example 2:
Input: $str = "guest6 Python1 most4 the3 popular5 is2 language7"
Output: "Python is the most popular guest language"

Example 3:
Input: $str = "Challenge3 The1 Weekly2"
Output: "The Weekly Challenge"

Example 4:
Input: $str = "The1 brown9 dog10 fox4 jumped5 lazy8 over6 quick2
               red3 the7"
Output: "The quick red fox jumped over the lazy brown dog"

Perl

I also decided to look up how to do named captures in Perl, because even though they were introduced way back in 5.10, I really never got into the habit of using them over the past two decades. Remapping the words isn’t as concise in my Perl as it was in my Raku, but I think it’s plenty readable, and that’s my top priority.

sub sortString($str) {
  my %words;
  foreach my $w ( split /\s+/, $str ) {
    $w ~~ / (?<word>\D+) (?<order>\d+) /x;
    $words{$+{order}} = $+{word};
  }
  return join q{ }, map {
    $words{$_}
  } sort { $a <=> $b } keys %words;
}

View the entire Perl script for this task on GitHub.

Python

Since I was doing named captures in Raku and Perl, I needed to use them in Python as well.

import re

def sortString(str):
    words = {}
    for w in str.split():
        m = re.search('(?P<word>\D+)(?P<order>\d+)', w)
        words[m.group('order')] = m.group('word')
    
    return (' '.join([
        words[k] for k in sorted(words.keys(), key=int)
    ]))

View the entire Python script for this task on GitHub.

Elixir

I feel like this is one of the first problems where I was able to write really elegant Elixir that was pretty much one long statement joined by the pipe operator (|>). The pipe operator takes the output of the expression on the left of it and feeds it into the function on the right of it as the first parameter. String.split/1 takes a single string argument and splits on whitespace into a list. Enum.map/2 takes an enumerable (the list) and runs a function against it, and returns a list of the results of each of those function calls. In my little function, I’m using Regex.run/3 to grab the words and their numeric orders, and I’m returning them as a Map. This list of maps is then passed to Enum.sort_by/3 to sort the list by a function, which I use to sort on the :order keys of the maps. This is where I thought I was going to have to pass the list to some function I’d write to piece them all together, but then I noticed Enum.map_join/3, which lets me specify both a character to join the list on and a function to apply to each element before joining it!

  def sortString(str) do
    str
    |> String.split
    |> Enum.map(fn(w) ->
         [_, word, order] = Regex.run(~r/(\D+)(\d+)/, w)
         %{order: order, word: word}
       end)
    |> Enum.sort_by( &( Integer.parse(&1[:order]) ) )
    |> Enum.map_join(" ", &( &1[:word] ))
  end


View the entire Elixir script for this task on GitHub
.


Intermission

Doc Brown: “🎶 And now I find my glory in this stainless steel DeLorean! 🎶”
Marty McFly: “Hey, Doc… who are the girls?”
Doc Brown: “I don’t know, they just show up every time I start singing!”


Task 2: Reverse Word

You are given a word, $word and a character, $char.

Write a script to replace the substring up to and including $char with its characters sorted alphabetically. If the $char doesn’t exist then DON'T do anything.

Example 1

Input: $str = "challenge", $char = "e"
Output: "acehllnge"

Example 2

Input: $str = "programming", $char = "a"
Output: "agoprrmming"

Example 3

Input: $str = "champion", $char = "b"
Output: "champion"

Approach

Ok, this one seems tailor made to Perl’s index function. Find the first position of the character, split the string on that position using substr, and sort the first part.

Raku

Because Raku and Perl are cousins, of course Raku has an index function, though in this case it’s a method on the Str class. It also has a substr function. Then use .comb to pull apart the characters of the first part, .sort them and then re .join them.

sub reverseWord($str, $char) {
  my $pos = $str.index($char);

  # if the character isn't in the string, do nothing
  return $str unless $pos.defined;

  my @parts = (
    substr($str, 0, $pos+1),
    substr($str, $pos+1)
  );
  
  @parts[0] = @parts[0].comb.sort.join;
  
  return @parts.join;
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: $str = "challenge", $char = "e"
Output: "acehllnge"

Example 2:
Input: $str = "programming", $char = "a"
Output: "agoprrmming"

Example 3:
Input: $str = "champion", $char = "b"
Output: "champion"

Perl

Even though I thought about this in Perl terms in the Approach section, I made myself actually write the Raku first.

sub reverseWord($str, $char) {
  my $pos = index($str, $char);

  # if the character isn't in the string, do nothing
  return $str unless $pos >= 0;

  my @parts = (
    substr($str, 0, $pos+1),
    substr($str, $pos+1)
  );

  $parts[0] = join(q{}, sort split //, $parts[0]);

  return join(q{}, @parts);
}

View the entire Perl script for this task on GitHub.

Python

In Python I opted not to use index, because it’s “Like find(), but raise ValueError when the substring is not found.” So I used find instead.

def reverseWord(str, char):
  pos = str.find(char)

  # if the character isn't in the string, do nothing
  if pos < 0: return str

  parts = [ str[0:pos+1], str[pos+1:] ]

  parts[0] = ''.join(sorted(list(parts[0])))

  return ''.join(parts)

View the entire Python script for this task on GitHub.

Elixir

When I went looking for String.index/2, I found that there wasn’t one. I found this elixir-lang-core discussion suggesting the way I wound up doing it.

  def reverseWord(str, char) do
    if String.contains?(str, char) do
      [part1, part2] = String.split(str, char, parts: 2)
      part1 = part1 <> char # put back the char from the split
      |> String.graphemes
      |> Enum.sort
      |> Enum.join
      Enum.join([part1, part2])
    else
      str
    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-278/packy-anderson

One thought on “Perl Weekly Challenge: Word Reverse String Sort

  1. Pingback: Perl Weekly Challenge: Split ’em away! | Packy’s Place

Comments are closed.