Perl Weekly Challenge: Word Crimes are Justified

Since Perl Weekly Challenge 360‘s tasks are “Text Justifier” and “Word Sorter”, I first thought about trying to find some song in my music collection that featured justification. “That shouldn’t be hard,” I thought, “because I’ve got so much folk…”

Then the weird part of my brain took over, and insisted on Al Yankovich’s Word Crimes.

Task 1: Text Justifier

You are given a string and a width.

Write a script to return the string that centers the text within that width using asterisks * as padding.

Example 1

Input: $str = "Hi", $width = 5
Output: "*Hi**"

Text length = 2, Width = 5
Need 3 padding characters total
Left padding: 1 star, Right padding: 2 stars

Example 2

Input: $str = "Code", $width = 10
Output: "***Code***"

Text length = 4, Width = 10
Need 6 padding characters total
Left padding: 3 stars, Right padding: 3 stars

Example 3

Input: $str = "Hello", $width = 9
Output: "**Hello**"

Text length = 5, Width = 9
Need 4 padding characters total
Left padding: 2 stars, Right padding: 2 stars

Example 4

Input: $str = "Perl", $width = 4
Output: "Perl"

No padding needed

Example 5

Input: $str = "A", $width = 7
Output: "***A***"

Text length = 1, Width = 7
Need 6 padding characters total
Left padding: 3 stars, Right padding: 3 stars

Example 6

Input: $str = "", $width = 5
Output: "*****"

Text length = 0, Width = 5
Entire output is padding

Approach

This problem takes me back to high school when I was writing code to print headers on the screen that were centered and used equal signs to pad the given text into the center. The problem is straightforward: subtract the length of $str from the desired $width to yield the number of padding characters we need ($pad), and then integer divide that by 2 to get the number of padding characters we need on each side ($lside). To even things out when the number of characters needed is odd, we’ll pad the left with $lside, and pad the right with ($pad-$lside).

Raku

There’s really nothing special here. Furthering the approach a little more, I’m handling the two special cases first: when the string is ≥ the desired width (and, consequently, no padding is needed) or when the string length is 0, and it’s all padding.

sub justify($str, $width) {
  my $len = $str.chars;
  # handle special cases
  if ($len >= $width) {
    return ($str, "No padding needed");
  }
  elsif ($len == 0) {
    return (
      '*' x $width, 
      "Text length = 0, Width = $width\nEntire output is padding"
    );
  }
  my $pad   = $width - $len;
  my $lside = $pad div 2;
  my $rside = $pad - $lside;
  my $output = ('*' x $lside) ~ $str ~ ('*' x $rside);
  my $explain = "Text length = $len, Width = $width\n"
              ~ "Need $pad padding characters total\n"
              ~ "Left padding: $lside stars, "
              ~ "Right padding: $rside stars";
  return ($output, $explain);
}

View the entire Raku script for this task on GitHub.

$ raku/ch-1.raku
Example 1:
Input: $str = "Hi", $width = 5
Output: "*Hi**"

Text length = 2, Width = 5
Need 3 padding characters total
Left padding: 1 stars, Right padding: 2 stars

Example 2:
Input: $str = "Code", $width = 10
Output: "***Code***"

Text length = 4, Width = 10
Need 6 padding characters total
Left padding: 3 stars, Right padding: 3 stars

Example 3:
Input: $str = "Hello", $width = 9
Output: "**Hello**"

Text length = 5, Width = 9
Need 4 padding characters total
Left padding: 2 stars, Right padding: 2 stars

Example 4:
Input: $str = "Perl", $width = 4
Output: "Perl"

No padding needed

Example 5:
Input: $str = "A", $width = 7
Output: "***A***"

Text length = 1, Width = 7
Need 6 padding characters total
Left padding: 3 stars, Right padding: 3 stars

Example 6:
Input: $str = "", $width = 5
Output: "*****"

Text length = 0, Width = 5
Entire output is padding

Perl

The Perl solution is almost identical to the Raku solution, except we use length to get the length of the string and int($pad / 2) to do integer division by 2 (I could have done use integer; and then my $lside = $pad / 2; but that would have been more characters).

sub justify($str, $width) {
  my $len = length($str);
  # handle special cases
  if ($len >= $width) {
    return ($str, "No padding needed");
  }
  elsif ($len == 0) {
    return (
      '*' x $width, 
      "Text length = 0, Width = $width\nEntire output is padding"
    );
  }
  my $pad   = $width - $len;
  my $lside = int($pad / 2);
  my $rside = $pad - $lside;
  my $output = ('*' x $lside) . $str . ('*' x $rside);
  my $explain = "Text length = $len, Width = $width\n"
              . "Need $pad padding characters total\n"
              . "Left padding: $lside stars, "
              . "Right padding: $rside stars";
  return ($output, $explain);
}

View the entire Perl script for this task on GitHub.

Python

Again, the Python solution looks a lot like the Perl solution. In Python, the string repetition operator is *.

