Utils
StringFormatter.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BitShelter.Utils
{
// https://github.com/alphaleonis/AlphaVSS
///
/// Clast performing various formatting operations on strings based on fixed width characters.
///
public static clast StringFormatter
{
///
/// Represents a column to be used with .
///
public struct ColumnInfo
{
#region Private fields
private WordWrappingMethod m_workdWrappingMethod;
private Alignment m_alignment;
private int m_width;
private string m_content;
private VerticalAlignment m_verticalAlignment;
#endregion
#region Constructors
///
/// Initializes a new instance of the struct.
///
/// The width of the column in (fixed-width) characters.
/// The content of this column.
/// The alignment to use for this column.
/// The vertical alignment to use for this column
/// The word wrapping method to use for this column
public ColumnInfo(int width, string content, Alignment alignment, VerticalAlignment verticalAlignment, WordWrappingMethod method)
{
m_width = width;
m_content = content;
m_alignment = alignment;
m_verticalAlignment = verticalAlignment;
m_workdWrappingMethod = method;
}
///
/// Initializes a new instance of the struct.
///
/// The width of the column in (fixed-width) characters.
/// The content of this column.
/// The alignment to use for this column.
/// The vertical alignment to use for this column
public ColumnInfo(int width, string content, Alignment alignment, VerticalAlignment verticalAlignment)
: this(width, content, alignment, verticalAlignment, WordWrappingMethod.Optimal)
{
}
///
/// Initializes a new instance of the struct.
///
/// The width of the column in (fixed-width) characters.
/// The content of this column.
/// The alignment to use for this column.
/// The word wrapping method used will be the one described by .
public ColumnInfo(int width, string content, Alignment alignment)
: this(width, content, alignment, VerticalAlignment.Top)
{
}
///
/// Initializes a new instance of the struct.
///
/// The width of the column in (fixed-width) characters.
/// The content of this column.
/// The word wrapping method used will be the one described by , and
/// each line in this column will be left aligned.
public ColumnInfo(int width, string content)
: this(width, content, Alignment.Left)
{
}
#endregion
#region Public methods
///
/// Implements the operator ==.
///
/// The LHS.
/// The RHS.
/// The result of the operator.
public static bool operator ==(ColumnInfo lhs, ColumnInfo rhs)
{
return lhs.Equals(rhs);
}
///
/// Implements the operator !=.
///
/// The LHS.
/// The RHS.
/// The result of the operator.
public static bool operator !=(ColumnInfo lhs, ColumnInfo rhs)
{
return !lhs.Equals(rhs);
}
///
/// Indicates whether this instance and a specified object are equal.
///
/// Another object to compare to.
///
/// true if obj and this instance are the same type and represent the same value; otherwise, false.
///
public override bool Equals(object obj)
{
if (!(obj is ColumnInfo))
return false;
ColumnInfo ci = (ColumnInfo)obj;
return Width.Equals(ci.Width) && Content.Equals(ci.Content) && Alignment.Equals(ci.Alignment) && WordWrappingMethod.Equals(ci.WordWrappingMethod);
}
///
/// Returns the hash code for this instance.
///
///
/// A 32-bit signed integer that is the hash code for this instance.
///
public override int GetHashCode()
{
return Width.GetHashCode() ^ Content.GetHashCode() ^ Alignment.GetHashCode() ^ WordWrappingMethod.GetHashCode();
}
#endregion
#region Public properties
///
/// Gets the width of this column in fixed width characters.
///
/// the width of this column in fixed width characters.
public int Width
{
get { return m_width; }
}
///
/// Gets the content of this column.
///
/// The content of this column.
public string Content
{
get { return m_content; }
}
///
/// Gets the alignment of this column.
///
/// The alignment of this column.
public Alignment Alignment
{
get { return m_alignment; }
}
///
/// Gets the word wrapping method to use for this column.
///
/// The the word wrapping method to use for this column.
public WordWrappingMethod WordWrappingMethod
{
get { return m_workdWrappingMethod; }
}
///
/// Gets or sets the vertical alignment of the contents of this column.
///
/// The vertical alignment of this column.
public VerticalAlignment VerticalAlignment
{
get { return m_verticalAlignment; }
set { m_verticalAlignment = value; }
}
#endregion
}
///
/// Represents the alignment of text for use with
///
public enum Alignment
{
///
/// Text will be left aligned
///
Left,
///
/// Text will be right aligned
///
Right,
///
/// Text will be centered within the specified field
///
Center,
///
/// Spaces between words will be expanded so the string takes up the full width of the field.
///
Justified
}
///
/// Represents the vertical alignment to use for methods in .
///
public enum VerticalAlignment
{
///
/// Indicates that the text should be aligned to the top of the field
///
Top,
///
/// Indicates that the text should be aligned to the bottom of the field
///
Bottom,
///
/// Indicates that the text should be aligned to the (vertical) middle of the field
///
Middle
}
///
/// Represents the word wrapping method to use for various operations performed by .
///
public enum WordWrappingMethod
{
///
/// Uses a greedy algorithm for performing the word wrapping,
/// that puts as many words on a line as possible, then moving on to the next line to do the
/// same until there are no more words left to place.
///
/// This is the fastest method, but will often create a less esthetically pleasing result than the
/// method.
Greedy,
///
/// Uses an algorithm attempting to create an optimal solution of breaking the lines, where the optimal solution is the one
/// where the remaining space on the end of each line is as small as possible.
///
/// This method creates esthetically more pleasing results than those created by the method,
/// but it does so at a significantly slower speed. This method will work fine for wrapping shorter strings for console
/// output, but should probably not be used for a real time WYSIWYG word processor or something similar.
Optimal
}
///
/// Specifies how strings will be cropped in case they are too long for the specified field. Used by .
///
public enum Cropping
{
///
/// The left hand side of the string will be cropped, and only the rightmost part of the string will remain.
///
Left,
///
/// The right hand side of the string will be cropped, and only the leftmost part of the string will remain.
///
Right,
///
/// Both ends of the string will be cropped and only the center part will remain.
///
Both
}
#region Public methods
///
/// Aligns the specified string within a field of the desired width, cropping it if it doesn't fit, and expanding it otherwise.
///
/// The string to align.
/// The width of the field in characters in which the string should be fitted.
/// The aligmnent that will be used for fitting the string in the field in case it is shorter than the specified field width.
///
/// A string exactly characters wide, containing the specified string fitted
/// according to the parameters specified.
///
///
/// If the string consists of several lines, each line will be aligned according to the specified parameters.
/// The padding character used will be the normal white space character (' '), and the ellipsis used will be the
/// string "...". Cropping will be done on the right hand side of the string.
///
/// was a null reference (Nothing in Visual Basic)
/// The specified was less than, or equal to zero.
/// is less than the length of the specified , or,
/// the cropping specified is and was less than twice the
/// length of the
public static string Align(string str, int width, Alignment alignment)
{
return Align(str, width, alignment, Cropping.Right, "...");
}
///
/// Aligns the specified string within a field of the desired width, cropping it if it doesn't fit, and expanding it otherwise.
///
/// The string to align.
/// The width of the field in characters in which the string should be fitted.
/// The aligmnent that will be used for fitting the string in the field in case it is shorter than the specified field width.
/// The method that will be used for cropping if the string is too wide to fit in the specified width.
///
/// A string exactly characters wide, containing the specified string fitted
/// according to the parameters specified.
///
///
/// If the string consists of several lines, each line will be aligned according to the specified parameters.
/// The padding character used will be the normal white space character (' '), and the ellipsis used will be the
/// string "...".
///
/// was a null reference (Nothing in Visual Basic)
/// The specified was less than, or equal to zero.
/// is less than the length of the specified , or,
/// the cropping specified is and was less than twice the
/// length of the
public static string Align(string str, int width, Alignment alignment, Cropping cropping)
{
return Align(str, width, alignment, cropping, "...");
}
///
/// Aligns the specified string within a field of the desired width, cropping it if it doesn't fit, and expanding it otherwise.
///
/// The string to align.
/// The width of the field in characters in which the string should be fitted.
/// The aligmnent that will be used for fitting the string in the field in case it is shorter than the specified field width.
/// The method that will be used for cropping if the string is too wide to fit in the specified width.
/// A string that will be inserted at the cropped side(s) of the string to denote that the string has been cropped.
///
/// A string exactly characters wide, containing the specified string fitted
/// according to the parameters specified.
///
///
/// If the string consists of several lines, each line will be aligned according to the specified parameters.
/// The padding character used will be the normal white space character (' ').
///
/// was a null reference (Nothing in Visual Basic)
/// The specified was less than, or equal to zero.
/// is less than the length of the specified , or,
/// the cropping specified is and was less than twice the
/// length of the
public static string Align(string str, int width, Alignment alignment, Cropping cropping, string ellipsis)
{
return Align(str, width, alignment, cropping, ellipsis, ' ');
}
///
/// Aligns the specified string within a field of the desired width, cropping it if it doesn't fit, and expanding it otherwise.
///
/// The string to align.
/// The width of the field in characters in which the string should be fitted.
/// The aligmnent that will be used for fitting the string in the field in case it is shorter than the specified field width.
/// The method that will be used for cropping if the string is too wide to fit in the specified width.
/// A string that will be inserted at the cropped side(s) of the string to denote that the string has been cropped.
/// The character that will be used for padding the string in case it is shorter than the specified field width.
///
/// A string exactly characters wide, containing the specified string fitted
/// according to the parameters specified.
///
/// If the string consists of several lines, each line will be aligned according to the specified parameters.
/// was a null reference (Nothing in Visual Basic)
/// The specified was less than, or equal to zero.
/// is less than the length of the specified , or,
/// the cropping specified is and was less than twice the
/// length of the
public static string Align(string str, int width, Alignment alignment, Cropping cropping, string ellipsis, char padCharacter)
{
if (str == null)
throw new ArgumentNullException("str");
if (width width)
{
dest.Append('\n');
dest.Append(word.ToString(0, width));
word.Remove(0, width);
spaceLeft = width;
}
dest.Append('\n');
dest.Append(word);
spaceLeft = width - word.Length;
word.Length = 0;
}
else
{
dest.Append(word);
spaceLeft -= word.Length;
word.Length = 0;
}
if (ch != -1 && spaceLeft > 0)
{
dest.Append(' ');
spaceLeft--;
}
}
while (ch != -1);
}
return dest.ToString();
}
///
/// Performs word wrapping on the specified string, making it fit within the specified width and additionally aligns each line
/// according to the specified.
///
/// The string to word wrap and align.
/// The width of the field in which to fit the string.
/// The method to use for word wrapping.
/// The alignment to use for each line of the resulting string.
/// A word wrapped version of the original string aligned and padded as specified.
/// If padding is required, the normal simple white space character (' ') will be used.
/// was a null reference (Nothing in Visual Basic)
/// The specified was less than, or equal to zero.
public static string WordWrap(string str, int width, WordWrappingMethod method, Alignment alignment)
{
return WordWrap(str, width, method, alignment, ' ');
}
///
/// Performs word wrapping on the specified string, making it fit within the specified width and additionally aligns each line
/// according to the specified.
///
/// The string to word wrap and align.
/// The width of the field in which to fit the string.
/// The method to use for word wrapping.
/// The alignment to use for each line of the resulting string.
/// The character to use for padding lines that are shorter than the specified width.
/// A word wrapped version of the original string aligned and padded as specified.
/// was a null reference (Nothing in Visual Basic)
/// The specified was less than, or equal to zero.
public static string WordWrap(string str, int width, WordWrappingMethod method, Alignment alignment, char padCharacter)
{
return StringFormatter.Align(WordWrap(str, width, method), width, alignment, Cropping.Left, "", padCharacter);
}
///
/// Splits the specified strings at line breaks, resulting in an indexed collection where each item represents one line of the
/// original string.
///
/// The string to split.
/// an indexed collection where each item represents one line of the
/// original string.
/// This might seem identical to the String.Split method at first, but it is not exactly, since this method
/// recognizes line breaks in the three formats: "\n", "\r" and "\r\n". Note that any newline characters will not be present
/// in the returned collection.
public static IList SplitAtLineBreaks(string str)
{
return SplitAtLineBreaks(str, false);
}
///
/// Splits the specified strings at line breaks, resulting in an indexed collection where each item represents one line of the
/// original string.
///
/// The string to split.
/// if set to true any empty lines will be removed from the resulting collection.
///
/// an indexed collection where each item represents one line of the
/// original string.
///
/// This might seem identical to the String.Split method at first, but it is not exactly, since this method
/// recognizes line breaks in the three formats: "\n", "\r" and "\r\n". Note that any newline characters will not be present
/// in the returned collection.
public static IList SplitAtLineBreaks(string str, bool removeEmptyLines)
{
IList result = new List();
StringBuilder temp = new StringBuilder();
for (int i = 0; i < str.Length; i++)
{
if (i < str.Length - 1 && str[i] == '\r' && str[i + 1] == '\n')
i++;
if (str[i] == '\n' || str[i] == '\r')
{
if (!removeEmptyLines || temp.Length > 0)
result.Add(temp.ToString());
temp.Length = 0;
}
else
{
temp.Append(str[i]);
}
}
if (temp.Length > 0)
result.Add(temp.ToString());
return result;
}
///
/// Retrieves the number of words in the specified string.
///
/// The string in which to count the words.
/// The number of words in the specified string.
/// A word here is defined as any number (greater than zero) of non-whitespace characters, separated from
/// other words by one or more white space characters.
/// was a null reference (Nothing in Visual Basic)
public static int GetWordCount(string str)
{
if (str == null)
throw new ArgumentNullException("str");
int count = 0;
bool readingWord = false;
for (int i = 0; i < str.Length; i++)
{
if (!char.IsWhiteSpace(str[i]))
readingWord = true;
else if (readingWord)
{
count++;
readingWord = false;
}
}
if (readingWord)
count++;
return count;
}
///
/// Formats several fixed width strings into columns of the specified widths, performing word wrapping and alignment as specified.
///
/// The indentation (number of white space characters) to use before the first column.
/// The spacing to use in between columns.
/// An array of the objects representing the columns to use.
/// A single string that when printed will represent the original strings formatted as specified in each
/// object.
/// A for a column was a null reference (Nothing in Visual Basic)
/// The specified for a column was less than, or equal to zero.
/// was less than zero, or, was less than
/// zero, or, no columns were specified.
public static string FormatInColumns(int indent, int columnSpacing, params ColumnInfo[] columns)
{
if (columnSpacing < 0)
throw new ArgumentException("columnSpacing must not be less than zero", "columnSpacing");
if (indent < 0)
throw new ArgumentException("indent must not be less than zero", "indent");
if (columns.Length == 0)
return "";
IList[] strings = new IList[columns.Length];
int totalLineCount = 0;
// Calculate the total number of lines that needs to be printed
for (int i = 0; i < columns.Length; i++)
{
strings[i] = SplitAtLineBreaks(WordWrap(columns[i].Content, columns[i].Width, columns[i].WordWrappingMethod, columns[i].Alignment, ' '), false);
totalLineCount = Math.Max(strings[i].Count, totalLineCount);
}
// Calculate the first line on which each column should start to print, based
// on its vertical alignment.
int[] startLine = new int[columns.Length];
for (int col = 0; col < columns.Length; col++)
{
switch (columns[col].VerticalAlignment)
{
case VerticalAlignment.Top:
startLine[col] = 0;
break;
case VerticalAlignment.Bottom:
startLine[col] = totalLineCount - strings[col].Count;
break;
case VerticalAlignment.Middle:
startLine[col] = (totalLineCount - strings[col].Count) / 2;
break;
default:
throw new NotSupportedException("Invalid vertical alignment specified.");
}
}
StringBuilder result = new StringBuilder();
for (int line = 0; line < totalLineCount; line++)
{
result.Append(' ', indent);
for (int col = 0; col < columns.Length; col++)
{
if (line >= startLine[col] && line - startLine[col] < strings[col].Count)
{
result.Append(strings[col][line - startLine[col]]);
}
else
{
result.Append(' ', columns[col].Width);
}
if (col < columns.Length - 1)
result.Append(' ', columnSpacing);
}
if (line != totalLineCount - 1)
result.Append(Environment.NewLine);
}
return result.ToString();
}
#endregion
#region Private clastes
///
/// Clast performing an "optimal solution" word wrapping creating a somewhat more estetically pleasing layout.
///
/// This is based on the
/// "optimal solution" as described on the Wikipedia page for "Word Wrap" (http://en.wikipedia.org/wiki/Word_wrap).
/// The drawback of this method compared to the simple "greedy" technique is that this is much, much slower. However for
/// short strings to print as console messages this will not be a problem, but using it in a WYSIWYG word processor is probably
/// not a very good idea.
private clast OptimalWordWrappedString
{
#region Constructors
public OptimalWordWrappedString(string s, int lineWidth)
{
string[] lines = s.Split('\n');
for (int c = 0; c < lines.Length; c++)
{
m_str = lines[c].Trim();
m_lineWidth = lineWidth;
BuildWordList(m_str, m_lineWidth);
m_costCache = new int[m_wordList.Length, m_wordList.Length];
for (int x = 0; x < m_wordList.Length; x++)
for (int y = 0; y < m_wordList.Length; y++)
m_costCache[x, y] = -1;
m_cache = new LineBreakResult[m_wordList.Length];
Stack stack = new Stack();
LineBreakResult last = new LineBreakResult(0, m_wordList.Length - 1);
stack.Push(last.K);
while (last.K >= 0)
{
last = FindLastOptimalBreak(last.K);
if (last.K >= 0)
stack.Push(last.K);
}
int start = 0;
while (stack.Count > 0)
{
int next = stack.Pop();
m_result.Append(GetWords(start, next));
if (stack.Count > 0)
m_result.Append(Environment.NewLine);
start = next + 1;
}
if (c != lines.Length - 1)
m_result.Append(Environment.NewLine);
}
m_wordList = null;
m_cache = null;
m_str = null;
m_costCache = null;
}
#endregion
#region Public methods
public override string ToString()
{
return m_result.ToString();
}
#endregion
#region Private methods
private string GetWords(int i, int j)
{
int start = m_wordList[i].pos;
int end = (j + 1 >= m_wordList.Length) ? m_str.Length : m_wordList[j + 1].pos - (m_wordList[j + 1].spacesBefore - m_wordList[j].spacesBefore);
return m_str.Substring(start, end - start);
}
private struct WordInfo
{
public int spacesBefore;
public int pos;
public int length;
public int totalLength;
}
private void BuildWordList(string s, int lineWidth)
{
Debug.astert(!s.Contains("\n"));
List wordListAL = new List();
bool lookingForWs = false;
WordInfo we = new WordInfo();
we.pos = 0;
int spaces = 0;
int totalLength = 0;
for (int i = 0; i < s.Length; i++)
{
char ch = s[i];
if (lookingForWs && ch == ' ')
{
spaces++;
if (we.pos != i)
wordListAL.Add(we);
we = new WordInfo();
we.spacesBefore = spaces;
we.pos = i + 1;
lookingForWs = false;
continue;
}
else if (ch != ' ')
{
lookingForWs = true;
}
we.length++;
totalLength++;
we.totalLength = totalLength;
if (we.length == lineWidth)
{
wordListAL.Add(we);
we = new WordInfo();
we.spacesBefore = spaces;
we.pos = i + 1;
}
}
wordListAL.Add(we);
m_wordList = wordListAL.ToArray();
}
private int SumWidths(int i, int j)
{
return i == 0 ? m_wordList[j].totalLength : m_wordList[j].totalLength - m_wordList[i - 1].totalLength;
}
private int GetCost(int i, int j)
{
int cost = m_costCache[i, j];
if (cost == -1)
{
cost = m_lineWidth - (m_wordList[j].spacesBefore - m_wordList[i].spacesBefore) - SumWidths(i, j);
cost = cost < 0 ? m_infinity : cost * cost;
m_costCache[i, j] = cost;
}
return cost;
}
private LineBreakResult FindLastOptimalBreak(int j)
{
if (m_cache[j] != null)
{
return m_cache[j];
}
int cost = GetCost(0, j);
if (cost < m_infinity)
{
return new LineBreakResult(cost, -1);
}
LineBreakResult min = new LineBreakResult();
for (int k = 0; k < j; k++)
{
int result = FindLastOptimalBreak(k).Cost + GetCost(k + 1, j);
if (result < min.Cost)
{
min.Cost = result;
min.K = k;
}
}
m_cache[j] = min;
return min;
}
#endregion
#region Private types
private clast LineBreakResult
{
public LineBreakResult()
{
Cost = m_infinity;
K = -1;
}
public LineBreakResult(int cost, int k)
{
this.Cost = cost;
this.K = k;
}
public int Cost;
public int K;
}
#endregion
#region Private fields
private WordInfo[] m_wordList;
private StringBuilder m_result = new StringBuilder();
private LineBreakResult[] m_cache;
private string m_str;
private int m_lineWidth;
private const int m_infinity = int.MaxValue / 2;
// We need a rectangular array here, so this warning is unwarranted.
[System.Diagnostics.Codeastysis.SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Member")]
private int[,] m_costCache;
#endregion
}
#endregion
}
}