Simplifying the predicate when checking for severa

2019-08-11 04:14发布

问题:

Kotlin often uses very pragmatic approaches. I wonder whether there is some I don't know of to simplify a filter predicate which just asks for some known values.

E.g. consider the following list:

val list = listOf("one", "two", "2", "three")

To filter out "two" and "2" filtering can be accomplished in several ways, e.g.:

list.filter {
  it in listOf("two", "2") // but that creates a new list every time... (didn't check though)
}

// extracting the list first, uses more code... and may hide the list somewhere sooner or later
val toCheck = listOf("two", "2")
list.filter { it in toCheck } 

// similar, but probably less readable due to naming ;-)
list.filter(toCheck::contains)

// alternative using when, but that's not easier for this specific case and definitely longer:
list.filter {
    when (it) {
        "two", "2" -> true
        else -> false
    } 
}

// probably one of the simplest... but not so nice, if we need to check more then 2 values
list.filter { it == "two" || it == "2" }

I wonder... is there something like list.filter { it in ("two", "2") } or any other simple way to create/use a short predicate for known values/constants? In the end that's all I wanted to check.

EDIT: I just realised that the sample doesn't make much sense as listOf("anything", "some", "other").filter { it in listOf("anything") } will always be just: listOf("anything"). However, the list intersection makes sense in constellations where dealing with, e.g. a Map. In places where the filter actually doesn't return only the filtered value (e.g. .filterKeys). The subtraction (i.e. list.filterNot { it in listOf("two", "2") }) however also makes sense in lists as well.

回答1:

Kotlin provides some set operations on collections which are

  • intersect (what both collections have in common)
  • union (combine both collections)
  • subtract (collections without elements of the other)

In your case, instead of filter, you may use the set operation subtract

val filteredList  = list.subtract(setOf("two","2"))

and there you go.

EDIT:

and the fun (pun intended) doesn't end there: you could extend the collections with your own functions such as a missing outerJoin or for filtering something like without or operators i.e. / for intersect

For example, by adding these

infix fun <T> Iterable<T>.without(other Iterable<T>) = this.subtract(other)
infix fun <T> Iterable<T>.excluding(other Iterable<T>) = this.subtract(other)

operator fun <T> Iterable<T>.div(other: Iterable<T>) = this.intersect(other)

Your code - when applied to your example using the intersect - would become

val filtered = list / filter //instead of intersect filter 

or - instead of substract:

val filtered = list without setOf("two", "2")

or

val filtered = list excluding setOf("two", "2")

Pragmatic enough?



回答2:

I ended up with the following now:

fun <E> containedIn(vararg elements: E) = { e:E -> e in elements }
fun <E> notContainedIn(vararg elements: E) = { e:E -> e !in elements }

which can be used for maps & lists using filter, e.g.:

list.filter(containedIn("two", "2"))
list.filter(notContainedIn("two", "2"))
map.filterKeys(containedIn("two", "2"))
map.filterValues(notContainedIn("whatever"))

In fact it can be used for anything (if you like):

if (containedIn(1, 2, 3)(string.toInt())) {

My first approach inspired by Gerald Mückes answer, but with minus instead of subtract (so it only covers the subtraction-part):

(list - setOf("two", "2"))
       .forEach ...

Or with own extension functions and using vararg:

fun <T> Iterable<T>.without(vararg other: T) = this - other

with the following usage:

list.without("two", "2")
    .forEach... // or whatever...

With the above variant however no infix is possible then. For only one exclusion an infix can be supplied as well... otherwise the Iterable-overload must be implemented:

infix fun <T> Iterable<T>.without(other : T) = this - other
infix fun <T> Iterable<T>.without(other : Iterable<T>) = this - other

Usages:

list without "two"
list without listOf("two", "2")


回答3:

I don't think there is anything simpler than to create the filtering list/set and then apply it:

val toCheck = listOf("two", "2") 
val filtered = list.filter { it in toCheck }

or

val toCheck = setOf("two", "2")
val filtered = list.filter { it in toCheck } 

but if you prefer you can create a Predicate:

val predicate: (String) -> Boolean = { it in listOf("2", "two") }
val filtered = list.filter { predicate(it) }

Edit: as for the approach with minus, which is not the case here but has been mentioned, it does not provide simplicity or efficiency since itself is using filter:

/**
 * Returns a list containing all elements of the original collection except the elements contained in the given [elements] collection.
 */
public operator fun <T> Iterable<T>.minus(elements: Iterable<T>): List<T> {
    val other = elements.convertToSetForSetOperationWith(this)
    if (other.isEmpty())
        return this.toList()
    return this.filterNot { it in other }
}

(from Collections.kt)



标签: kotlin