
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.11 Show documentation
Show all versions of grizzled-scala_2.11 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