grizzled.string.WordWrapper.scala Maven / Gradle / Ivy
                 Go to download
                
        
                    Show more of this group  Show more artifacts with this name
Show all versions of grizzled-scala_2.13.0-RC2 Show documentation
                Show all versions of grizzled-scala_2.13.0-RC2 Show documentation
A general-purpose Scala utility library
                
             The newest version!
        
        package grizzled.string
import scala.annotation.tailrec
import scala.sys.SystemProperties
/** Wraps strings on word boundaries to fit within a proscribed output
  * width. The wrapped string may have a prefix or not; prefixes are useful
  * for error messages, for instance. You tell a `WordWrapper` about
  * a prefix by passing a non-empty prefix to the constructor.
  *
  * Examples:
  *
  * {{{Unable to open file /usr/local/etc/wombat: No such file or directory}}}
  *
  * might appear like this without a prefix:
  *
  * {{{
  * Unable to open file /usr/local/etc/wombat: No such file or
  * directory
  * }}}
  *
  * and like this if the prefix is "myprog:"
  *
  * {{{
  * myprog: Unable to open file /usr/local/etc/wombat: No such
  *         file or directory
  * }}}
  *
  * Alternatively, if the output width is shortened, the same message
  * can be made to wrap something like this:
  *
  * {{{
  * myprog: Unable to open file
  *         /usr/local/etc/wombat:
  *         No such file or
  *         directory
  * }}}
  *
  * Note how the wrapping logic will "tab" past the prefix on wrapped
  * lines.
  *
  * This method also supports the notion of an indentation level, which is
  * independent of the prefix. A non-zero indentation level causes each line,
  * including the first line, to be indented that many characters. Thus,
  * initializing a `WordWrapper` object with an indentation value of 4
  * will cause each output line to be preceded by 4 blanks. (It's also
  * possible to change the indentation character from a blank to any other
  * character.
  *
  * Notes
  *
  * - The class does not do any special processing of tab characters.
  *   Embedded tab characters can have surprising (and unwanted) effects
  *   on the rendered output.
  * - Wrapping an already wrapped string is an invitation to trouble.
  *
  * @param wrapWidth   the number of characters after which to wrap each line
  * @param indentation how many characters to indent
  * @param prefix      the prefix to use, or "" for none. Cannot be null.
  * @param ignore      set of characters to ignore when calculating wrapping.
  *                    This feature can be useful when certain characters
  *                    represent escape characters, and you intend to
  *                    post-process the wrapped string.
  * @param indentChar  the indentation character to use.
  */
final case class WordWrapper(wrapWidth:    Int = 79,
                             indentation:  Int = 0,
                             prefix:       String = "",
                             ignore:       Set[Char] = Set.empty[Char],
                             indentChar:   Char = ' ') {
  require(Option(prefix).isDefined) // null check
  private val prefixLength = wordLen(prefix)
  private val lineSep = (new SystemProperties).getOrElse("line.separator", "\n")
  /** Wrap a string, using the wrap width, prefix, indentation and indentation
    * character that were specified to the `WordWrapper` constructor.
    * The resulting string may have embedded newlines in it.
    *
    * @param s the string to wrap
    * @return the wrapped string
    */
  def wrap(s: String): String = {
    import scala.collection.mutable.ArrayBuffer
    import grizzled.string.Implicits.String._
    val indentString = indentChar.toString
    val prefixIndentChars = indentString * prefixLength
    val indentChars = indentString * indentation
    val buf = new ArrayBuffer[String]
    def assembleLine(prefix: String, buf: Vector[String]): String =
      prefix + indentChars + buf.mkString(" ")
    def wrapOneLine(line: String, prefix: String): String = {
      @tailrec
      def wrapNext(words:     List[String],
                   curLine:   Vector[String],
                   curPrefix: String,
                   lines:     Vector[String]): (Vector[String], String) = {
        words match {
          case Nil if curLine.isEmpty => (lines, curPrefix)
          case Nil => (lines :+ assembleLine(curPrefix, curLine), curPrefix)
          case word :: rest =>
            val wordLength   = word.length
            val prefixLength = wordLen(curPrefix)
            // Total number of blanks between words in this line = number of
            // words - 1 (since we don't put a blank at the end of the line).
            val totalBlanks  = scala.math.max(curLine.length - 1, 0)
            // Combined length of all words in the current line.
            val wordLengths = curLine.map(wordLen).sum
            // The length of the line being assembled is the length of each
            // word in the curLine buffer, plus a single blank between them,
            // plus any prefix and indentation. to map the words to their
            // lengths, and a fold-left operation to sum them up.
            val currentLength = totalBlanks + wordLengths +
                                curPrefix.length + indentation
            if ((wordLength + currentLength + 1) > wrapWidth) {
              // Adding this word to the current line would exceed the wrap
              // width. Put the line together, save it, and start a new one.
              val line = assembleLine(curPrefix, curLine)
              wrapNext(rest, Vector(word), prefixIndentChars, lines :+ line)
            }
            else {
              // It's safe to put this word in the current line.
              wrapNext(rest, curLine :+ word, curPrefix, lines)
            }
        }
      }
      val (lineOut, curPrefix) = wrapNext(line.split("""\s""").toList,
                                          Vector.empty[String],
                                          prefix,
                                          Vector.empty[String])
      if (lineOut.nonEmpty)
        lineOut.mkString(lineSep).rtrim
      else
        ""
    }
    val lines = s.split(lineSep)
    buf += wrapOneLine(lines(0), prefix)
    for (line <- lines.drop(1))
      buf += wrapOneLine(line, prefixIndentChars)
    buf mkString lineSep
  }
  private def wordLen(word: String) = word.filter(! ignore.contains(_)).length
}
    © 2015 - 2025 Weber Informatics LLC | Privacy Policy