Elixir Weekly Challenge: Line Counts

Yesterday, I tackled PWC 267 Task 1. Today I took on PWC 267 Task 2.

This one took a lot of thought. I stumbled onto how to create the map of characters to character widths fairly quickly:

alphabet = String.split("abcdefghijklmnopqrstuvwxyz", "",
                        trim: true)
Enum.zip(alphabet, widths) |> Map.new()

But it was looping over the list of characters in the string and maintaining a count of how wide the string that was being built was that was throwing me. To use Perl terminology, Elixir’s variables are always scoped locally to the code block they’re defined in, so to be able to change a variable you need to return it from whatever block you’re changing it in. Since I wanted to maintain four variables—the line count, the current line, the width of the current line, and an explanatory string that would accumulate the lines and their widths as I went along—I decided that making them values in a Map would make the most sense; I could return them as a unit from whatever block I was modifying them in.

But how to do the loop? I looked at Enum.each/2, but it didn’t allow me to return a single value (in this case, my Map of variables) from the function getting called. Then I remembered Doug’s video about Pattern Matching Functions with Lists. Basically, I could accomplish what I wanted through recursion by having two different definitions for the same function:

# if the character list is empty
defp charCount([], _widthMap, vars) do
  # format the last explanatory text
  vars
end

# split the character list into the first character
# and all the remaining characters in the list
defp charCount([c | remaining], widthMap, vars) do
  # handle counting characters
  # recursively call self to deal with the rest of the list
  charCount(remaining, widthMap, vars)
end

Note the parameter with the leading underscore in the first definition: that means it’s a placeholder, and no value is assigned to it in the function even though there’s a value passed to it in the function call. This allows both function definitions to accept the same three parameters, and the only difference is whether or not the first parameter is an empty list or not.

This lead me to this full implementation:

#!/usr/bin/env elixir

# You are given a string, $str, and a 26-items array @widths
# containing the width of each character from a to z.

# Write a script to find out the number of lines and the width
# of the last line needed to display the given string,
# assuming you can only fit 100 width units on a line.

# Example 1
# Input: $str = "abcdefghijklmnopqrstuvwxyz"
#        @widths = (10,10,10,10,10,10,10,10,10,10,10,10,10,
#                   10,10,10,10,10,10,10,10,10,10,10,10,10)
# Output: (3, 60)
#
# Line 1: abcdefghij (100 pixels)
# Line 2: klmnopqrst (100 pixels)
# Line 3: uvwxyz (60 pixels)

# Example 2
# Input: $str = "bbbcccdddaaa"
#        @widths = (4,10,10,10,10,10,10,10,10,10,10,10,10,
#                   10,10,10,10,10,10,10,10,10,10,10,10,10)
# Output: (2, 4)
#
# Line 1: bbbcccdddaa (98 pixels)
# Line 2: a (4 pixels)

defmodule PWC do
  @alphabet String.split("abcdefghijklmnopqrstuvwxyz", "", trim: true)

  defp updateExplain(vars) do
    # because this is being called with an older version of
    # vars (before lines is incremented), we need to add one
    # to the value in this function
    vars[:explain] <> "\nLine " <> to_string(vars[:lines]+1) <>
    ": #{vars[:last_line]} (#{to_string(vars[:line_width])} pixels)"
  end

  # if the character list is empty
  defp charCount([], _widthMap, vars) do
    vars = %{vars |
      explain: updateExplain(vars),
      lines: vars[:lines] + 1
    }
    vars
  end

  # split the character list into the first character
  # and all the remaining characters in the list
  defp charCount([c | remaining], widthMap, vars) do
    char_width = widthMap[c]
    vars = if vars[:line_width] + char_width > 100 do
      vars = %{vars |
        explain: updateExplain(vars),
        lines: vars[:lines] + 1,
        line_width: char_width,
        last_line: c
      }
      vars
    else
      vars = %{vars |
        line_width: vars[:line_width] + char_width,
        last_line: vars[:last_line] <> c
      }
      vars
    end
    charCount(remaining, widthMap, vars)
  end

  def lineCounts(str, widths) do
    widthMap = Enum.zip(@alphabet, widths)
             |> Map.new()
    vars = %{
      lines: 0,
      line_width: 0,
      last_line: "",
      explain: ""
    }
    charCount(String.split(str, "", trim: true), widthMap, vars)
  end

  def solution(str, widths) do
    IO.puts("Input: $str = '#{str}'")
    IO.puts("       @widths = (#{Enum.join(widths, ", ")})")
    vars = PWC.lineCounts(str, widths)
    IO.puts("Output: (#{to_string(vars[:lines])}, " <>
            "#{to_string(vars[:line_width])})#{vars[:explain]}")
  end
end

IO.puts("Example 1:")
PWC.solution(
  "abcdefghijklmnopqrstuvwxyz",
  [10,10,10,10,10,10,10,10,10,10,10,10,10,
   10,10,10,10,10,10,10,10,10,10,10,10,10]
)

IO.puts("\nExample 2:")
PWC.solution(
  "bbbcccdddaaa",
  [ 4,10,10,10,10,10,10,10,10,10,10,10,10,
   10,10,10,10,10,10,10,10,10,10,10,10,10]
)

IO.puts("\nExample 3:")
PWC.solution(
  "thequickbrownfoxjumpedoverthelazydog",
  [7,8,7,8,7,5,8,8,4,4,8,4,12,
   8,8,8,8,5,6,4,8,8,12,8,8,7]
)
$ elixir/ch-2.exs
Example 1:
Input: $str = 'abcdefghijklmnopqrstuvwxyz'
       @widths = (10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10)
Output: (3, 60)
Line 1: abcdefghij (100 pixels)
Line 2: klmnopqrst (100 pixels)
Line 3: uvwxyz (60 pixels)

Example 2:
Input: $str = 'bbbcccdddaaa'
       @widths = (4, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10)
Output: (2, 4)
Line 1: bbbcccdddaa (98 pixels)
Line 2: a (4 pixels)

Example 3:
Input: $str = 'thequickbrownfoxjumpedoverthelazydog'
       @widths = (7, 8, 7, 8, 7, 5, 8, 8, 4, 4, 8, 4, 12, 8, 8, 8, 8, 5, 6, 4, 8, 8, 12, 8, 8, 7)
Output: (3, 65)
Line 1: thequickbrownf (100 pixels)
Line 2: oxjumpedovert (95 pixels)
Line 3: helazydog (65 pixels)

The code can be found on GitHub at https://github.com/packy/perlweeklychallenge-club/tree/master/challenge-267/packy-anderson/elixir