csharp/alexis-/BitShelter/BitShelter.Common/Utils/StringFormatter.cs

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

  }
}