The (Semi-Long) Solution

It's taken me a while to perfect (probably much, much longer than an interviewer would have allowed for), but I've come up with an elegant, 162 line OOP solution to this problem. I included functionality to allow for the justifying of a single string, array of strings (already separated into lines) or a long string that needs to be broken up into lines of a maximum width first. Demos follow the code block.

Important Note: This class will only work in PHP 5.4. I realized this when running a version on my own server PHP (5.3.6) to get profiling stats with XDebug. PHP 5.3 complains about my use of $this in the anonymous function. A quick check of the docs on anonymous functions reveals that $this could not be used in the context of an anonymous function until 5.4. If anyone can find a clean workaround to this, please drop it in the comments. Added support for PHP 5.3!

<?php class Justifier { private $text; public function __construct($text) { if(!is_string($text) && !is_array($text)) { throw new InvalidArgumentException('Expected a string or an array of strings, instead received type: ' . gettype($text)); } if(is_array($text)) { // String arrays must be converted to JustifierLine arrays $this->text = array_map(function($line) { return JustifierLine::fromText($line); }, $text); } else { // Single line of text input $this->text = $text; } } public function format($width = null) { // Strings have to be broken into an array and then jusitifed if(is_string($this->text)) { if($width == null) { throw new InvalidArgumentException('A width must be provided for separation when an un-split string is provided'); } if($width <= 0) { throw new InvalidArgumentException('Expected a positive, non-zero width, instead received width of ' . $width); } // Break up a JustifierLine of all text until each piece is smaller or equal to $width $lines = array(JustifierLine::fromText($this->text)); $count = 0; $newLine = $lines[0]->breakAtColumn($width); while($newLine !== null) { $lines[] = $newLine; $newLine = $lines[++$count]->breakAtColumn($width); } } else { $lines = $this->text; // Allow for fluid width (uses longest line with single space) if($width == NULL) { $width = -1; foreach($lines as $line) { // Width of line = Sum of the lengths of the words and the spaces (number of words - 1) $newWidth = $line->calculateWordsLength() + $line->countWords() - 1; if($newWidth > $width) { // Looking for the longest line $width = $newWidth; } } } } // Justify each element of array (PHP 5.4 ONLY) //$output = array_map(function($line) use ($width) { // return $this->justify($line, $width); //}, $lines); // Support for PHP 5.3 $output = array(); foreach($lines as $line) { $output = $this->justify($line, $width); } // If a single-line is passed in, a single line is returned if(count($output)) { return $output[0]; } return $output; } private function justify(JustifierLine $line, $width) { // Retrieve already calculated line information $words = $line->extractWords(); $spaces = $line->countWords() - 1; $wordLens = $line->findWordLengths(); $wordsLen = $line->calculateWordsLength(); $minWidth = $wordsLen + $spaces; $output = ''; if($minWidth > $width) { throw new LengthException('A minimum width of ' . $minWidth . ' was required, but a width of ' . $width . ' was given instead'); } // No spaces means only one word (center align) if($spaces == 0) { return str_pad($words[0], $width, ' ', STR_PAD_BOTH); } for(;$spaces > 0; $spaces--) { // Add next word to output and subtract its length from counters $output .= array_shift($words); $length = array_shift($wordLens); $wordsLen -= $length; $width -= $length; if($spaces == 1) { // Last Iteration return $output . str_repeat(' ', $width - $wordsLen) . $words[0]; } // Magic padding is really just simple math $padding = floor(($width - $wordsLen) / $spaces); $output .= str_repeat(' ', $padding); $width -= $padding; } } } class JustifierLine { private $words; private $numWords; private $wordLengths; private $wordsLength; public static function fromText($text) { // Split words into an array preg_match_all('/[^ ]+/', $text, $matches, PREG_PATTERN_ORDER); $words = $matches[0]; // Count words $numWords = count($words); // Find the length of each word $wordLengths = array_map('strlen', $words); //And Finally, calculate the total length of all words $wordsLength = array_reduce($wordLengths, function($result, $length) { return $result + $length; }, 0); return new JustifierLine($words, $numWords, $wordLengths, $wordsLength); } private function __construct($words, $numWords, $wordLengths, $wordsLength) { $this->words = $words; $this->numWords = $numWords; $this->wordLengths = $wordLengths; $this->wordsLength = $wordsLength; } public function extractWords() { return $this->words; } public function countWords() { return $this->numWords; } public function findWordLengths() { return $this->wordLengths; } public function calculateWordsLength() { return $this->wordsLength; } public function breakAtColumn($column) { // Avoid extraneous processing if we can determine no breaking can be done if($column >= ($this->wordsLength + $this->numWords - 1)) { return null; } $width = 0; $wordsLength = 0; for($i = 0; $i < $this->numWords; $i++) { // Add width of next word $width += $this->wordLengths[$i]; // If the line is overflowing past required $width if($width > $column) { // Remove overflow at end & create a new object with the overflow $words = array_splice($this->words, $i); $numWords = $this->numWords - $i; $this->numWords = $i; $wordLengths = array_splice($this->wordLengths, $i); $tempWordsLength = $wordsLength; $wordsLength = $this->wordsLength - $wordsLength; $this->wordsLength = $tempWordsLength; return new JustifierLine($words, $numWords, $wordLengths, $wordsLength); } $width++; // Assuming smallest spacing to fit // We also have to keep track of the total $wordsLength $wordsLength += $this->wordLengths[$i]; } return null; } }

Demos

Original Question (Justifying Lines of Text to width = 48)

You can pass in an array of many strings or just one string to Justifier . Calling Justifier::format($desired_length) will always return an array of justified lines *if an array of strings or string that required segmentation was passed to the constructor. Otherwise, a string will be returned. (Codepad Demo)

$jus = new Justifier(array( 'hello world there ok then', 'hello', 'ok then', 'two words', 'three ok words', '1 2 3 4 5 6 7 8 9' )); print_r( $jus->format(48) );

Output

Array ( [0] => hello world there ok then [1] => hello [2] => ok then [3] => two words [4] => three ok words [5] => 1 2 3 4 5 6 7 8 9 )

You may notice I omitted one of the OP's test lines. This is because it was 54 characters and would exceed the $desired_length passed to Justifier::format() . The function will throw an IllegalArgumentException for widths that aren't positive, non-zero numbers that exceed or equal to the minimum width. The minimum width is calculated by finding the longest line (of all the lines passed to the constructor) with single spacing.

Fluid Width Justifying With An Array of Strings

If you omit the width, Justifier will use the width of the longest line (of those passed to the constructor) when single spaced. This is the same calculation as finding the minimum width in the previous demo. (Codepad Demo)

$jus = new Justifier(array( 'hello world there ok then', 'hello', 'ok then', 'this string is almost certainly longer than 48 I think', 'two words', 'three ok words', '1 2 3 4 5 6 7 8 9' )); print_r( $jus->format() );

Output

Array ( [0] => hello world there ok then [1] => hello [2] => ok then [3] => this string is almost certainly longer than 48 I think [4] => two words [5] => three ok words [6] => 1 2 3 4 5 6 7 8 9 )

Justifying a Single String of Text (width = 48)

I've also included a feature in the class which allows you to pass a single, non-broken string to the constructor. This string can be of any length. When you call Justifier::format($desired_length) the string is broken into lines such that each line is filled with as much text as possible and justified before starting a new line. The class will complain with an InvalidArgumentException because you must provide a width into which it can break the string. If anyone can think of a sensible default or way to programmatically determine a default for a string, I'm completely open to suggestions. (Codepad Demo)

$jus = new Justifier( 'hello world there ok then hello ok then this string is almost certainly longer than 48 I think two words three ok words 1 2 3 4 5 6 7 8 9' ); print_r( $jus->format(48) );

Output