0

I have a scenario where I have a function, scanForTargets, that returns an Observable of type FoundNumber. In FoundNumber I just need an ID field I can grab out of it. As each element comes back in the scanResults Observable, I want to check to see if the name field matches one of the names on a target list. If so, then I want to emit that. For example, if I am looking for numbers 1, and 2, and scanForTargets() emits back 1, 2, 3, and 4, then I want scanForValues to emit back only 1 and 2.

The caveat is that I only want to continue doing this until either: 1) A time period elapses (in which case I throw and error) 2) All items on the String list are found before the timeout.

What I have so far looks like this, but I cannot get it to work for me mostly due to the shortcut of stopping once/if all of the targets are found before the timeout.

fun scanForValues(targetList: List<String>): Observable<FoundNumber> {
    val scanResult = scanForTargets()

    return scanResult.doOnNext {scanResult -> Log.d(TAG, "Found potential target: " + scanResult.name) }
            .filter(TargetPredicate(targetList)) //See if it's one of those we want
            .timeout(5, TimeUnit.SECONDS) //Wait a max of 5 seconds to find all items
            .doOnError { Log.w(TAG, "Failed to scan"}") }
            .map{s->scanResult.name}  
}

class TargetPredicate(private val targetList: List<String>) : Predicate<ScanResult> { override fun test(scanResult: ScanResult): Boolean {
        if(scanResult == null) {
            return false
        }
        return scanResult.name in targetList 
    }
}

How can I also add the check to stop if I find all of the items in the list? I can't just add another predicate right?

Thanks.

Update: As requested, here is some data to show what I mean.

Let's say that the scanForTargets() and supporting code looks like this:

var emittedList: List<String?> = listOf(null, "0", "1", "2", "3")


fun scanForTargets(): Observable<FoundNumber> = Observable
    .intervalRange(0, emittedList.size.toLong(), 0, 1, TimeUnit.SECONDS)
    .map { index -> FoundNumber(emittedList[index.toInt()]) }

data class FoundNumber(val targetId: String?)

Now if scanForValues was called with a list of 1 and 2, then it should emit back an Observable of 1 and then 2.

user443654
  • 821
  • 1
  • 7
  • 21

1 Answers1

1

No, it is not as simple as adding another filter.

A possible solution is to use scan to remove items from a set containing your targets, and complete when the set becomes empty.

Example:

val targets = listOf("a", "b", "c")

fun scanForTarget(): Observable<String> = Observable.just("a", "b")

fun scanForValues(targets: List<String>): Completable {
    val initial = targets.toMutableSet()
    return scanForTarget()
            .timeout(5, TimeUnit.SECONDS)
            .scan(initial) { acc, next -> acc.remove(next); acc }
            .filter { it.isEmpty() }
            .singleOrError()
            .toCompletable()
}

Note: a Completable is a special type of publisher that can only signal onComplete or onError.


Update: response to question update.

The new example in your question won't work, because null values are not allowed in RxJava2.

Assuming you fix that, the following solution may help you.

fun scanForValues(targets: List<String>): Observable<String> {
    val accumulator: Pair<Set<String>, String?> = targets.toSet() to null
    return scanForTarget()
            .timeout(5, TimeUnit.SECONDS)
            .scan(accumulator) { acc, next -> 
                val (set, previous) = acc
                val item = if (next in set) next else null
                (set - next) to item     // return set and nullable item
            }
            .filter { it.second != null } // item not null
            .take(initial.size)           // limit to the number of items
            .map { it.second }            // unwrap the item from the pair
            .map { FoundNumber(it) }      // wrap in your class
}

Instead of using only the Set<String> as the accumulator, now we also add the item.

The item is nullable, this allows us to check if a given item was present or not.

Notice that no null values are passed through the observable flow. In this case null values are wrapped inside Pair<Set<String>, String?> which are never null themselves.

ESala
  • 6,878
  • 4
  • 34
  • 55
  • Thanks. This is helpful! The requirement of emitting back each item that passes though isn't met as the Completable, as you mention, only signals onComplete or onError. Is it just a matter of changing the return to an Observerable and removing the .toCompletable() or does that cause other issues? Again, it needs to emit back each element (a, b, and c in this case) that passes the test as it passes the test. – user443654 May 29 '18 at 02:22
  • ah ok, you also need each item to be emitted. Removing the `toCompletable` will not solve it. Maybe you could add an example of correct vs incorrect sequence in your question. – ESala May 29 '18 at 11:17
  • Data added as requested. Note that I tried to remove the .toCompletable() and changed the return type to Observable but there is more to it. It is working with a mutable set and needs to be an Observable – user443654 Jun 05 '18 at 20:52
  • @user443654 I have updated the answer. Now the observable will return the items besides checking if they are present in the set. – ESala Jun 06 '18 at 09:03
  • Thanks for the update. I had to modify it a bit to get it to build (think it's a Kotlin version issue...had to replace the Pair with Pair and then due to that, convert from Set to MutableSet to do the check for null). In any case though, it works the majority of the way. The only thing is it won't timeout if you don't find one of the numbers in the time allotted. Think I will mark this as accepted, then open up one that just deals with how to wire up a timer that fails if ALL items in a List aren't found, not just one item being emitted like timeout() does. – user443654 Jun 06 '18 at 17:17