All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.organicdesign.indented.StringUtils.kt Maven / Gradle / Ivy

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()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy