Uploaded image for project: 'Groovy'
  1. Groovy
  2. GROOVY-4253

findResult object/collection/map enhancement patch

Attach filesAttach ScreenshotVotersWatch issueWatchersCreate sub-taskLinkCloneUpdate Comment AuthorReplace String in CommentUpdate Comment VisibilityDelete Comments
    XMLWordPrintableJSON

Details

    • Improvement
    • Status: Closed
    • Major
    • Resolution: Fixed
    • None
    • 1.7.5, 1.8-beta-2
    • 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

        Issue Links

        Activity

          This comment will be Viewable by All Users Viewable by All Users
          Cancel

          People

            guillaume Guillaume Sauthier
            tnaleid Ted Naleid
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved:

              Slack

                Issue deployment