I'm trying to merge values from two maps in Kotlin, but I can't seem to find a solution online to do so. For example, I have these two Map Objects that have nested maps as shown below.
first: {settings={exit={exitMessage=message0}}, 2=2}
second: {settings={exit={differentMessage=message1, secondMessage=message2}}, 2=zebra}
As you can see, there are some keys that are the same(like settings and exit), but sometimes the keys are different. I'm wondering how I can merge the two Map objects so the end result is like
{settings={exit={exitMessage=message0, differentMessage=message1, secondMessage=message2}}, 2=zebra}
We can't simply "add" the two maps together, because Kotlin will override the Map objects
For example, if we add the two objects up above, as first + second, the end result will just be
{settings={exit={differentMessage=message1, secondMessage=message2}}, 2=zebra}
as the exit map in second takes priority over the exit map object in first.
You can try flattening both maps first:
settings.exit.exitMessage=message0
2=2
settings.exit.differentMessage=message1
settings.exit.secondMessage=message2
2=zebra
Then after merging the maps you end up with this:
settings.exit.exitMessage=message0
settings.exit.differentMessage=message1
settings.exit.secondMessage=message2
2=zebra
And then just rebuild the map again by exploding the keys.
Full code sample here: https://pl.kotl.in/p9BaUVOF3
Flattening a map
fun flatten(source: Map<String, Any>, target: HashMap<String, Any>, prefix: String = "") {
for (entry in source) {
var fullKey = if (prefix.length > 0) prefix + "." + entry.key else entry.key
if (entry.value is Map<*, *>) {
flatten(entry.value as Map<String, Any>, target, fullKey)
continue
}
target.put(fullKey, entry.value)
}
}
Exploding a flattened map
fun explode(source: Map<String, Any>, target: HashMap<String, Any>) {
for (entry in source) {
var targetMap = target
var keySegments = entry.key.split(".")
// Rebuild the map
for (keySegment in keySegments.dropLast(1)) {
if (targetMap.containsKey(keySegment)) {
var value = targetMap.get(keySegment)
if (value is Map<*, *>) {
targetMap = value as HashMap<String, Any>
continue
}
}
var newNestedMap = HashMap<String, Any>()
targetMap.put(keySegment, newNestedMap)
targetMap = newNestedMap
}
// Put values into it
targetMap.put(keySegments.last(), entry.value)
}
}
Related
I'm currently working on a project with different functions.
One of the functions is supposed to take a map and a string as inputs, then compare the items in the map and string and create a new map with matching items.
This function is not working because "containsKey" always returns false.
"in" always returns fault.
I tried substituting the map with a mutable List and got the same results.
I tried this same code on kotlin playground, link; https://pl.kotl.in/zZm1Smz3Z
and it worked well but wouldn't work on the platform I am using.
The function is below
fun createVariable (stringList: String, mapNumber: MutableMap<String, String>):MutableMap<String, String> {
var mapOfNumber = mutableMapOf<String, String>()
var conList : MutableList<String> = mutableListOf()
var regex = Regex("\\s*=\\s*")
var regez = Regex("\\s+")
var numberList = stringList.split(regex).toMutableList()
for (i in 0.. numberList.size -1) {
if ( regez.matches(numberList[i])) {
numberList.removeAt(i)
}
}
if (mapNumber.containsKey(numberList[1])) {
mapOfNumber.put(numberList[0], (mapNumber[numberList[1]])?: "0")
}
print(mapOfNumber)
return mapOfNumber
}
I have two functions (GetPodsOne and GetPodsTwo) that return me a big csv string. I then do some processing to discard the part of the string I don't want. See snippet below.
var podValues = execGetPodsOne()
val testPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
testPodValuesLst.forEach { it ->
println("value from testPodList=$it")
}
podValues = execGetPodsTwo()
val sitPodValuesLst: List<String> = podValues.split(",").map { it -> it.substringAfterLast("/") }
sitPodValuesLst.forEach { it ->
println("value from sitPodList=$it")
}
This leaves me with two lists. See output of the above below:
value from testPodList=api-car-v1:0.0.118
value from testPodList=api-dog-v1:0.0.11
value from testPodList=api-plane-v1:0.0.36
value from sitPodList=api-car-v1:0.0.119
value from sitPodList=api-dog-v1:0.0.12
value from sitPodList=api-plane-v1:0.0.37
What i would like to do is end up with the objects inside a data class like below:
data class ImageVersions(val apiName: String, val testPodVersion: String, val sitPodVersion: String)
api-car-v1, 0.0.118, 0.0.119
api-dog-v1, 0.0.11, 0.0.12
api-plane-v1, 0.0.36, 0.0.37
I've used test and sit above but I'm going to have maybe another 5 environments eventually. Looking for a nice way to get the versions for each api and easily combine into that ImageVersions data class.
thanks
Considering that you're going to have maybe another 5 environments eventually, I tried to write something that will scale well:
enum class Env { Test, Sit }
data class ImageVersions(val apiName: String, val versions: Map<Env, String?>)
fun String.getNameAndVersion() = substringBefore(':') to substringAfter(':')
fun getVersions(envMap: Map<Env, List<String>>): List<ImageVersions> {
val envApiNameMap = envMap.mapValues { it.value.associate(String::getNameAndVersion) }
val allApiNames = envApiNameMap.flatMap { it.value.keys }.distinct()
return allApiNames.map { apiName ->
ImageVersions(apiName, envApiNameMap.mapValues { it.value[apiName] })
}
}
Playground example
So instead of separate val testPodVersion: String, val sitPodVersion: String, here you have a map. Now the structure of ImageVersions always remains the same irrespective of how many environments you have.
getNameAndVersion is a helper function to extract apiName and version from the original string.
getVersions accepts a list of versions corresponding to each environment and returns a list of ImageVersions
envApiNameMap is same as envMap just that the list is now a map of apiName and its version.
allApiNames contains all the available apiNames from all environments.
Then for every apiName, we take all the versions of that apiName from all the environments.
In future, if your have another environment, just add it in the Env enum and pass an extra map entry in the envMap of getVersions. You need not modify this function every time you have a new environment.
How about this:
val testPodValuesMap = testPodValuesLst.associate { it.split(':').zipWithNext().single() }
val sitPodValuesMap = sitPodValuesLst.associate { it.split(':').zipWithNext().single() }
val mergedMap = (testPodValuesMap.keys + sitPodValuesMap.keys).associateWith { key ->
testPodValuesMap.getValue(key) to sitPodValuesMap.getValue(key)
}
val imageVersions = mergedMap.map { (k, v) -> ImageVersions(k, v.first, v.second) }
println(imageVersions.joinToString("\n"))
which prints
ImageVersions(apiName=api-car-v1, testPodVersion=0.0.118, sitPodVersion=0.0.119)
ImageVersions(apiName=api-dog-v1, testPodVersion=0.0.11, sitPodVersion=0.0.12)
ImageVersions(apiName=api-plane-v1, testPodVersion=0.0.36, sitPodVersion=0.0.37)
As a first step I would extract the apiNames from both lists:
val apiNames = list1.map { it.replace("value from ", "").split("[=:]".toRegex())[1] }
.plus(list2.map { it.replace("value from ", "").split("[=:]".toRegex())[1] })
.distinct()
Then I'd create the ImageVersions instances by looping over apiNames:
val result = apiNames
.map { apiName ->
ImageVersions(
apiName,
(list1.firstOrNull { it.contains(apiName) } ?: "").split(":")[1],
(list2.firstOrNull { it.contains(apiName) } ?: "").split(":")[1]
)
}
.toList()
The reason to first extract the apiNames is, that apiNames missing in one of the two lists will still end up in the final result.
Kotlin Playground
how can I set properties of a dataclass by its name. For example, I have a raw HTTP GET response
propA=valueA
propB=valueB
and a data class in Kotlin
data class Test(var propA: String = "", var propB: String = ""){}
in my code i have an function that splits the response to a key value array
val test: Test = Test()
rawResp?.split('\n')?.forEach { item: String ->
run {
val keyValue = item.split('=')
TODO
}
}
In JavaScript I can do the following
response.split('\n').forEach(item => {
let keyValue = item.split('=');
this.test[keyValue[0]] = keyValue[1];
});
Is there a similar way in Kotlin?
You cannot readily do this in Kotlin the same way you would in JavaScript (unless you are prepared to handle reflection yourself), but there is a possibility of using a Kotlin feature called Delegated Properties (particularly, a use case Storing Properties in a Map of that feature).
Here is an example specific to code in your original question:
class Test(private val map: Map<String, String>) {
val propA: String by map
val propB: String by map
override fun toString() = "${javaClass.simpleName}(propA=$propA,propB=$propB)"
}
fun main() {
val rawResp: String? = """
propA=valueA
propB=valueB
""".trimIndent()
val props = rawResp?.split('\n')?.map { item ->
val (key, value) = item.split('=')
key to value
}?.toMap() ?: emptyMap()
val test = Test(props)
println("Property 'propA' of test is: ${test.propA}")
println("Or using toString: $test")
}
This outputs:
Property 'propA' of test is: valueA
Or using toString: Test(propA=valueA,propB=valueB)
Unfortunately, you cannot use data classes with property delegation the way you would expect, so you have to 'pay the price' and define the overridden methods (toString, equals, hashCode) on your own if you need them.
By the question, it was not clear for me if each line represents a Test instance or not. So
If not.
fun parse(rawResp: String): Test = rawResp.split("\n").flatMap { it.split("=") }.let { Test(it[0], it[1]) }
If yes.
fun parse(rawResp: String): List<Test> = rawResp.split("\n").map { it.split("=") }.map { Test(it[0], it[1]) }
For null safe alternative you can use nullableString.orEmpty()...
I have a
val map = Map<String,String>
map.put("Nurseiyt","android")
I want to get a value by subString like:
map["Nurs"] should return "android"
is it possible?
Use kotlin.Collections, there are methods like filter.
Two things - it's better to use regular expression. So, you can even get better control what will be returned. And the second one, there can be more than one elements matched to that regex. So that's why I return list.
fun <T> substringKey(map: Map<String, T>, regex: Regex): List<T> {
return map.filter { it.key.contains(regex) }
.map { it.value }
}
If you want to use that notation you need to create your own map and override proper operator. What's worth to notice, you cannot return list of values then. So, in this case I just return first found value.
class SubstringMap<V> : HashMap<String, V>() {
override operator fun get(key: String): V? {
return this.entries.first { it.key.contains(key) }.value
}
}
fun main() {
val map = SubstringMap<String>()
map["Nurseiyt"] = "android"
println(map["Nurs"]) // "android"
}
And as the last thing - in kotlin you can create your own operator, like withKeyPart. This would be much better than overriding default operator (because I wouldn't expect that [] operator will work in different way than usual.
infix fun <V> Map<String, V>.withKeyPart(keyPart: String): List<V> {
return this.filter { it.key.contains(keyPart) }
.map { it.value }
}
and then call it like this:
fun main() {
val map = HashMap<String, String>()
map withKeyPart "KeyPart" // infix notation
map.withKeyPart("KeyPart") // standard call
}
Filtering the map, as per other answers, is simple and straightforward, but it doesn't scale well; it takes time proportional to the size of the map, so if the map could grow big, it could get very slow.
If you're always going to be searching for a leading substring, i.e. the start of a map key, then a better general solution is a data structure called a trie. This lets you search efficiently, with just one lookup per character.
Of course, writing one from scratch may not be justified for your project. But there are third-party implementations you could use, such as this one in Apache Commons. Or see the answers to this question.
write top level function like this
fun HashMap<String, String>.getContainskeyValue(search: String): String?
{
var returnList = ArrayList<String?>()
this.keys.filter { it.contains(search) }.map {
returnList.add(this[it])
}
return returnList.first()
//if you want all keys 'contains' values just return list
/* Ex
map.put("Nurseiyt", "android")
map.put("Nurseiyt1", "androidone")
map.put("Nurseirt2", "andrrroidtwo")
val isContainsdata = map.getContainskeyValue("N")
println(" result " + containsdata)
output :result [andrrroidtwo, android, androidone]
*/
}
then call like this
val map = HashMap<String, String>()
map.put("Nurseiyt", "android")
val containsdata = map.getContainskeyValue("Nurs")
println(" result " + containsdata)
output
android
I have two JsonNode objects which are arrays returned from API calls and look like this
exportedNodeArray:
[
{"key":"111", "value":"aaa"},
{"key":"222", "value":"bbb"},
{"key":"333", "value":"ccc"}
]
localNodeArray
[
{"key":"999", "value":"aaa"},
{"key":"888", "value":"bbb"},
{"key":"777", "value":"ccc"}
]
The required output is a Map of any keys which correspond to the same values in each array. The values are guaranteed to be unique within an array.
"111"="999"
"222"="888"
"333"="777"
This function returns the correct result, but seems like a very in-elegant way to do it.
fun mapIds(exportedNodeArray: JsonNode, localNodeArray: JsonNode) : MutableMap<String, String?> {
val localMap = mutableMapOf<String, String>()
localNodeArray.forEach {
localMap[it["value"].asText()] = it["key"].asText()
}
val idMap = mutableMapOf<String, String?>()
exportedNodeArray.forEach {
idMap[it["key"].asText()] = localMap[it["value"].asText()]
}
return idMap
}
I am new to Kotlin, and would like to understand a more functional approach. Especially if there is a way to access elements of a JsonNode by attribute value, and accomplish this in a single loop or map call.
If you define one more function, it could look like this:
fun mapIds(exportedNodeArray: JsonNode, localNodeArray: JsonNode) : Map<String, String> {
val localConverted = convert(localNodeArray)
return convert(exportedNodeArray)
.filterKeys(localConverted::containsKey)
.map { it.value to localConverted.getValue(it.key) }
.toMap()
}
fun convert(node: JsonNode): Map<String, String> = node.associate {
it["value"].asText() to it["key"].asText()
}