Details
-
Improvement
-
Status: Closed
-
Major
-
Resolution: Fixed
-
None
-
None
-
None
-
Patch
Description
Groovy find and findAll take the result of the passed in closure and use groovy truth to either find the first element in the collection/map or filter the collection/map.
There are many situations situations where what you actually want to find or collect is the result of the closure, rather than test original elements and then reprocessing them all again.
The attached patch (with javadocs & unit tests) adds 2 methods to groovy objects, collections, and maps; enhanced versions of the existing groovy find and findAll methods:
<object/collection/map>.findResult(closure) <object/collection/map>.findAllResults(closure)
findResult and findAllresults are valuable for shortening/clarifying code in many situations, but they're the most valuable when the closure calls a relatively expensive method (or one that isn't idempotent). When calling the method again with the results of a find/findAll is impractical.
I had a discussion today on twitter with Robert Fletcher (http://twitter.com/rfletcherEW/status/16410798707) that spurred this patch, though there have been many times in the past that I've wanted this functionality and ended up writing the 4+ lines of boilerplate code to work around it being missing.
Details:
The object/collection version of findAll iterates through the object and passes each value to the closure. The first closure result that is non-null (including false) will short circuit evaluation and return the value, example:
// works on lists/collections assert "RAB" == ["foo", "bar", "baz"].findResult { def uCaseReversed = it.reverse().toUpperCase() // proxy for expensive method that we don't want to do more often than necessary if (uCaseReversed ==~ /..B/) return uCaseReversed } // and objects with iterators like ranges assert "Found 4" == (1..10).findResult { if (it > 3) return "Found $it" }
The map version of findResult is slightly different as what it returns is a Map.Entry with the key equal to the key of the map entry that generated a non-null value from the closure:
// closure has 0/1 parameters, it passes the current Map.Entry in as it iterates [a: 1, b: 2, c: 3, d: 4].findResult { if (it.value > 2) return "I found ${it.key}:${it.value}" }.with { assert "c" == it.key assert "I found c:3" == it.value } // closure has 2 parameters, it passes the key and value in as it iterates [a: 1, b: 2, c: 3, d: 4].findResult { key, value -> if (value > 2) return "I found ${key}:${value}" }.with { assert "c" == it.key assert "I found c:3" == it.value }
findAllResults will spin through the whole iterator, but will collect only those results from the closure that are non-null, so it can both collect and filter in one step:
assert ["Found 4", "Found 5"] == (1..5).findAllResults { if (it > 3) return "Found $it" }
The map version returns a map that contains the keys that had a non-null result with the result of the closure:
def origMap = [a:1, b:3, c:5] origMap.findAllResults { if (it.value >= 3) return "Found ${it.key}:${it.value}" }.with { mappedResults -> assert 2 == mappedResults.size() assert "Found b:3" == mappedResults.b assert "Found c:5" == mappedResults.c }
Other groovy iterators (like collect and inject) are not able to easily fulfill this role. Collect will not do any filtering, but instead returns a full list with nulls that need to be removed. Inject can sort of be made to fill this role, but it's often more work than it's worth as you need to manage the list creation/appending and the additional closure parameter. There isn't an equivalent of collect/inject on Maps.
I'd love it if this could be added to 1.7.4, but 1.8 would work too .
Attachments
Attachments
Issue Links
- is related to
-
GROOVY-3526 It would be nice to have a GDK method for replacing particular items in a list, i.e. combining findAll and collect
- Closed
- relates to
-
GROOVY-4899 findResults object/collection/map enhancement patch (CLONED from findResult)
- Closed