Search code examples
kotlinstring-interpolation

keeping track of injected variables when using string interpolation in kotlin


I'm looking for a way to keep track of variables used when doing a string interpolation without parsing the string. For example, if I have a string:

val expStr = "${var1} some other useless text ${var2}"

I want to be able to identify the order of the variables used, again without parsing the string. In this case [var1, var2] would be an expected output. So far I've thought of defining a class where I pass it all of the variables. Then reference said variables through the class function grab.

val wrapper = ContainerClass(var1, var2)
val expStr = "${wrapper.grab(var1)} some other useless text ${wrapper.grab(var2)}"

inside ContainerClass is a array, each time a variable is referenced it is added to the array and outputted through getReferenceCalls

val whatIWant = wrapper.getReferenceCalls() // [var1, var2]

This works great until I introduce the injection of strings into strings.

val wrapper = ContainerClass(var1, var2, var3)
val snippet = "${wrapper.grab(var1)} some other useless text ${wrapper.grab(var2)}"
val expStr = "${wrapper.grab(var3)} ${snippet}"

val notWhatIWant = wrapper.getReferenceCalls() // [var1, var2, var3]

Here, I want to identify the order of the injected variables in the final expStr ie. [var3, var1, var2]. My question is, is this possible without parsing expStr? I did also think of a not so elegant solution of allowing my class to define any given "snippet" and the class identifies the variables referenced in the snippet. This works but becomes convoluted fast. What I really need is an eligant solution...if it exists.


Solution

  • I have implemented a "ContainerClass" to achieve your goal. I uses String.format instead of string templates so that I don't need prior information of the input.

    class StringNode(private val format: String, vararg args : Any) {
        private val argv = args
    
        override fun toString() : String = String.format(format,*argv)
    
        fun getFlatArgs() : List<Any> = argv.flatMap {
            if(it is StringNode){
                it.getFlatArgs()
            } else{
                listOf(it)
            }
        }
    }
    

    Usage:

    fun main(){
            val sn1 = StringNode("1:%s 2:%s 3:%s","abc",123,"def")
            println(sn1)
            println(sn1.getFlatArgs())
    
            val sn2 = StringNode("foo:%s bar:%s","foo",sn1);
            println(sn2)
            println(sn2.getFlatArgs())
    
            val sn3 = StringNode("sn1:%s, sn2:%s",sn1,sn2);
            println(sn3)
            println(sn3.getFlatArgs())
    }
    

    Output:

    1:abc 2:123 3:def
    [abc, 123, def]
    foo:foo bar:1:abc 2:123 3:def
    [foo, abc, 123, def]
    sn1:1:abc 2:123 3:def, sn2:foo:foo bar:1:abc 2:123 3:def
    [abc, 123, def, foo, abc, 123, def]