Skip to content

Instantly share code, notes, and snippets.

@carymrobbins
Last active January 17, 2024 14:02
Show Gist options
  • Save carymrobbins/7b8ed52cd6ea186dbdf8 to your computer and use it in GitHub Desktop.
Save carymrobbins/7b8ed52cd6ea186dbdf8 to your computer and use it in GitHub Desktop.
Pretty print Scala case classes and other data structures.
/**
* Pretty prints a Scala value similar to its source represention.
* Particularly useful for case classes.
* @param a - The value to pretty print.
* @param indentSize - Number of spaces for each indent.
* @param maxElementWidth - Largest element size before wrapping.
* @param depth - Initial depth to pretty print indents.
* @return
*/
private def prettyPrint(a: Any, indentSize: Int = 2, maxElementWidth: Int = 30, depth: Int = 0): String = {
val indent = " " * depth * indentSize
val fieldIndent = indent + (" " * indentSize)
val thisDepth = prettyPrint(_: Any, indentSize, maxElementWidth, depth)
val nextDepth = prettyPrint(_: Any, indentSize, maxElementWidth, depth + 1)
a match {
// Make Strings look similar to their literal form.
case s: String =>
val replaceMap = Seq(
"\n" -> "\\n",
"\r" -> "\\r",
"\t" -> "\\t",
"\"" -> "\\\""
)
'"' + replaceMap.foldLeft(s) { case (acc, (c, r)) => acc.replace(c, r) } + '"'
// For an empty Seq just use its normal String representation.
case xs: Seq[_] if xs.isEmpty => xs.toString()
case xs: Seq[_] =>
// If the Seq is not too long, pretty print on one line.
val resultOneLine = xs.map(nextDepth).toString()
if (resultOneLine.length <= maxElementWidth) return resultOneLine
// Otherwise, build it with newlines and proper field indents.
val result = xs.map(x => s"\n$fieldIndent${nextDepth(x)}").toString()
result.substring(0, result.length - 1) + "\n" + indent + ")"
// Product should cover case classes.
case p: Product =>
val prefix = p.productPrefix
// We'll use reflection to get the constructor arg names and values.
val cls = p.getClass
val fields = cls.getDeclaredFields.filterNot(_.isSynthetic).map(_.getName)
val values = p.productIterator.toSeq
// If we weren't able to match up fields/values, fall back to toString.
if (fields.length != values.length) return p.toString
fields.zip(values).toList match {
// If there are no fields, just use the normal String representation.
case Nil => p.toString
// If there is just one field, let's just print it as a wrapper.
case (_, value) :: Nil => s"$prefix(${thisDepth(value)})"
// If there is more than one field, build up the field names and values.
case kvps =>
val prettyFields = kvps.map { case (k, v) => s"$fieldIndent$k = ${nextDepth(v)}" }
// If the result is not too long, pretty print on one line.
val resultOneLine = s"$prefix(${prettyFields.mkString(", ")})"
if (resultOneLine.length <= maxElementWidth) return resultOneLine
// Otherwise, build it with newlines and proper field indents.
s"$prefix(\n${prettyFields.mkString(",\n")}\n$indent)"
}
// If we haven't specialized this type, just use its toString.
case _ => a.toString
}
}
@bakenezumi
Copy link

Very easy to use with REPL. Thank you!

@swoogles
Copy link

swoogles commented Jan 4, 2018

Just a heads up - since this uses reflection, it's not going to work if you're compiling with ScalaJS.

@Andrei-Pozolotin
Copy link

thank you

@xtulnx
Copy link

xtulnx commented Sep 19, 2018

thank you

@gekomad
Copy link

gekomad commented Jun 25, 2019

Good Job, you should add
case opt: Some[_] => "Some(" + prettyPrint(opt.get) + ")"

@myDisconnect
Copy link

myDisconnect commented Jul 18, 2019

@TakehideSoh
Copy link

thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment