3

I'm looking for a Kotlin way to do a dynamic values substitution into a string. It is clear how to implement it, just want to check if there is something similar in standard library.

Could you help me to find a function which given template and data map returns a resulting string with all template keys replaced with their values?

fun format(template: String, data: Map<String, Any>): String { /* magic */ }

format("${a} ${b} ${a}", mapOf("a" to "Home", "b" to "Sweet))   // -> "Home Sweet Home"
diziaq
  • 6,881
  • 16
  • 54
  • 96
  • 1
    It's not possible with your current signature, but I think it might work if you change the signature a little bit... – Sweeper Jan 19 '22 at 16:46
  • 1
    How about something like `fun format(data: Map, template: (Map) -> String): String`? And you would write the template like: `{ "${it["a"]} ${it["b"]} ${it["a"]}" }`. – Sweeper Jan 19 '22 at 16:49
  • Sweeper has a point there. Define it as `fun format(data: Map, template: (Map) -> String) = data.let(template)` and then call it like this: `format(mapOf("a" to "Home", "b" to "Sweet")) {"${it["a"]} ${it["b"]} ${it["a"]}"}`. – k314159 Jan 20 '22 at 16:29

4 Answers4

1
fun format(template: String, data: Map<String, String>): String {
  var retval = template
  data.forEach { dataEntry ->
    retval = retval.replace("\${" + dataEntry.key + "}", dataEntry.value)
  }
  return retval
}

// The $ signs in the template string need to be escaped to prevent
// string interpolation
format("\${a} \${b} \${a}", mapOf("a" to "Home", "b" to "Sweet"))
lukas.j
  • 6,453
  • 2
  • 5
  • 24
1

Not shorter than lukas.j's answer, just different (using Regex):

val regex = "\\\$\\{([a-z])}".toRegex()

fun format(template: String, data: Map<String, String>) =
    regex.findAll(template).fold(template) { result, matchResult ->
        val (match, key) = matchResult.groupValues
        result.replace(match, data[key] ?: match)
    }
Emanuel Moecklin
  • 28,488
  • 11
  • 69
  • 85
1

I did not find any thing standard to solve the problem.

So here is a balanced (readability/performance/extensibility) solution also handling cases when some substitutions are undefined in dataMap.

makeString("\${a} # \${b} @ \${c}", mapOf("a" to 123, "c" to "xyz"))   // => "123 # ??? @ xyz"

--

object Substitutions {
    private val pattern = Pattern.compile("\\$\\{([^}]+)\\}")

    fun makeString(
        template: String, 
        dataMap: Map<String, Any?>, 
        undefinedStub: String = "???"
    ): String {
        val replacer = createReplacer(dataMap, undefinedStub)
        val messageParts = splitWithDelimiters(template, pattern, replacer)
        return messageParts.joinToString("")
    }

    private fun createReplacer(dataMap: Map<String, Any?>, stub: String): (Matcher) -> String {
        return { m ->
            val key = m.group(1)
            (dataMap[key] ?: stub).toString()
        }
    }

    private fun splitWithDelimiters(
        text: String,
        pattern: Pattern,
        matchTransform: (Matcher) -> String
    ): List<String> {
        var lastMatch = 0
        val items = mutableListOf<String>()
        val m = pattern.matcher(text)

        while (m.find()) {
            items.add(text.substring(lastMatch, m.start()))
            items.add(matchTransform(m))
            lastMatch = m.end()
        }

        items.add(text.substring(lastMatch))

        return items
    }
}
diziaq
  • 6,881
  • 16
  • 54
  • 96
0

Similar to Emanuel Moecklin's answer, but with support of $v notation without braces.

private val regex = "\\\$\\{([^}]+)\\}|\\\$(\\w+)".toRegex()

/** Treat the string as template (according to Kotlin string template format), substitutes keys in the
 * template with values from [valuesMap]
 */
fun String.substitute(valuesMap : Map<String, Any?>) = substitute(valuesMap::get)

fun String.substitute(keyedValueFn : (String) -> Any?) =
    regex.replace(this) { matchResult ->
        val (match, key1, key2) = matchResult.groupValues
        keyedValueFn(key1.takeNullIfEmpty() ?: key2)?.toString() ?: match
    }

//////

@Test
fun `test string template`() {
    "lala \$k1 bebe \${k 2}".substitute(mapOf("k1" to "v1", "k 2" to "v2")) shouldBe
            "lala v1 bebe v2"

    "lala \$nokey".substitute(mapOf()) shouldBe
            "lala \$nokey"

    "lala \${unfinished".substitute(mapOf()) shouldBe
            "lala \${unfinished"
}
plinyar
  • 185
  • 1
  • 9