Justifying a paragraph of text: Part 3

Jun 08, 2023

Read part one here. Read part two here.

In the last part, I left here:

All that remains now is getting ValidLines out of the given array of words.

I just got around to writing the function that converts an array of strings into an array of valid lines that I can then pass/process through the validLineToParaLine function I wrote in part two.

validLineToParaLine :: Int -> ValidLine -> String
validLineToParaLine maxLen (ValidLine xs) =
  specialIntercalate xs (makeSpaces maxLen (ValidLine xs))
  # Str.joinWith ""

Here's how I approached the validline generation. I was going to rely on an accumulating recursive function (very common in functional/recursive programs):

  1. The function I'm going to write will keep track of a current valid line, a final array of valid lines, and the list of words to process.
  2. As a base case, if the list of words to process is empty, it's simply going to concatenate the final array of valid lines and the current valid line and return the whole thing. That will be my final valid line list!
  3. If the list of words to process is not empty:

In code:

listToValidLines :: Int -> Array String -> Array ValidLine
listToValidLines maxlen xs = helper (ValidLine []) [] xs
  where
    helper :: ValidLine -> Array ValidLine -> Array String -> Array ValidLine
    helper (ValidLine acc) final [] = snoc final (ValidLine acc)
    helper (ValidLine acc) final ys =
      case head ys of
        Maybe.Nothing -> helper (ValidLine acc) final (Maybe.fromMaybe [] $ tail ys)
        Maybe.Just wrd ->
          let
            tempValidLine = snoc acc wrd
            lengthTempValidLine = totalCharLength $ ValidLine tempValidLine
          in
            if lengthTempValidLine > maxlen
              then helper (ValidLine []) (snoc final (ValidLine acc)) ys
              else helper (ValidLine tempValidLine) final (Maybe.fromMaybe [] $ tail ys)

There's a bit of a Maybe wrangling because I'm using head and tail, but it's OK. The code is safer.

Now that I have a function that converts an array of words to an array of valid lines, I can simply map over this list to generate a list of justified lines!

justify :: Int -> Array String -> Array String
justify maxWidth = listToValidLines maxWidth >>> map (validLineToParaLine maxWidth)

maxWidth is the same as max length of a line.

I could've written it this way too:

justify :: Int -> Array String -> Array String
justify maxWidth xs = listToValidLines maxWidth xs # map (validLineToParaLine maxWidth)

Time to test:

> justify 16 ["This", "is", "an", "example", "of", "text", "justification", "folks,", "okay?"]
["This----is----an","example--of-text","justification","folks,-----okay?"]

We can join this text with "\n" to get a paragraph:

justify 16 ["This", "is", "an", "example", "of", "text", "justification", "folks,", "okay?"] # joinWith "\n"
This----is----an
example--of-text
justification
folks,-----okay?

The last rule in the puzzle was:

the last line can be left-aligned so just one space between the words is fine.

I think there are a couple of ways to accomplish this.

I could have used an indexed map in justify function to not justify the last item in the array of valid lines.

Or I could simply use regex to replace all multilpe spaces (ie, one or more spaces) with just one space in the last line.

Those are trivial, so leaving it here for now.


You can play around with the full-code here.