Using a Map with Nullable Keys? - kotlin

(new to Kotlin) I am writing a function to log differences between two Lists, like so:
fun logDifferences(booksOne: List<Book>?, booksTwo: List<Book>?, logEntryFactory: LogEntryFactory) {
val booksOneByKey: Map<String?, Book> = booksOne?.associateBy({ it.key }, { it }) ?: mapOf()
val booksTwoByKey: Map<String?, Book> = booksTwo?.associateBy({ it.key }, { it }) ?: mapOf()
val allBookKeysSet: Set<String?> = booksOneByKey.keys.union(booksTwoByKey.keys)
allBookKeysSet.forEach {
val bookOne = booksOneByKey[it]
val bookTwo = booksTwoByKey[it]
if (bookOne != bookTwo) {
bookOne?.let { // log the book) }
bookTwo?.let { // log the book)}
}
}
}
The idea is that if a book from booksOne or booksTwo is null, that is still a difference that I would like to capture. But as it is written, I am realizing that if a key can be nullable in my map, how could I even look up the result?
Is there a way of refactoring this to log instances where one list has a null object and not the other, or am I looking at this the wrong way?

Your code works perfectly fine even with null keys. consider this full kotlin program:
data class Book(
val key: String?,
val title: String
)
val list1 = listOf(
Book("1", "Book A"),
Book("2", "Book B"),
Book("3", "Book C"),
)
val list2 = listOf(
Book("2", "Book B"),
Book("4", "Book D"),
Book(null, "Book NullKey"),
)
fun main() {
val booksOneByKey: Map<String?, Book> = list1?.associateBy({ it.key }, { it }) ?: mapOf()
val booksTwoByKey: Map<String?, Book> = list2?.associateBy({ it.key }, { it }) ?: mapOf()
val allBookKeysSet: Set<String?> = booksOneByKey.keys.union(booksTwoByKey.keys)
allBookKeysSet.forEach {
val bookOne = booksOneByKey[it]
val bookTwo = booksTwoByKey[it]
println("key $it :")
if (bookOne != bookTwo) {
bookOne?.let { println("check $it") }
bookTwo?.let { println("check2 $it") }
}
}
}
this will print the following:
key 1 :
check Book(key=1, title=Book A)
key 2 :
key 3 :
check Book(key=3, title=Book C)
key 4 :
check2 Book(key=4, title=Book D)
key null :
check2 Book(key=null, title=Book NullKey)
as you can see it will also take the null book

This is not an answer to the question, as I do not really understand what the issue is. Maybe look and play around with the built-in collection functions:
data class Book(
val key: String,
val title: String
)
val list1 = listOf(
Book("1", "Book A"),
Book("2", "Book B"),
Book("3", "Book C")
)
val list2 = listOf(
Book("2", "Book B"),
Book("4", "Book D")
)
val all = (list1 + list2).distinct() // same as: (list1.plus(list2)).distinct()
println(all) // Books 1, 2, 3, 4
val inBoth = list1.intersect(list2)
println(inBoth) // Book 2
val inList1Only = list1.minus(list2)
println(inList1Only) // Books 1, 3
val inList2Only = list2.minus(list1)
println(inList2Only) // Book 4

If I've got this right, you have two lists of Books which have an id property, and you want to compare those lists to find:
books that share an ID with a different book
books that only appear in one list
books that appear in both lists, but with different IDs
And books with a null ID still need to be considered, i.e. if it only appears in one list (with a null ID), or if it appears in both lists, but the ID is null for one of them (i.e. different IDs).
I don't know if two different books with null IDs would be considered as "two different books sharing an ID", I'll guess no!
So your problem is if that ID is supposed to be unique, but it's also nullable, you can't use it as a key unless you're sure there will only be one with ID=null in each list. So you can't use the ID as a lookup. Really what you're doing there isn't storing Books - you're storing IDs, and the single book that maps to each. And since you're expecting multiple books to have the same ID (including null) that's not gonna work, right?
There's a couple of options I can think of. First the easy one - the same Book with a different ID is, ideally, not equal as far as equals() is concerned. Different data, right? If that's the case, you can just find all the Books that only appear in one of the lists (i.e. if they are in the other list, they don't match):
(yoinking Ivo Becker's setup code, thanks!)
data class Book(
val key: String?,
val title: String
)
val list1 = listOf(
Book("1", "Book A"),
Book("2", "Book B"),
Book("3", "Book C"),
)
val list2 = listOf(
Book("2", "Book B"),
Book("4", "Book D"),
Book(null, "Book NullKey"),
Book(null, "Bad Book")
)
fun main() {
// or (list1 subtract list2) union (list2 subtract list1) if you like
val suspects = (list1 union list2) subtract (list1 intersect list2)
println(suspects)
}
>> [Book(key=1, title=Book A), Book(key=3, title=Book C), Book(key=4, title=Book D),
Book(key=null, title=Book NullKey), Book(key=null, title=Bad Book)]
That's just using some set operations to find all the items not present in both lists (the intersect is the stuff that's in both, union is everything).
If you need to keep them separate so you can log per list, you can do something like this:
list1.filterNot { it in list2 }.forEach { println("List one: $it") }
list2.filterNot { it in list1 }.forEach { println("List two: $it") }
>>> List one: Book(key=1, title=Book A)
List one: Book(key=3, title=Book C)
List two: Book(key=4, title=Book D)
List two: Book(key=null, title=Book NullKey)
List two: Book(key=null, title=Bad Book)
If you can't do that for whatever reason (Books with different IDs return true for equals) then you could do something like this:
fun List<Book>.hasSameBookWithSameId(book: Book) =
firstOrNull { it == book }
?.let { it.key == book.key }
?: false
list1.filterNot(list2::hasSameBookWithSameId).forEach { println("List one: $it")}
list2.filterNot(list1::hasSameBookWithSameId).forEach { println("List two: $it")}

Related

Kotlin generic object sort: how would I sort by an object list parameters count or size

I wrote a nifty generic function for a Kotlin js project that will sort a List of objects by parameter.
For instance, a list of book objects look like
data class Book(val id: Int, val title: String, val year: Int, val authors:List<Author>)
will be sorted by my generic function:
fun <T> sortColumnData(data: List<T>, direction: SORTDIRECTION, selector: KProperty1<T, Comparable<*>>) :List<T> {
val sortedData = when (direction) {
SORTDIRECTION.UP -> data.sortedWith(compareBy(selector))
SORTDIRECTION.DOWN -> data.sortedWith(compareByDescending(selector))
}
return sortedData
}
And, I can pass the selector in very conveniently : Book::title
I need some direction in how to write a sortColumnData function that will sort by author count.
No need to reinvent the wheel, see the Kotlin standard library Ordering|Kotlin
data class Author(val name: String)
data class Book(val id: Int, val title: String, val year: Int, val authors: List<Author>)
val authorA = Author("A")
val authorB = Author("B")
val list = listOf(
Book(1, "Book 1", 2000, listOf(authorA)),
Book(2, "Book 2", 2000, listOf(authorA)),
Book(3, "Book 3", 2000, listOf(authorA, authorB)),
Book(4, "Book 4", 2000, listOf(authorB))
)
list.sortedBy { it.title }.forEach(::println)
list.sortedByDescending { it.title }.forEach(::println)
list.sortedBy { it.authors.size }.forEach(::println)
list.sortedByDescending { it.authors.size }.forEach(::println)
You can also use Method Referencing:
val result = list.sortedBy(Book::title)
which is equivalent to:
val result = list.sortedBy { it.title }
For list.sortedBy { it.authors.size } it is not possible to use Method Reference.
––––––––––––––––––––
Edit:
You can add comparator functions to your data class, one for each comparison you want to do. You then add a second sortColumnData function with a KFunction2 argument instead of a KProperty1 one.
import kotlin.reflect.KFunction2
data class Author(val name: String)
data class Book(val id: Int, val title: String, val year: Int, val authors: List<Author>): Comparable<Book> {
override fun compareTo(other: Book) = compareValuesBy(this, other, { it.id }, { it.id })
fun compareByAuthorCount(other: Book) = compareValuesBy(this, other, { it.authors.size }, { it.authors.size })
fun compareByTitleLength(other: Book) = compareValuesBy(this, other, { it.title.length }, { it.title.length })
}
val authorA = Author("A")
val authorB = Author("B")
val list = listOf(
Book(1, "Book 1 - 123", 2000, listOf(authorA)),
Book(2, "Book 2 - 1", 2000, listOf(authorA)),
Book(3, "Book 3 - 12", 2000, listOf(authorA, authorB)),
Book(4, "Book 4 - ", 2000, listOf(authorB))
)
enum class SortDirection { UP, DOWN }
fun <T : Comparable<T>> sortColumnData(data: List<T>, direction: SortDirection, comparator: KFunction2<T, T, Int>): List<T> {
return when (direction) {
SortDirection.UP -> data.sortedWith(comparator)
SortDirection.DOWN -> data.sortedWith(comparator).reversed()
}
}
sortColumnData(list, SortDirection.DOWN, Book::compareTo).forEach(::println)
// Ids: 4, 3, 2, 1
sortColumnData(list, SortDirection.UP, Book::compareByAuthorCount).forEach(::println)
// Ids: 1, 2, 4, 3 (UP = from least to most authors)
sortColumnData(list, SortDirection.UP, Book::compareByTitleLength).forEach(::println)
// Ids: 4, 2, 3, 1 (UP = from shortest to longest title)

Kotlin: mutable map of mutable list won't update the list

(Kotlin newbie here) I have a text file with rows that look like these:
1-1-1
1-1-2
1-1-3
2-1-1
2-1-2
etc.
I have to transform these data to a map where the key is the first 2 elements and the value is a list of the third elements that that match the key. For example, the above records will transform into this JSON:
1-1: [1, 2, 3]
2-1: [1, 2]
etc.
I'm unable to increment the list. Here's a simplified version, I get stuck on the "else":
fun main () {
val l1 = mutableListOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val m = mutableMapOf<String, List<Int>>()
for (e in l1) {
val c = e.split("-")
val key = "${c[0]}-${c[1]}"
if (m[key] == null) m[key] = listOf(c[2].toInt())
else println("How do I append to the list?")
}
println(m)
}
Output:
{1-1=[1], 2-1=[1]}
But I want:
{1-1=[1, 2, 3], 2-1=[1, 2]}
Thank you (comments about idiomatic form are welcome!)
If we continue to follow your strategy, what you need is for the value type to be a MutableList. Then you can add to the existing MutableList when there's already an existing list for that key:
fun main() {
val l1 = mutableListOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val m = mutableMapOf<String, MutableList<Int>>()
for (e in l1) {
val c = e.split("-")
val key = "${c[0]}-${c[1]}"
if (m[key] == null) m[key] = mutableListOf(c[2].toInt())
else m[key]!!.add(c[2].toInt())
}
println(m)
}
This can be more natural using getOrPut(). It returns the existing MutableList or creates one and puts it in the map if it's missing. Then we don't have to deal with if/else, and can simply add the new item to the list.
fun main() {
val l1 = mutableListOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val m = mutableMapOf<String, MutableList<Int>>()
for (e in l1) {
val c = e.split("-")
val key = "${c[0]}-${c[1]}"
m.getOrPut(key, ::mutableListOf).add(c[2].toInt())
}
println(m)
}
But we can use the map and groupBy functions to create it more simply:
val m = l1.map { it.split("-") }
.groupBy(
{ "${it[0]}-${it[1]}" }, // keys
{ it[2].toInt() } // values
)
You can achieve your desired output with a single call to groupBy of the Kotlin standard library.
val input = listOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val result = input.groupBy(
{ it.substringBeforeLast("-") }, // extract key from item
{ it.substringAfterLast("-").toInt() } // extract value from item
)
The first lambda function extracts the key to group by of every list item. The second lambda function provides the value to use for each list item.
You can also do it by first mapping your values to Pairs and then group them as follows:
fun main(args: Array<String>) {
val input = listOf("1-1-1", "1-1-2", "1-1-3", "2-1-1", "2-1-2")
val result = input.map {
val values = it.split("-")
"${values[0]}-${values[1]}" to values[2]
}.groupBy ({ it.first }) { it.second }
println(result)
}

Update object inside a list Kotlin

I am trying to modify my allVehicles variable, so it should contain the problem information that's in the vehiclesWithProblems list.
This is a simplified version of my code, but I need an updated allVehicles list that contains the replacement and remove the old one (the one without the problem).
How could I achieve that? This code is not working, the allVehicles list remains unchanged.
data class VehicleContainer(
val id: Int,
val vehicles: List<Vehicle>
)
data class Vehicle(
val id: Int,
val name: String,
val problem: Problem
)
data class Problem(
val quantity: Int,
val problemList: List<Int>,
val info: String? = ""
)
fun main() {
val vehiclesWithProblems = listOf<Vehicle>() //list of vehicles with problems - wont be empty
val allVehicles = mutableListOf<Vehicle>()//list of all vehicles (initially without any problems, but won't be empty either)
allVehicles.forEachIndexed { index, vehicle ->
val newVehicle = vehiclesWithProblems.find { vehicleWithProblem -> vehicle.id == vehicleWithProblem.id }
if (newVehicle != null) {
val replacement = vehicle.copy(problem = Problem(
quantity = newVehicle.problem.quantity,
problemList = newVehicle.problem.problemList,
info = newVehicle.problem.info)
)
allVehicles[index] = replacement
}
}
}
Seems to me the allVehicles list is actually modified, but beware! You make a copy of the vehicle where only the problem is changed, the rest remains unchanged. Runn the underneath code and you will see that after looping, the «Tesla without a problem» is still in the list, but now with a problem (so the list actually is changed.):
fun main() {
val vehiclesWithProblems = listOf(Vehicle(1, "Tesla", Problem(1, listOf(1), "Problem #1"))) //list of vehicles with problems - wont be empty
val allVehicles = mutableListOf(Vehicle(1, "Tesla without a problem", Problem(0, listOf(0), "No problem")))//list of all vehicles (initially without any problems, but won't be empty either)
println("vehiclesWithProblems: $vehiclesWithProblems")
println("allVehicles: $allVehicles")
allVehicles.forEachIndexed { index, vehicle ->
val newVehicle = vehiclesWithProblems.find { vehicleWithProblem -> vehicle.id == vehicleWithProblem.id }
if (newVehicle != null) {
val replacement = vehicle.copy(problem = Problem(
quantity = newVehicle.problem.quantity,
problemList = newVehicle.problem.problemList,
info = newVehicle.problem.info)
)
println("Changing #$index!")
allVehicles[index] = replacement
}
}
println("After the loop, allVehicles: $allVehicles")
}

Duplicates values in Map: Kotlin

I have a scenario whereby I need a map containing duplicate keys and values. I have created a list first and then I used associatedBy to convert them to a map, however the duplicates issue is not taken into account. Here is my implementation:
class State(private val startStatus: Status, private val expectedStatus: Status) {
companion object StatusList {
val listStatuses = listOf(
State(Status.A, Status.B),
State(Status.B, Status.A),
State(Status.B, Status.C),
State(Status.C, Status.B),
State(Status.C, Status.E),
State(Status.C, Status.D),
State(Status.D, Status.B),
State(Status.E, Status.C),
State(Status.E, Status.B)
)
open fun mapStatuses(): Map<Status, Collection<Status>> {
return listStatuses.associateBy(
keySelector = { key -> key.expectedStatus },
valueTransform = State::startStatus)
}
}
}
I am struggling to find a Multimap in Kotlin that would allow me to deal with duplicates. Can you help?
Thanks
In short, there is no multimap in Kotlin.
A multimap would allow multiple, equivalent keys with different values - this can be implemented with unique keys and a collection of values associated with given key, instead of having a view over collection of key-value pairs with equivalent keys.
Thus, you can use groupBy():
data class Record(val id: Int, val name: String)
fun main() {
val records = listOf(
Record(1, "hello"),
Record(1, "there"),
Record(2, "general"),
Record(2, "kenobi")
)
val mapped = records.groupBy({ it.id }, { it.name })
for (entry in mapped) {
println("${entry.key} -> ${entry.value.joinToString()}")
}
}
Here I am using groupBy with a projection of key (which is Record's id) and a projection of value (which is Record's name). Quite similar to your States and Statuses.

Removing elements from a list which are not in another list - Kotlin

I have two mutableLists, listOfA has so many objects including duplicates while listOfB has fewer. So I want to use listOfB to filter similar objects in listOfA so all list will have equal number of objects with equivalent keys at the end. Code below could explain more.
fun main() {
test()
}
data class ObjA(val key: String, val value: String)
data class ObjB(val key: String, val value: String, val ref: Int)
fun test() {
val listOfA = mutableListOf(
ObjA("one", ""),
ObjA("one", "o"),
ObjA("one", "on"),
ObjA("one", "one"),
ObjA("two", ""),
ObjA("two", "2"),
ObjA("two", "two"),
ObjA("three", "3"),
ObjA("four", "4"),
ObjA("five", "five")
)
//Use this list's object keys to get object with similar keys in above array.
val listOfB = mutableListOf(
ObjB("one", "i", 2),
ObjB("two", "ii", 5)
)
val distinctListOfA = listOfA.distinctBy { it.key } //Remove duplicates in listOfA
/*
val desiredList = doSomething to compare keys in distinctListOfA and listOfB
for (o in desiredList) {
println("key: ${o.key}, value: ${o.value}")
}
*/
/* I was hoping to get this kind of output with duplicates removed and comparison made.
key: one, value: one
key: two, value: two
*/
}
If you want to operate directly on that distinctListOfA you may want to use removeAll to remove all the matching entries from it. Just be sure that you initialize the keys of B only once so that it doesn't get evaluated every time the predicate is applied:
val keysOfB = listOfB.map { it.key } // or listOfB.map { it.key }.also { keysOfB ->
distinctListOfA.removeAll {
it.key !in keysOfB
}
//} // if "also" was used you need it
If you have a MutableMap<String, ObjA> in place after evaluating your unique values (and I think it may make more sense to operate on a Map here), the following might be what you are after:
val map : MutableMap<String, ObjA> = ...
map.keys.retainAll(listOfB.map { it.key })
retainAll just keeps those values that are matching the given collection entries and after applying it the map now contains only the keys one and two.
In case you want to keep your previous lists/maps and rather want a new list/map instead, you may just call something like the following before operating on it:
val newList = distinctListOfA.toList() // creates a new list with the same entries
val newMap = yourPreviousMap.toMutableMap() // create a new map with the same entries
I tried this
primaryList.removeAll { primaryItem ->
secondaryList.any { it.equals(primary.id, true) }
}
PrimaryList here is a list of objects
SecondaryList here is a list of strings