Giuseppe
Giuseppe

Reputation: 63

Using a Map with Nullable Keys?

(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?

Upvotes: 0

Views: 1062

Answers (3)

cactustictacs
cactustictacs

Reputation: 19524

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

Upvotes: 0

Ivo
Ivo

Reputation: 23164

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

Upvotes: 2

lukas.j
lukas.j

Reputation: 7163

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

Upvotes: 0

Related Questions