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