org.organicdesign.indented.StringUtils.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Indented Show documentation
Show all versions of Indented Show documentation
Make debugging, pretty-print indented for easy reading.
The newest version!
package org.organicdesign.indented
import com.planbase.taint.Taintable
import com.planbase.taint.Taintable.Companion.charToStr
import com.planbase.taint.Taintable.Companion.stringify
import java.io.File
/**
* Adds a method to Kotlin's Pair to convert it to a Map.Entry.
*/
fun Pair.toEntry() = object: Map.Entry {
override val key: K = first
override val value: V = second
}
/**
* String-Object-Pair.
* This is an ideal Pair for Java because Java often has trouble inferring types
* when constructing data.
* Using a String as the fist type makes Java very happy.
* We use a generic second type in case that's useful, but since Java's just looking
* for an Object anyway, it can ignore that type altogether if it gets confused.
*/
fun sO(k: String, v: V) = object: Map.Entry {
override val key: String = k
override val value: V = v
}
/**
* Utility function that returns null when A equals X, otherwise returns A unchanged.
* This is really just syntactic sugar for brevity.
*/
fun nullWhen(a: A, x: A) =
when (a) {
x -> null
else -> a
}
/**
* Utility function that returns null when f(A) is true, otherwise returns A unchanged.
* This is really just syntactic sugar for brevity.
*/
fun nullWhen(a: A, f: (A) -> Boolean) =
when (f.invoke(a)) {
true -> null
else -> a
}
/**
* Utilities for producing pretty-print indented strings that could nearly compile to Kotlin or Java
* (some abbreviations for brevity).
*/
object StringUtils {
private val SPACES = arrayOf("",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ")
private val SPACES_LENGTH_MINUS_ONE = SPACES.size - 1
/**
* Efficiently returns a String with the given number of spaces.
* @param len the number of spaces
* @return a [String] with the specified number of spaces.
*/
@JvmStatic
fun spaces(len: Int): String =
when {
len < 0 -> throw IllegalArgumentException("Can't show negative spaces: $len")
len <= SPACES_LENGTH_MINUS_ONE -> SPACES[len]
else -> {
var remainingLen = len
val sB = StringBuilder()
while (remainingLen > SPACES_LENGTH_MINUS_ONE) {
sB.append(SPACES[SPACES_LENGTH_MINUS_ONE])
remainingLen -= SPACES_LENGTH_MINUS_ONE
}
sB.append(SPACES[remainingLen]).toString()
}
}
/**
* Pretty-prints any iterable with the given indent and class/field name
* @param entriesAsSymbols If true, treat Map.Entry keys of type String as symbols (don't quote them).
* Also, if true and a Map.Entry key is "", print only the value ("" is a sentinel value meaning
* to print positional parameters).
* @param singleLine If true, constrain all sub-collections to a single line.
*/
@JvmStatic
@JvmOverloads
fun iterableToStr(
indent: Int,
collName: String,
ls: Iterable,
entriesAsSymbols: Boolean = false,
singleLine: Boolean = false
): String {
val subIndent: Int = indent + collName.length + 1 // + 1 is for the paren.
val spaces: String = spaces(subIndent)
var needsComma = false
val sB = StringBuilder(collName).append("(")
ls.forEach {
if (needsComma) {
when {
singleLine -> sB.append(", ")
else -> sB.append(",\n").append(spaces)
}
needsComma = false
}
sB.append(indent(subIndent, it,
entriesAsSymbols = entriesAsSymbols,
singleLine = singleLine))
needsComma = true
}
return sB.append(")").toString()
}
/**
* Use this to pretty-print a class
*/
@JvmStatic
fun classFields(
indent: Int,
collName: String,
fields: Iterable>,
singleLine: Boolean,
): String = iterableToStr(indent, collName, fields,
entriesAsSymbols = true,
singleLine)
/**
* Kotlin wrapper because Pair does not implement Map.Entry and Pair is not accessible in Java.
*/
fun classFieldsK(
indent: Int,
collName: String,
fields: Iterable>,
singleLine: Boolean,
): String = iterableToStr(indent, collName, fields.map { it.toEntry() }, entriesAsSymbols = true, singleLine)
/**
* Use this to pretty-print a class with one field per line.
*/
@JvmStatic
fun oneFieldPerLine(
indent: Int,
collName: String,
fields: Iterable>
): String = classFields(indent, collName, fields, false)
/**
* Kotlin wrapper because Pair does not implement Map.Entry and Pair is not accessible in Java.
*/
fun oneFieldPerLineK(
indent: Int,
collName: String,
fields: Iterable>
): String = classFieldsK(indent, collName, fields, false)
/**
* Use this to pretty-print a class with all fields on one line.
*/
@JvmStatic
fun fieldsOnOneLine(
indent: Int,
collName: String,
fields: Iterable>
): String = classFields(indent, collName, fields, true)
/**
* Kotlin wrapper because Pair does not implement Map.Entry and Pair is not accessible in Java.
*/
fun fieldsOnOneLineK(
indent: Int,
collName: String,
fields: Iterable>
): String = classFieldsK(indent, collName, fields, true)
/**
* Takes a shot at pretty-printing anything you throw at it.
* If it's already an [IndentedStringable], it calls [IndentedStringable.indentedStr].
* Otherwise, takes its best shot at indenting whatever it finds.
* @param entriesAsSymbols If true, treat Map.Entry keys of type String as symbols (don't quote them).
* Also, if true and a Map.Entry key is "", print only the value ("" is a sentinel value meaning
* to print positional parameters).
* @param singleLine If true, constrain all sub-collections to a single line.
*/
@JvmStatic
@JvmOverloads
fun indent(
indent: Int,
item: Any?,
entriesAsSymbols: Boolean = false,
singleLine: Boolean = false
): String =
when (item) {
null -> "null"
is IndentedStringable -> item.indentedStr(indent, singleLine)
is String -> stringify(item)
is Map.Entry<*,*> -> {
val itemKey = item.key
when {
entriesAsSymbols -> {
when (itemKey) {
// Blank string suppresses key from printing at all in entriesAsSymbols mode
// for showing unnamed parameters (in order)
"" -> {
indent(indent, item.value, entriesAsSymbols=true, singleLine)
}
is String -> {
itemKey + "=" + indent(indent + itemKey.length + 1, item.value,
entriesAsSymbols=true, singleLine)
}
else -> {
throw IllegalStateException("When entriesAsSymbols is set, the entries must be Strings!")
}
}
}
else -> {
val key: String = indent(indent, item.key, entriesAsSymbols=false, singleLine)
key + "=" + indent(indent + key.length + 1, item.value, entriesAsSymbols=false,
singleLine)
}
}
}
is Pair<*,*> -> {
val first = indent(indent, item.first, entriesAsSymbols, singleLine)
first + " to " + indent(indent + first.length + 4, item.second, entriesAsSymbols, singleLine)
}
is Taintable -> stringify(item)
is Char -> charToStr(item)
is Float -> floatToStr(item)
is List<*> -> iterableToStr(indent, "listOf", item, singleLine = singleLine)
is Map<*,*> -> iterableToStr(indent, "mapOf", item.entries, singleLine = singleLine)
is Set<*> -> iterableToStr(indent, "setOf", item, singleLine = singleLine)
is Iterable<*> -> iterableToStr(indent, item::class.java.simpleName, item, singleLine = singleLine)
// Interesting, but too much info.
// is Array<*> -> iterableToStr(indent, "arrayOf<${item::class.java.componentType.simpleName}>",
// item.toList())
is Array<*> -> iterableToStr(indent, "arrayOf", item.toList(), singleLine = singleLine)
is File -> {
val details = StringBuilder()
if(item.exists()) {
if (item.isHidden) {
details.append(" hidden")
}
if (item.isDirectory) {
details.append(" dir")
} else if (item.isFile) {
details.append(" file")
}
details.append(" ")
details.append(if (item.canRead()) "r" else "_")
details.append(if (item.canWrite()) "w" else "_")
details.append(if (item.canExecute()) "x" else "_")
}
val ret = StringBuilder("File(")
ret.append(stringify(item.canonicalPath))
if (details.isNotEmpty()) {
ret.append(details)
}
ret.append(")").toString()
}
else -> item.toString()
}
/** Makes Float output distinguishable from Double. */
@JvmStatic
fun floatToStr(f: Float?): String {
if (f == null) {
return "null"
}
val str = f.toString()
return if (str.endsWith(".0")) {
str.substring(0, str.length - 2)
} else {
str
} + "f"
}
/**
* Single-quotes a string for Bash, escaping only single quotes. Returns '' for both the empty string and null.
* Will not write out any back-spaces.
*/
@JvmStatic
fun bashSingleQuote(s: String?): String {
if ( (s == null) || s.isEmpty() ) {
return "''"
}
var idx = 0
val sB = StringBuilder()
// True if the end of the output up to this point is inside a quote.
// We need this because single quotes must be escaped *outside* a quoted String.
// That's Right
// becomes:
// 'That'\''s Right'
// So, in the middle of the String, a single quote is escaped "stuff'\''more"
// At the end it's:
// 'boys'\'
// At the beginning:
// \''kay'
// And multiple in the middle:
// 'abc'\'\'\''def'
// So we have some state here to tell whether the end of the output so far is inside or outside a quote.
var outputQuoted = false
while (idx < s.length) {
val c = s[idx]
if (c == '\'') {
if (outputQuoted) {
sB.append("'\\'")
outputQuoted = false
} else {
sB.append("\\'")
}
} else if (c != '\u0008') { // Don't write out backspace.
if (outputQuoted) {
sB.append(c)
} else {
sB.append("'").append(c)
outputQuoted = true
}
}
idx++
}
if (outputQuoted) {
// Close the quote.
sB.append("'")
}
return sB.toString()
}
}