def justify(string, width):
  length = len(string)
  # handle special cases
  if length >= width:
    return(string, "No padding needed")
  elif length == 0:
    return(
      '*' * width, 
      f'Text length = 0, Width = {width}\nEntire output is padding'
    )
  pad   = width - length
  lside = int(pad / 2)
  rside = pad - lside
  output = ('*' * lside) + string + ('*' * rside)
  explain = (
    f"Text length = {length}, Width = {width}\n" +
    f"Need {pad} padding characters total\n" +
    f"Left padding: {lside} stars, Right padding: {rside} stars"
  )
  return (output, explain)

View the entire Python script for this task on GitHub.

Elixir

Elixir differs in that it it accepts the two-argument version of justify just to calculate the length of the string and pass it to a three-argument version of justify. This allows us to handle the special cases with guards where we compare the length with the width. If I wanted to say that this would be restricted to ASCII characters only, I could use Kernel.byte_size/1 in a guard to determine the length of str, but that would break the moment I used non-ASCII characters:

iex(1)> byte_size("perl")
4
iex(2)> byte_size("perls")
5
iex(3)> byte_size("perlß")
6
iex(4)> String.length("perlß")
5

Anyway, string repetition is accomplished via String.duplicate/2.

# handle special cases
def justify(str, len, width) when len >= width, do:
  {str, "No padding needed"}

def justify(_, len, width) when len == 0, do:
  {
    String.duplicate("*", width),
    "Text length = 0, Width = #{width}\n" <>
    "Entire output is padding"
  }

def justify(str, len, width) do
  pad   = width - len
  lside = div(pad, 2)
  rside = pad - lside
  {
    String.duplicate("*", lside) <> str <>
    String.duplicate("*", rside),

    "Text length = #{len}, Width = #{width}\n" <>
    "Need #{pad} padding characters total\n" <>
    "Left padding: #{lside} stars, " <>
    "Right padding: #{rside} stars"
  }
end

def justify(str, width) do
  justify(str, String.length(str), width)
end

View the entire Elixir script for this task on GitHub.


Task 2: Word Sorter

You are given a sentence.

Write a script to order words in the given sentence alphabetically but keeps the words themselves unchanged.

Example 1

Input: $str = "The quick brown fox"
Output: "brown fox quick The"

Example 2

Input: $str = "Hello    World!   How   are you?"
Output: "are Hello How World! you?"

Example 3

Input: $str = "Hello"
Output: "Hello"

Example 4

Input: $str = "Hello, World! How are you?"
Output: "are Hello, How World! you?"

Example 5

Input: $str = "I have 2 apples and 3 bananas!"
Output: "2 3 and apples bananas! have I"

Approach

This is another simple problem; it’s basically

  1. Split a string into words by splitting on whitespace
  2. Sort the words based on the folded case version of the word (case folding is like getting the lower-case version of a string, but it’s unicode safe).

Raku

In Raku, the Str routine .words accomplishes the first part. Then you use the Str routine .fc to get the folded case version of each word. One thing I’m doing here is using sort with a custom routine accepting only one element, which is shorter than writing sort: { $^a.fc cmp $^b.fc }.

sub wordSorter($str) {
  $str.words.sort: *.fc
}

View the entire Raku script for this task on GitHub.

$ raku/ch-2.raku
Example 1:
Input: $str = "The quick brown fox"
Output: "brown fox quick The"

Example 2:
Input: $str = "Hello    World!   How   are you?"
Output: "are Hello How World! you?"

Example 3:
Input: $str = "Hello"
Output: "Hello"

Example 4:
Input: $str = "Hello, World! How are you?"
Output: "are Hello, How World! you?"

Example 5:
Input: $str = "I have 2 apples and 3 bananas!"
Output: "2 3 and apples bananas! have I"

Perl

Again, we’re doing the same thing in Perl, just with a slightly different syntax. We’re using a special case of the split function that emulates awk and splits on whitespace when the split pattern is a single space. Then we pass the package global variables $a and $b through fc() to fold the case when sorting.

sub wordSorter($str) {
  sort { fc($a) cmp fc($b) } split " ", $str;
}

View the entire Perl script for this task on GitHub.

Python

The Python solution is so simple, it’s one of the examples in the Python Sorting Techniques documentation page.

def word_sorter(string):
  return " ".join(sorted(string.split(), key=str.casefold))

View the entire Python script for this task on GitHub.

Elixir

In Elixir, however, case folding isn’t a core feature of the language. To get it, I would have to install the Unicode String module and use Unicode.String.fold/2. But, when I try to do that, I get errors, so I’m punting and going with the ASCII-only solution and using String.downcase/2 to compare the strings while sorting them (and, to specify a function to call while sorting, I need to use Enum.sort_by/3).

def word_sorter(str) do
  str
  |> String.split
  |> Enum.sort_by(&String.downcase/1)
  |> Enum.join(" ")
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-360/packy-anderson

Leave a Reply