/// <summary>
/// Adapts a string to fit across a specified width. Whitespace is added between words to
/// make the string the correct width.
/// </summary>
/// <param name="source">The string to justify.</param>
/// <param name="width">The justified width.</param>
/// <returns>The string adapted to with specified width.</returns>
public static string Justify(this string source, int width)
{
const char whitespaceChar = ' ';
// Short circuit if no work to do.
if (source == null)
{
return null;
}
if (width < source.Length)
{
throw new ArgumentException(nameof(width), "Justification width must be >= source string length.");
}
// First pass through the string, find the total number of words. We need this to know how many characters
// to insert between non-whitespace clusters.
int nonWhitespaceClusterCount = source.GetNonWhitespaceClusterCount();
// Short circuit for single word.
if (nonWhitespaceClusterCount <= 1)
{
return source.PadRight(width);
}
int whitespaceCharsToInsertPerGap = (width - source.Length) / (nonWhitespaceClusterCount - 1);
var remainingWhitespaceCharsToDistribute = (width - source.Length) % (nonWhitespaceClusterCount - 1);
// Create the destination, pre-allocate since we know how big this will be.
var justifiedString = new char[width];
int justifiedStringOffset = 0;
var inWhitespaceGap = false;
foreach (var currentCharacter in source)
{
justifiedString[justifiedStringOffset++] = currentCharacter;
var currentCharIsWhitespace = char.IsWhiteSpace(currentCharacter);
if (!inWhitespaceGap && currentCharIsWhitespace)
{
for(var i = 0; i < whitespaceCharsToInsertPerGap; i++)
{
justifiedString[justifiedStringOffset++] = whitespaceChar;
}
// Don't like this. All extra spaces are added from left to right.
// TODO: Distribute the spaces a little better. Each end? Start in middle? All at end? Talk to the PM, see what they want done.
if (remainingWhitespaceCharsToDistribute-- > 0)
{
justifiedString[justifiedStringOffset++] = whitespaceChar;
}
}
inWhitespaceGap = currentCharIsWhitespace;
}
return new string(justifiedString);
}
/// <summary>
/// In order to justify a string we need the number of non-whitespace character groups between which we'll
/// add the extra whitespace.
/// </summary>
/// <param name="source">The string to analyze</param>
/// <returns>The number of non-whitespace clusters.</returns>
private static int GetNonWhitespaceClusterCount(this string source)
{
int nonWhitespaceClusterCount = 0;
if (source != null)
{
bool inWord = false;
foreach (var currentChar in source)
{
if (char.IsWhiteSpace(currentChar))
{
inWord = false;
}
else if (!inWord)
{
nonWhitespaceClusterCount++;
inWord = true;
}
}
}
return nonWhitespaceClusterCount;
}