├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── io │ └── github │ └── jhspetersson │ └── packrat │ ├── AtLeastGatherer.java │ ├── AtMostGatherer.java │ ├── BreakingGatherer.java │ ├── CollectingGatherer.java │ ├── DistinctByGatherer.java │ ├── DropLastNGatherer.java │ ├── DropNthGatherer.java │ ├── EqualChunksGatherer.java │ ├── FilterEntriesGatherer.java │ ├── FilteringGatherer.java │ ├── FilteringWithIndexGatherer.java │ ├── FixedSizeDeque.java │ ├── FlatMapGatherer.java │ ├── IdentityGatherer.java │ ├── IncreasingDecreasingChunksGatherer.java │ ├── IncreasingDecreasingGatherer.java │ ├── IntoListGatherer.java │ ├── LastingGatherer.java │ ├── MapWhileUntilGatherer.java │ ├── MappingGatherer.java │ ├── MinMaxGatherer.java │ ├── NCopiesGatherer.java │ ├── NthGatherer.java │ ├── Packrat.java │ ├── PeekWithIndexGatherer.java │ ├── RemoveDuplicatesGatherer.java │ ├── RepeatGatherer.java │ ├── RotateLeftGatherer.java │ ├── SamplingGatherer.java │ ├── WindowFixedWithIndexGatherer.java │ ├── WindowSlidingWithIndexGatherer.java │ ├── ZipGatherer.java │ ├── ZipWithIndexGatherer.java │ └── package-info.java └── test └── java └── io └── github └── jhspetersson └── packrat ├── AtLeastTest.java ├── AtMostTest.java ├── BreakingTest.java ├── CollectingTest.java ├── DistinctByTest.java ├── DropLastNTest.java ├── DropNthGathererTest.java ├── Employee.java ├── EqualChunksGathererTest.java ├── FilterEntriesTest.java ├── FilterWithIndexTest.java ├── FilteringTest.java ├── FlatMapTest.java ├── IdentityGathererTest.java ├── IncreasingDecreasingChunksTest.java ├── IncreasingDecreasingTest.java ├── IntoListTest.java ├── LastNTest.java ├── MapWhileUntilTest.java ├── MapWithIndexTest.java ├── MappingTest.java ├── MinMaxTest.java ├── NCopiesTest.java ├── NthTest.java ├── PeekWithIndexTest.java ├── RemoveDuplicatesTest.java ├── RepeatGathererTest.java ├── SamplingTest.java ├── TestUtils.java ├── WindowFixedWithIndexTest.java ├── WindowSlidingWithIndexTest.java ├── ZipTest.java └── ZipWithIndexTest.java /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 24 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '24' 20 | distribution: 'corretto' 21 | 22 | - name: Setup Gradle 23 | uses: gradle/actions/setup-gradle@v4 24 | with: 25 | gradle-version: '8.14' 26 | 27 | - name: Make gradlew executable 28 | run: chmod +x ./gradlew 29 | 30 | - name: Build with Gradle Wrapper 31 | run: ./gradlew build 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | *.iml 4 | build/ 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Packrat 2 | 3 | Packrat is a Java library that provides various [Gatherer](https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/util/stream/Gatherer.html) implementations for the Stream API. Gatherers can enhance streams with custom intermediate operations. 4 | 5 | [Introduction to the Gatherers by Viktor Klang](https://www.youtube.com/watch?v=8fMFa6OqlY8) 6 | 7 | ### Availability 8 | 9 | > [!IMPORTANT] 10 | > You will need a very fresh JDK version with preview features enabled to actually use Gatherers. 11 | 12 | |JEP|JDK|Status| 13 | |---|---|---| 14 | |[461](https://openjdk.org/jeps/461)|22|Preview| 15 | |[473](https://openjdk.org/jeps/473)|23|Second Preview| 16 | |[485](https://openjdk.org/jeps/485)|24|Final| 17 | 18 | Build scripts expect to run on JDK version not lower than 24. 19 | 20 | ### Usage 21 | 22 | #### Maven 23 | 24 | ```xml 25 | 26 | io.github.jhspetersson 27 | packrat 28 | 0.1.0 29 | 30 | ``` 31 | 32 | #### Gradle 33 | 34 | ```groovy 35 | implementation("io.github.jhspetersson:packrat:0.1.0") 36 | ``` 37 | 38 | ### Gatherers 39 | 40 | #### Filtering and mapping operations 41 | 42 | | Name | Description | 43 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 44 | | [distinctBy](#distinctby) | Distinct values with custom mapper | 45 | | [filterBy](#filterby) | Filter with custom mapper and (optionally) predicate | 46 | | [filterEntries](#filterentries) | Filter Map.Entry elements using a BiPredicate on key and value | 47 | | [removeBy](#removeby) | Remove with custom mapper and (optionally) predicate | 48 | | [removeEntries](#removeentries) | Remove Map.Entry elements using a BiPredicate on key and value | 49 | | [removeDuplicates](#removeduplicates) | Removes consecutive duplicates from a stream | 50 | | [flatMapIf](#flatmapif) | Optional `flatMap` depending on predicate | 51 | | [minBy](#minby) | The smallest element compared after mapping applied | 52 | | [maxBy](#maxby) | The greatest element compared after mapping applied | 53 | 54 | #### Sequence operations 55 | 56 | | Name | Description | 57 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 58 | | [increasing](#increasing) | Increasing sequence, other elements dropped | 59 | | [increasingOrEqual](#increasingorequal) | Increasing (or equal) sequence, other elements dropped | 60 | | [decreasing](#decreasing) | Decreasing sequence, other elements dropped | 61 | | [decreasingOrEqual](#decreasingorequal) | Decreasing (or equal) sequence, other elements dropped | 62 | | [reverse](#reverse) | All elements in reverse order | 63 | | [rotate](#rotate) | All elements rotated left or right | 64 | | [shuffle](#shuffle) | All elements in random order | 65 | 66 | #### Mapping with position operations 67 | 68 | | Name | Description | 69 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 70 | | [mapFirst](#mapfirst) | Maps first element with mapper, other unchanged | 71 | | [mapN](#mapn) | Maps __n__ elements, other unchanged | 72 | | [skipAndMap](#skipandmap) | Skips __n__ elements, maps others | 73 | | [skipAndMapN](#skipandmapn) | Skips __skipN__ elements, maps __mapN__ others | 74 | | [mapWhile](#mapwhile) | Maps elements using the supplied function while the predicate evaluates to true. | 75 | | [mapUntil](#mapuntil) | Maps elements using the supplied function until the predicate evaluates to false. | 76 | 77 | #### Collection and chunking operations 78 | 79 | | Name | Description | 80 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 81 | | [increasingChunks](#increasingchunks) | Lists of increasing values | 82 | | [increasingOrEqualChunks](#increasingorequalchunks) | Lists of increasing or equal values | 83 | | [equalChunks](#equalchunks) | Lists of equal values | 84 | | [decreasingChunks](#decreasingchunks) | Lists of decreasing values | 85 | | [decreasingOrEqualChunks](#decreasingorequalchunks) | Lists of decreasing or equal values | 86 | | [nCopies](#ncopies) | Copies every element __n__ times | 87 | | [repeat](#repeat) | Collects the whole stream and repeats it __n__ times | 88 | | [atLeast](#atleast) | Distinct values that appear at least __n__ times | 89 | | [atMost](#atmost) | Distinct values that appear at most __n__ times | 90 | 91 | #### Indexing and zipping operations 92 | 93 | | Name | Description | 94 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 95 | | [zip](#zip) | Zips values with zipper, leftovers dropped | 96 | | [mapWithIndex](#zipwithindex) or [zipWithIndex](#zipwithindex) | Maps/zips values with an increasing index | 97 | | [peekWithIndex](#peekwithindex) | Peek at each element with its index | 98 | | [filterWithIndex](#filterwithindex) | Filter elements based on their index and a predicate | 99 | | [removeWithIndex](#removewithindex) | Remove elements based on their index and a predicate | 100 | | [windowSlidingWithIndex](#windowslidingwithindex) | Returns fixed-size windows of elements along with their indices | 101 | | [windowFixedWithIndex](#windowfixedwithindex) | Returns fixed-size non-overlapping windows of elements along with their indices | 102 | 103 | #### Element selection operations 104 | 105 | | Name | Description | 106 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 107 | | [sample](#sample) | Sample of the specified size | 108 | | [nth](#nth) | Takes nth element from the stream | 109 | | [dropNth](#dropnth) | Drops every nth element from the stream | 110 | | [last](#last) | Last __n__ elements | 111 | | [lastUnique](#lastunique) | Last __n__ unique elements | 112 | | [dropLast](#droplast) | Drops last __n__ elements | 113 | 114 | #### Text processing operations 115 | 116 | | Name | Description | 117 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 118 | | [chars](#chars) | String split by Unicode graphemes | 119 | | [words](#words) | String split by words | 120 | | [sentences](#sentences) | String split by sentences | 121 | 122 | #### Utility operations 123 | 124 | | Name | Description | 125 | |----------------------------------------------------------------|-----------------------------------------------------------------------------------| 126 | | [asGatherer](#asgatherer) | Converts `Collector` into `Gatherer` | 127 | | [identity](#identity) | Passes elements through unchanged | 128 | 129 | ### Filtering and mapping operations 130 | 131 | #### distinctBy 132 | 133 | `distinctBy(mapper)` - returns elements with distinct values that result from a mapping by the supplied function 134 | 135 | ```java 136 | import static io.github.jhspetersson.packrat.Packrat.distinctBy; 137 | var oneOddOneEven = IntStream.range(1, 10).boxed().gather(distinctBy(i -> i % 2)).toList(); 138 | System.out.println(oneOddOneEven); 139 | ``` 140 | > [1, 2] 141 | 142 | #### filterBy 143 | 144 | `filterBy(mapper, value)` - filters mapped elements based on the equality to the value, stream continues with original elements 145 | 146 | ```java 147 | import static io.github.jhspetersson.packrat.Packrat.filterBy; 148 | var oneDigitNumbers = IntStream.range(0, 100).boxed().gather(filterBy(i -> i.toString().length(), 1)).toList(); 149 | System.out.println(oneDigitNumbers); 150 | ``` 151 | > [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 152 | 153 | `filterBy(mapper, value, predicate)` - filters mapped elements based on the predicate test against the value, stream continues with original elements 154 | 155 | ```java 156 | import static io.github.jhspetersson.packrat.Packrat.filterBy; 157 | var ffValue = IntStream.range(0, 1000).boxed().gather(filterBy(Integer::toHexString, "ff", String::equalsIgnoreCase)).toList(); 158 | System.out.println(ffValue); 159 | ``` 160 | > [255] 161 | 162 | #### filterEntries 163 | 164 | `filterEntries(predicate)` - filters Map.Entry elements using a BiPredicate that tests the key and value of each entry 165 | 166 | ```java 167 | import static io.github.jhspetersson.packrat.Packrat.filterEntries; 168 | var map = Map.of("one", 1, "two", 2, "three", 3, "four", 4, "five", 5); 169 | 170 | // Filter entries where the value is even 171 | var evenValues = map.entrySet().stream() 172 | .gather(filterEntries((key, value) -> value % 2 == 0)) 173 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 174 | System.out.println(evenValues); 175 | ``` 176 | > {two=2, four=4} 177 | 178 | #### removeBy 179 | 180 | `removeBy(mapper, value)` - removes mapped elements based on the equality to the value, stream continues with original elements 181 | 182 | ```java 183 | import static io.github.jhspetersson.packrat.Packrat.removeBy; 184 | var oneDigitNumbers = IntStream.range(0, 100).boxed().gather(removeBy(i -> i.toString().length(), 2)).toList(); 185 | System.out.println(oneDigitNumbers); 186 | ``` 187 | > [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 188 | 189 | `removeBy(mapper, value, predicate)` - removes mapped elements based on the predicate test against the value, stream continues with original elements 190 | 191 | ```java 192 | import static io.github.jhspetersson.packrat.Packrat.removeBy; 193 | var ageDivisibleByThree = getEmployees().gather(removeBy(emp -> emp.age() % 3, 0, (i, value) -> !Objects.equals(i, value))).toList(); 194 | System.out.println(ageDivisibleByThree); 195 | ``` 196 | > [Employee[name=Mark Bloom, age=21], Employee[name=Rebecca Schneider, age=24]] 197 | 198 | #### removeEntries 199 | 200 | `removeEntries(predicate)` - removes Map.Entry elements using a BiPredicate that tests the key and value of each entry 201 | 202 | ```java 203 | import static io.github.jhspetersson.packrat.Packrat.removeEntries; 204 | var map = Map.of("one", 1, "two", 2, "three", 3, "four", 4, "five", 5); 205 | 206 | // Remove entries where the value is even 207 | var oddValues = map.entrySet().stream() 208 | .gather(removeEntries((key, value) -> value % 2 == 0)) 209 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 210 | System.out.println(oddValues); 211 | ``` 212 | > {one=1, three=3, five=5} 213 | 214 | #### removeDuplicates 215 | 216 | `removeDuplicates()` - removes consecutive duplicates from a stream, only adjacent elements that are equal will be considered duplicates 217 | 218 | ```java 219 | import static io.github.jhspetersson.packrat.Packrat.removeDuplicates; 220 | var listWithCopies = List.of(0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 8, 8, 9, 8, 7, 7, 6, 5, 4, 4, 4, 3, 2, 1, 0); 221 | var unique = listWithCopies.stream().gather(removeDuplicates()).toList(); 222 | System.out.println(unique); 223 | ``` 224 | > [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 225 | 226 | #### removeDuplicatesBy 227 | 228 | `removeDuplicatesBy(mapper)` - removes consecutive duplicates from a stream based on a mapping function, only adjacent elements that have equal mapped values will be considered duplicates 229 | 230 | ```java 231 | import static io.github.jhspetersson.packrat.Packrat.removeDuplicatesBy; 232 | var people = List.of( 233 | new Person("John", 25), 234 | new Person("Alice", 30), 235 | new Person("Bob", 30), 236 | new Person("Charlie", 30), 237 | new Person("David", 40), 238 | new Person("Eve", 40) 239 | ); 240 | var uniqueByAge = people.stream().gather(removeDuplicatesBy(Person::age)).toList(); 241 | System.out.println(uniqueByAge); 242 | ``` 243 | > [Person[name=John, age=25], Person[name=Alice, age=30], Person[name=David, age=40]] 244 | 245 | #### flatMapIf 246 | 247 | `flatMapIf(mapper, predicate)` - optionally flattens elements mapped to streams depending on the supplied predicate 248 | 249 | ```java 250 | import static io.github.jhspetersson.packrat.Packrat.flatMapIf; 251 | var strings = Stream.of("A", "BC", "DEF"); 252 | var result = strings.gather(flatMapIf(s -> Arrays.stream(s.split("")), s -> s.length() >= 3)).toList(); 253 | System.out.println(result); 254 | ``` 255 | 256 | > [A, BC, D, E, F] 257 | 258 | #### minBy 259 | 260 | `minBy(mapper)` - returns the smallest element in the stream, comparing is done after mapping function applied. 261 | 262 | ```java 263 | import static io.github.jhspetersson.packrat.Packrat.minBy; 264 | var check = Stream.of("2", "1", "-12", "22", "10").gather(minBy(Long::parseLong)).toList(); 265 | System.out.println(check); 266 | ``` 267 | 268 | > [-12] 269 | 270 | However, resulting list contains an original element of type `String`; 271 | 272 | `minBy(mapper, comparator)` - returns the smallest element in the stream, comparing with given comparator is done after mapping function applied. 273 | 274 | > [!CAUTION] 275 | > This gatherer will consume the entire stream before producing any output. 276 | 277 | #### maxBy 278 | 279 | `maxBy(mapper)` - returns the greatest element in the stream, comparing is done after mapping function applied. 280 | 281 | ```java 282 | import static io.github.jhspetersson.packrat.Packrat.maxBy; 283 | var check = Stream.of("2", "1", "-12", "22", "10").gather(maxBy(Long::parseLong)).toList(); 284 | System.out.println(check); 285 | ``` 286 | 287 | > [22] 288 | 289 | However, resulting list contains an original element of type `String`; 290 | 291 | `maxBy(mapper, comparator)` - returns the greatest element in the stream, comparing with given comparator is done after mapping function applied. 292 | 293 | > [!CAUTION] 294 | > This gatherer will consume the entire stream before producing any output. 295 | 296 | ### Sequence operations 297 | 298 | #### increasing 299 | 300 | `increasing()` - returns elements in an increasing sequence, elements out of the sequence, as well as repeating values, are dropped 301 | 302 | ```java 303 | import static io.github.jhspetersson.packrat.Packrat.increasing; 304 | var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20); 305 | var increasingNumbers = numbers.gather(increasing()).toList(); 306 | System.out.println(increasingNumbers); 307 | ``` 308 | 309 | > [1, 2, 5, 6, 9, 11, 20] 310 | 311 | #### increasingOrEqual 312 | 313 | `increasingOrEqual()` - returns elements in an increasing sequence, repeating values are preserved, elements out of the sequence are dropped 314 | 315 | #### decreasing 316 | 317 | `decreasing()` - returns elements in a decreasing sequence, elements out of the sequence, as well as repeating values, are dropped 318 | 319 | #### decreasingOrEqual 320 | 321 | `decreasingOrEqual()` - returns elements in a decreasing sequence, repeating values are preserved, elements out of the sequence are dropped 322 | 323 | #### reverse 324 | 325 | `reverse()` - reverses the elements 326 | 327 | ```java 328 | import static io.github.jhspetersson.packrat.Packrat.reverse; 329 | var reverseOrdered = IntStream.range(0, 10).boxed().gather(reverse()).toList(); 330 | System.out.println(reverseOrdered); 331 | ``` 332 | > [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 333 | 334 | > [!CAUTION] 335 | > This gatherer will consume the entire stream before producing any output. 336 | 337 | #### rotate 338 | 339 | `rotate(distance)` - rotates the elements 340 | 341 | ```java 342 | import static io.github.jhspetersson.packrat.Packrat.rotate; 343 | var positiveRotation = IntStream.range(0, 10).boxed().gather(rotate(3)).toList(); 344 | System.out.println(positiveRotation); 345 | var negativeRotation = IntStream.range(0, 10).boxed().gather(rotate(-4)).toList(); 346 | System.out.println(negativeRotation); 347 | ``` 348 | > [7, 8, 9, 0, 1, 2, 3, 4, 5, 6] 349 | 350 | > [4, 5, 6, 7, 8, 9, 0, 1, 2, 3] 351 | 352 | > [!CAUTION] 353 | > This gatherer will consume the entire stream before producing any output. 354 | 355 | #### shuffle 356 | 357 | `shuffle()` - shuffle the elements 358 | 359 | ```java 360 | import static io.github.jhspetersson.packrat.Packrat.shuffle; 361 | var randomlyOrdered = IntStream.range(0, 10).boxed().gather(shuffle()).toList(); 362 | System.out.println(randomlyOrdered); 363 | ``` 364 | > [2, 7, 6, 9, 8, 5, 1, 3, 0, 4] 365 | 366 | > [!CAUTION] 367 | > This gatherer will consume the entire stream before producing any output. 368 | 369 | ### Mapping with position operations 370 | 371 | #### mapFirst 372 | 373 | `mapFirst(mapper)` - returns all elements, the first element is mapped with the supplied mapping function 374 | 375 | ```java 376 | import static io.github.jhspetersson.packrat.Packrat.mapFirst; 377 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(mapFirst(n -> n * 10)).toList(); 378 | System.out.println(mapped); 379 | ``` 380 | 381 | > [10, 2, 3, 4, 5, 6, 7, 8, 9, 10] 382 | 383 | #### mapN 384 | 385 | `mapN(n, mapper)` - returns all elements, the first __n__ elements are mapped with the supplied mapping function 386 | 387 | ```java 388 | import static io.github.jhspetersson.packrat.Packrat.mapN; 389 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(mapN(5, n -> n * 10)).toList(); 390 | System.out.println(mapped); 391 | ``` 392 | 393 | > [10, 20, 30, 40, 50, 6, 7, 8, 9, 10] 394 | 395 | #### skipAndMap 396 | 397 | `skipAndMap(n, mapper)` - returns all elements that after the first __n__ are mapped with the supplied mapping function 398 | 399 | ```java 400 | import static io.github.jhspetersson.packrat.Packrat.skipAndMap; 401 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(skipAndMap(3, n -> n * 10)).toList(); 402 | System.out.println(mapped); 403 | ``` 404 | 405 | > [1, 2, 3, 40, 50, 60, 70, 80, 90, 100] 406 | 407 | #### skipAndMapN 408 | 409 | `skipAndMapN(skipN, mapN, mapper)` - returns all elements, after __skipN__ elements the first __mapN__ elements are mapped with the supplied mapping function 410 | 411 | ```java 412 | import static io.github.jhspetersson.packrat.Packrat.skipAndMapN; 413 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(skipAndMapN(3, 5, n -> n * 10)).toList(); 414 | System.out.println(mapped); 415 | ``` 416 | 417 | > [1, 2, 3, 40, 50, 60, 70, 80, 9, 10] 418 | 419 | #### mapWhile 420 | 421 | `mapWhile(predicate, mapper)` - maps elements using the supplied function while the predicate evaluates to true 422 | 423 | ```java 424 | import static io.github.jhspetersson.packrat.Packrat.mapWhile; 425 | var numbers = IntStream.rangeClosed(1, 10).boxed().gather(mapWhile(n -> n * 10, n -> n < 5)).toList(); 426 | System.out.println(numbers); 427 | ``` 428 | 429 | > [10, 20, 30, 40, 5, 6, 7, 8, 9, 10] 430 | 431 | #### mapUntil 432 | 433 | `mapUntil(predicate, mapper)` - maps elements using the supplied function until the predicate evaluates to false 434 | 435 | ```java 436 | import static io.github.jhspetersson.packrat.Packrat.mapUntil; 437 | var numbers = IntStream.rangeClosed(1, 10).boxed().gather(mapUntil(n -> n * 10, n -> n == 5)).toList(); 438 | System.out.println(numbers); 439 | ``` 440 | 441 | > [10, 20, 30, 40, 5, 6, 7, 8, 9, 10] 442 | 443 | ### Collection and chunking operations 444 | 445 | #### increasingChunks 446 | 447 | `increasingChunks()` - returns lists ("chunks") of elements, where each next element is greater than the previous one 448 | 449 | ```java 450 | import static io.github.jhspetersson.packrat.Packrat.increasingChunks; 451 | var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20); 452 | var result = numbers.gather(increasingChunks()).toList(); 453 | System.out.println(result); 454 | ``` 455 | 456 | > [[1, 2], [2, 5], [4], [2, 6, 9], [3, 11], [0, 1, 20]] 457 | 458 | #### increasingOrEqualChunks 459 | 460 | `increasingOrEqualChunks()` - returns lists ("chunks") of elements, where each next element is greater or equal than the previous one 461 | 462 | ```java 463 | import static io.github.jhspetersson.packrat.Packrat.increasingOrEqualChunks; 464 | var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20); 465 | var result = numbers.gather(increasingOrEqualChunks()).toList(); 466 | System.out.println(result); 467 | ``` 468 | 469 | > [[1, 2, 2, 5], [4], [2, 6, 9], [3, 11], [0, 1, 20]] 470 | 471 | #### equalChunks 472 | 473 | `equalChunks()` - returns lists ("chunks") of elements, where all elements in a chunk are equal to each other 474 | 475 | ```java 476 | import static io.github.jhspetersson.packrat.Packrat.equalChunks; 477 | var numbers = Stream.of(1, 1, 2, 2, 2, 3, 4, 4, 5, 5, 5, 5, 6); 478 | var result = numbers.gather(equalChunks()).toList(); 479 | System.out.println(result); 480 | ``` 481 | 482 | > [[1, 1], [2, 2, 2], [3], [4, 4], [5, 5, 5, 5], [6]] 483 | 484 | `equalChunksBy(mapper)` - returns lists ("chunks") of elements, where all elements in a chunk have equal values after applying the mapper function 485 | 486 | ```java 487 | import static io.github.jhspetersson.packrat.Packrat.equalChunks; 488 | var strings = Stream.of("apple", "apricot", "banana", "blueberry", "cherry", "date"); 489 | var result = strings.gather(equalChunks(s -> s.charAt(0))).toList(); 490 | System.out.println(result); 491 | ``` 492 | 493 | > [[apple, apricot], [banana, blueberry], [cherry], [date]] 494 | 495 | `equalChunks(comparator)` - returns lists ("chunks") of elements, where all elements in a chunk are equal according to the supplied comparator 496 | 497 | ```java 498 | import static io.github.jhspetersson.packrat.Packrat.equalChunks; 499 | // Case-insensitive string comparison 500 | var strings = Stream.of("Apple", "apple", "Banana", "banana", "Cherry", "cherry"); 501 | var result = strings.gather(equalChunks(String.CASE_INSENSITIVE_ORDER)).toList(); 502 | System.out.println(result); 503 | ``` 504 | 505 | > [[Apple, apple], [Banana, banana], [Cherry, cherry]] 506 | 507 | `equalChunksBy(mapper, comparator)` - returns lists ("chunks") of elements, where all elements in a chunk have equal values after applying the mapper function, with equality determined by the supplied comparator 508 | 509 | ```java 510 | import static io.github.jhspetersson.packrat.Packrat.equalChunks; 511 | record Person(String name, String id) {} 512 | 513 | // Group people by the first letter of their ID, case-insensitive 514 | var people = Stream.of( 515 | new Person("John", "A123"), 516 | new Person("Alice", "a456"), 517 | new Person("Bob", "B789"), 518 | new Person("Charlie", "b012"), 519 | new Person("David", "C345"), 520 | new Person("Eve", "c678") 521 | ); 522 | 523 | var result = people.gather(equalChunks( 524 | p -> p.id().substring(0, 1), // Map to first letter of ID 525 | String.CASE_INSENSITIVE_ORDER // Compare case-insensitive 526 | )).toList(); 527 | 528 | System.out.println(result); 529 | ``` 530 | 531 | > [[Person[name=John, id=A123], Person[name=Alice, id=a456]], [Person[name=Bob, id=B789], Person[name=Charlie, id=b012]], [Person[name=David, id=C345], Person[name=Eve, id=c678]]] 532 | 533 | #### decreasingChunks 534 | 535 | `decreasingChunks()` - returns lists ("chunks") of elements, where each next element is less than the previous one 536 | 537 | #### decreasingOrEqualChunks 538 | 539 | `decreasingOrEqualChunks()` - returns lists ("chunks") of elements, where each next element is less or equal than the previous one 540 | 541 | #### nCopies 542 | 543 | `nCopies(n)` - returns __n__ copies of every element, __n__ less than or equal to zero effectively empties the stream 544 | 545 | ```java 546 | import static io.github.jhspetersson.packrat.Packrat.nCopies; 547 | var numbers = IntStream.of(5).boxed().gather(nCopies(10)).toList(); 548 | System.out.println(numbers); 549 | ``` 550 | 551 | > [5, 5, 5, 5, 5, 5, 5, 5, 5, 5] 552 | 553 | #### repeat 554 | 555 | `repeat(n)` - collects the whole stream and repeats it __n__ times, __n__ equal to zero effectively empties the stream 556 | 557 | ```java 558 | import static io.github.jhspetersson.packrat.Packrat.repeat; 559 | var numbers = Stream.of(1, 2, 3).gather(repeat(2)).toList(); 560 | System.out.println(numbers); 561 | ``` 562 | 563 | > [1, 2, 3, 1, 2, 3] 564 | 565 | > [!CAUTION] 566 | > This gatherer will consume the entire stream before producing any output. 567 | 568 | #### atLeast 569 | 570 | `atLeast(n)` - returns distinct elements that appear at least __n__ times in the stream 571 | 572 | ```java 573 | import static io.github.jhspetersson.packrat.Packrat.atLeast; 574 | var numbers = Stream.of(1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 8, 8, 8, 8, 9, 10); 575 | var atLeastThree = numbers.gather(atLeast(3)).toList(); 576 | System.out.println(atLeastThree); 577 | ``` 578 | > [3, 3, 3, 8, 8, 8, 8] 579 | 580 | #### atMost 581 | 582 | `atMost(n)` - returns distinct elements that appear at most __n__ times in the stream 583 | 584 | ```java 585 | import static io.github.jhspetersson.packrat.Packrat.atMost; 586 | var numbers = Stream.of(1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 8, 8, 8, 8, 9, 10); 587 | var atMostTwo = numbers.gather(atMost(2)).toList(); 588 | System.out.println(atMostTwo); 589 | ``` 590 | > [1, 2, 4, 5, 5, 6, 7, 9, 10] 591 | 592 | `atMostBy(n, mapper)` - returns distinct elements mapped by the supplied function that appear at most __n__ times in the stream 593 | 594 | ```java 595 | import static io.github.jhspetersson.packrat.Packrat.atMostBy; 596 | var strings = Stream.of("apple", "banana", "cherry", "date", "elderberry", "fig", "grape"); 597 | var uniqueLengths = strings.gather(atMostBy(1, String::length)).toList(); 598 | System.out.println(uniqueLengths); 599 | ``` 600 | > [date, elderberry, fig] 601 | 602 | > [!CAUTION] 603 | > This gatherer will consume the entire stream before producing any output. 604 | 605 | ### Indexing and zipping operations 606 | 607 | #### zip 608 | 609 | `zip(input, mapper)` - returns elements mapped ("zipped") with the values from some other stream, iterable or iterator. 610 | 611 | ```java 612 | import static io.github.jhspetersson.packrat.Packrat.zip; 613 | var names = List.of("Anna", "Mike", "Sandra"); 614 | var ages = Stream.of(20, 30, 40, 50, 60, 70, 80, 90); 615 | var users = names.stream().gather(zip(ages, User::new)).toList(); 616 | System.out.println(users); 617 | ``` 618 | 619 | > [User[name=Anna, age=20], User[name=Mike, age=30], User[name=Sandra, age=40]] 620 | 621 | `zip(input)` - zips current stream and input into Map entries. 622 | 623 | ```java 624 | import static io.github.jhspetersson.packrat.Packrat.zip; 625 | var names = List.of("Anna", "Mike", "Sandra"); 626 | var ages = Stream.of(20, 30, 40); 627 | var users = names.stream().gather(zip(ages)).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 628 | System.out.println(users); 629 | ``` 630 | 631 | > {Mike=30, Anna=20, Sandra=40} 632 | 633 | #### zipWithIndex 634 | 635 | `zipWithIndex()` - zips current stream with an increasing index into Map entries. 636 | 637 | ```java 638 | import static io.github.jhspetersson.packrat.Packrat.zipWithIndex; 639 | var names = List.of("Anna", "Mike", "Sandra"); 640 | var users = names.stream().gather(zipWithIndex()).toList(); 641 | ``` 642 | 643 | > [0=Anna, 1=Mike, 2=Sandra] 644 | 645 | `zipWithIndex(startIndex)` - zips current stream with an increasing index (beginning with _startIndex_) into Map entries. 646 | 647 | ```java 648 | import static io.github.jhspetersson.packrat.Packrat.zipWithIndex; 649 | var names = List.of("Anna", "Mike", "Sandra"); 650 | var users = names.stream().gather(zipWithIndex(10)).toList(); 651 | ``` 652 | 653 | > [10=Anna, 11=Mike, 12=Sandra] 654 | 655 | `mapWithIndex(mapper)` or `zipWithIndex(mapper)` - maps/zips current stream with an increasing index, mapping function receives the index as the first argument. 656 | 657 | ```java 658 | import static io.github.jhspetersson.packrat.Packrat.zipWithIndex; 659 | var names = List.of("Anna", "Mike", "Sandra"); 660 | var users = names.stream().gather(zipWithIndex(User::new)).toList(); 661 | ``` 662 | 663 | > [User[index=0, name=Anna], User[index=1, name=Mike], User[index=2, name=Sandra]] 664 | 665 | `mapWithIndex(mapper, startIndex)` or `zipWithIndex(mapper, startIndex)` - maps/zips current stream with an increasing index (beginning with _startIndex_), mapping function receives the index as the first argument. 666 | 667 | ```java 668 | import static io.github.jhspetersson.packrat.Packrat.zipWithIndex; 669 | var names = List.of("Anna", "Mike", "Sandra"); 670 | var users = names.stream().gather(zipWithIndex(User::new, 10)).toList(); 671 | ``` 672 | 673 | > [User[index=10, name=Anna], User[index=11, name=Mike], User[index=12, name=Sandra]] 674 | 675 | #### peekWithIndex 676 | 677 | `peekWithIndex(consumer)` - peeks at each element along with its index (starting from 0), but passes the original element downstream unchanged 678 | 679 | ```java 680 | import static io.github.jhspetersson.packrat.Packrat.peekWithIndex; 681 | var names = List.of("Anna", "Mike", "Sandra"); 682 | var result = names.stream().gather(peekWithIndex((index, name) -> 683 | System.out.println("Element at index " + index + ": " + name))).toList(); 684 | System.out.println(result); 685 | ``` 686 | 687 | > Element at index 0: Anna 688 | > Element at index 1: Mike 689 | > Element at index 2: Sandra 690 | > [Anna, Mike, Sandra] 691 | 692 | `peekWithIndex(consumer, startIndex)` - peeks at each element along with its index (beginning with _startIndex_), but passes the original element downstream unchanged 693 | 694 | ```java 695 | import static io.github.jhspetersson.packrat.Packrat.peekWithIndex; 696 | var names = List.of("Anna", "Mike", "Sandra"); 697 | var result = names.stream().gather(peekWithIndex((index, name) -> 698 | System.out.println("Element at index " + index + ": " + name), 10)).toList(); 699 | System.out.println(result); 700 | ``` 701 | 702 | > Element at index 10: Anna 703 | > Element at index 11: Mike 704 | > Element at index 12: Sandra 705 | > [Anna, Mike, Sandra] 706 | 707 | #### filterWithIndex 708 | 709 | `filterWithIndex(predicate)` - filters elements based on their index and a predicate, the index starts from 0 710 | 711 | ```java 712 | import static io.github.jhspetersson.packrat.Packrat.filterWithIndex; 713 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 714 | var result = numbers.gather(filterWithIndex((index, element) -> index % 2 == 0)).toList(); 715 | System.out.println(result); 716 | ``` 717 | 718 | > [1, 3, 5, 7, 9] 719 | 720 | `filterWithIndex(predicate, startIndex)` - filters elements based on their index and a predicate, the index starts from _startIndex_ 721 | 722 | ```java 723 | import static io.github.jhspetersson.packrat.Packrat.filterWithIndex; 724 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 725 | var result = numbers.gather(filterWithIndex((index, element) -> index % 2 == 0, 1)).toList(); 726 | System.out.println(result); 727 | ``` 728 | 729 | > [2, 4, 6, 8, 10] 730 | 731 | #### removeWithIndex 732 | 733 | `removeWithIndex(predicate)` - removes elements based on their index and a predicate, the index starts from 0 734 | 735 | ```java 736 | import static io.github.jhspetersson.packrat.Packrat.removeWithIndex; 737 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 738 | var result = numbers.gather(removeWithIndex((index, element) -> index % 2 == 0)).toList(); 739 | System.out.println(result); 740 | ``` 741 | 742 | > [2, 4, 6, 8, 10] 743 | 744 | `removeWithIndex(predicate, startIndex)` - removes elements based on their index and a predicate, the index starts from _startIndex_ 745 | 746 | ```java 747 | import static io.github.jhspetersson.packrat.Packrat.removeWithIndex; 748 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 749 | var result = numbers.gather(removeWithIndex((index, element) -> index % 2 == 0, 1)).toList(); 750 | System.out.println(result); 751 | ``` 752 | 753 | > [1, 3, 5, 7, 9] 754 | 755 | #### windowSlidingWithIndex 756 | 757 | `windowSlidingWithIndex(windowSize)` - returns fixed-size windows of elements along with their indices, the index starts from 0 758 | 759 | ```java 760 | import static io.github.jhspetersson.packrat.Packrat.windowSlidingWithIndex; 761 | var numbers = IntStream.rangeClosed(1, 5).boxed(); 762 | var result = numbers.gather(windowSlidingWithIndex(3)).toList(); 763 | System.out.println(result); 764 | ``` 765 | 766 | > [0=[1, 2, 3], 1=[2, 3, 4], 2=[3, 4, 5]] 767 | 768 | `windowSlidingWithIndex(windowSize, startIndex)` - returns fixed-size windows of elements along with their indices, the index starts from _startIndex_ 769 | 770 | ```java 771 | import static io.github.jhspetersson.packrat.Packrat.windowSlidingWithIndex; 772 | var numbers = IntStream.rangeClosed(1, 5).boxed(); 773 | var result = numbers.gather(windowSlidingWithIndex(3, 10)).toList(); 774 | System.out.println(result); 775 | ``` 776 | 777 | > [10=[1, 2, 3], 11=[2, 3, 4], 12=[3, 4, 5]] 778 | 779 | `windowSlidingWithIndex(windowSize, mapper)` - returns fixed-size windows of elements along with their indices 780 | `windowSlidingWithIndex(windowSize, mapper, startIndex)` - returns fixed-size windows of elements along with their indices, the index starts from _startIndex_ 781 | 782 | #### windowFixedWithIndex 783 | 784 | `windowFixedWithIndex(windowSize)` - returns fixed-size non-overlapping windows of elements along with their indices, the index starts from 0 785 | 786 | ```java 787 | import static io.github.jhspetersson.packrat.Packrat.windowFixedWithIndex; 788 | var numbers = IntStream.rangeClosed(1, 10).boxed(); 789 | var result = numbers.gather(windowFixedWithIndex(3)).toList(); 790 | System.out.println(result); 791 | ``` 792 | 793 | > [0=[1, 2, 3], 1=[4, 5, 6], 2=[7, 8, 9]] 794 | 795 | `windowFixedWithIndex(windowSize, startIndex)` - returns fixed-size non-overlapping windows of elements along with their indices, the index starts from _startIndex_ 796 | 797 | ```java 798 | import static io.github.jhspetersson.packrat.Packrat.windowFixedWithIndex; 799 | var numbers = IntStream.rangeClosed(1, 6).boxed(); 800 | var result = numbers.gather(windowFixedWithIndex(2, 10)).toList(); 801 | System.out.println(result); 802 | ``` 803 | 804 | > [10=[1, 2], 11=[3, 4], 12=[5, 6]] 805 | 806 | `windowFixedWithIndex(windowSize, mapper)` - returns fixed-size non-overlapping windows of elements along with their indices 807 | `windowFixedWithIndex(windowSize, mapper, startIndex)` - returns fixed-size non-overlapping windows of elements along with their indices, the index starts from _startIndex_ 808 | 809 | ### Element selection operations 810 | 811 | #### sample 812 | 813 | `sample(n)` - returns a sample of the specified size from the stream of elements. 814 | 815 | ```java 816 | import static io.github.jhspetersson.packrat.Packrat.sample; 817 | var source = IntStream.range(0, 100).boxed().gather(sample(10)).toList(); 818 | System.out.println(source); 819 | ``` 820 | > [0, 8, 27, 33, 65, 66, 88, 90, 93, 96] 821 | 822 | `sample(n, maxSpan)` - returns a sample of the specified size from the stream of elements, inspects first __maxSpan__ elements. 823 | 824 | #### nth 825 | 826 | `nth(n)` - takes every nth element from the stream 827 | 828 | ```java 829 | import static io.github.jhspetersson.packrat.Packrat.nth; 830 | var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 831 | var result = numbers.stream().gather(nth(3)).toList(); 832 | System.out.println(result); 833 | ``` 834 | 835 | > [3, 6, 9] 836 | 837 | #### dropNth 838 | 839 | `dropNth(n)` - drops every nth element from the stream 840 | 841 | ```java 842 | import static io.github.jhspetersson.packrat.Packrat.dropNth; 843 | var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 844 | var result = numbers.stream().gather(dropNth(3)).toList(); 845 | System.out.println(result); 846 | ``` 847 | 848 | > [1, 2, 4, 5, 7, 8, 10] 849 | 850 | #### last 851 | 852 | `last()` - returns last element from the stream. 853 | 854 | ```java 855 | import static io.github.jhspetersson.packrat.Packrat.last; 856 | var integers = IntStream.range(0, 100).boxed().gather(last()).toList(); 857 | System.out.println(integers); 858 | ``` 859 | 860 | > [99] 861 | 862 | `last(n)` - returns __n__ last elements from the stream. 863 | 864 | ```java 865 | import static io.github.jhspetersson.packrat.Packrat.last; 866 | var integers = IntStream.range(0, 100).boxed().gather(last(10)).toList(); 867 | System.out.println(integers); 868 | ``` 869 | 870 | > [90, 91, 92, 93, 94, 95, 96, 97, 98, 99] 871 | 872 | > [!CAUTION] 873 | > This gatherer will consume the entire stream before producing any output. 874 | 875 | #### lastUnique 876 | 877 | `lastUnique(n)` - returns __n__ last unique elements from the stream. 878 | 879 | ```java 880 | import static io.github.jhspetersson.packrat.Packrat.lastUnique; 881 | var integers = List.of(1, 2, 3, 4, 5, 4, 1, 1, 1, 2, 2, 6).stream().gather(lastUnique(3)).toList(); 882 | System.out.println(integers); 883 | ``` 884 | 885 | > [1, 2, 6] 886 | 887 | > [!CAUTION] 888 | > This gatherer will consume the entire stream before producing any output. 889 | 890 | #### dropLast 891 | 892 | `dropLast()` - drops last element. 893 | 894 | `dropLast(n)` - drops last __n__ elements from the stream. 895 | 896 | ```java 897 | import static io.github.jhspetersson.packrat.Packrat.dropLast; 898 | var integers = IntStream.range(0, 10).boxed().gather(dropLast(3)).toList(); 899 | System.out.println(integers); 900 | ``` 901 | 902 | > [0, 1, 2, 3, 4, 5, 6] 903 | 904 | > [!CAUTION] 905 | > This gatherer will consume the entire stream before producing any output. 906 | 907 | ### Text processing operations 908 | 909 | #### chars 910 | 911 | `chars()` - returns characters as strings parsed from the stream elements 912 | 913 | ```java 914 | import static io.github.jhspetersson.packrat.Packrat.chars; 915 | var charStrings = Stream.of("Hello, \uD83D\uDC22!").gather(chars()).toList(); 916 | System.out.println(charStrings); 917 | ``` 918 | 919 | > [H, e, l, l, o, ,, , 🐢, !] 920 | 921 | #### words 922 | 923 | `words()` - returns words as strings parsed from the stream elements 924 | 925 | ```java 926 | import static io.github.jhspetersson.packrat.Packrat.words; 927 | var wordStrings = Stream.of("Another test!").gather(words()).toList(); 928 | System.out.println(wordStrings); 929 | ``` 930 | 931 | > [Another, test, !] 932 | 933 | #### sentences 934 | 935 | `sentences()` - returns sentences as strings parsed from the stream elements 936 | 937 | ```java 938 | import static io.github.jhspetersson.packrat.Packrat.sentences; 939 | var sentenceStrings = Stream.of("And another one. How many left?").gather(sentences()).toList(); 940 | System.out.println(sentenceStrings); 941 | ``` 942 | 943 | > [And another one. , How many left?] 944 | 945 | ### Utility operations 946 | 947 | #### asGatherer 948 | 949 | `asGatherer(collector)` - provides the result of the supplied collector as a single element into the stream, effectively converts any Collector into a Gatherer 950 | 951 | ```java 952 | import static io.github.jhspetersson.packrat.Packrat.asGatherer; 953 | var numbers = Stream.of(1, 2, 3, 4, 5); 954 | var listOfCollectedList = numbers.gather(asGatherer(Collectors.toList())).toList(); 955 | System.out.println(listOfCollectedList); 956 | ``` 957 | 958 | > [[1, 2, 3, 4, 5]] 959 | 960 | #### identity 961 | 962 | `identity()` - returns a gatherer that passes elements through unchanged 963 | 964 | ```java 965 | import static io.github.jhspetersson.packrat.Packrat.identity; 966 | var numbers = IntStream.range(0, 5).boxed(); 967 | var sameNumbers = numbers.gather(identity()).toList(); 968 | System.out.println(sameNumbers); 969 | ``` 970 | 971 | > [0, 1, 2, 3, 4] 972 | 973 | ### License 974 | 975 | Apache-2.0 976 | 977 | --- 978 | Supported by [JetBrains IDEA](https://jb.gg/OpenSourceSupport) open source license. 979 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'maven-publish' 4 | } 5 | 6 | group = 'io.github.jhspetersson' 7 | version = '0.2.0' 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | compileOnly 'org.jspecify:jspecify:1.0.0' 15 | testImplementation platform('org.junit:junit-bom:5.11.2') 16 | testImplementation 'org.junit.jupiter:junit-jupiter' 17 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 18 | } 19 | 20 | test { 21 | useJUnitPlatform() 22 | } 23 | 24 | jar { 25 | manifest { 26 | attributes('Implementation-Title': project.name, 27 | 'Implementation-Version': project.version) 28 | } 29 | } 30 | 31 | tasks.register('sourcesJar', Jar) { 32 | archiveClassifier.set('sources') 33 | from sourceSets.main.allSource 34 | } 35 | 36 | tasks.register('javadocJar', Jar) { 37 | archiveClassifier.set('javadoc') 38 | from tasks.javadoc 39 | } 40 | 41 | artifacts { 42 | archives sourcesJar 43 | archives javadocJar 44 | } 45 | 46 | publishing { 47 | publications { 48 | myPublication(MavenPublication) { 49 | from components.java 50 | 51 | pom { 52 | name = "${project.name}" 53 | description = 'Gatherers library for Java Stream API' 54 | url = 'https://github.com/jhspetersson/packrat' 55 | 56 | licenses { 57 | license { 58 | name = 'The Apache License, Version 2.0' 59 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 60 | } 61 | } 62 | 63 | developers { 64 | developer { 65 | name = 'Ivan Petrichenko' 66 | email = 'jhspetersson@gmail.com' 67 | organizationUrl = 'https://github.com/jhspetersson' 68 | } 69 | } 70 | 71 | scm { 72 | connection = 'scm:git://github.com/jhspetersson/packrat.git' 73 | developerConnection = 'scm:git:ssh://github.com/jhspetersson/packrat.git' 74 | url = 'https://github.com/jhspetersson/packrat' 75 | } 76 | } 77 | } 78 | } 79 | 80 | repositories { 81 | maven { 82 | url = layout.buildDirectory.dir('fake-repo') 83 | } 84 | } 85 | } 86 | 87 | tasks.register('generatePom', GenerateMavenPom) { 88 | group = 'build' 89 | description = 'Generates the POM file without publishing.' 90 | destination = file("${layout.buildDirectory.asFile.get()}/libs/${rootProject.name}-${project.version}.pom") 91 | pom = publishing.publications.myPublication.pom 92 | 93 | doLast { 94 | println "POM file written to: ${destination}" 95 | } 96 | } 97 | 98 | build.finalizedBy { 99 | tasks.generatePom 100 | } 101 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhspetersson/packrat/30560b76565e640ad44fca27aab7bbe59de288b9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'packrat' -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/AtLeastGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.function.Function; 9 | import java.util.function.Supplier; 10 | import java.util.stream.Gatherer; 11 | 12 | /** 13 | * Returns distinct elements that appear at least n times in the stream. 14 | * 15 | * @param element type 16 | * @param mapped element type 17 | * @author jhspetersson 18 | */ 19 | class AtLeastGatherer implements Gatherer>, T> { 20 | private final long atLeast; 21 | private final Function mapper; 22 | 23 | AtLeastGatherer(long atLeast, Function mapper) { 24 | if (atLeast < 0) { 25 | throw new IllegalArgumentException("atLeast must be a non-negative number"); 26 | } 27 | Objects.requireNonNull(mapper, "mapper cannot be null"); 28 | 29 | this.atLeast = atLeast; 30 | this.mapper = mapper; 31 | } 32 | 33 | @Override 34 | public Supplier>> initializer() { 35 | return HashMap::new; 36 | } 37 | 38 | @Override 39 | public Integrator>, T, T> integrator() { 40 | return Integrator.of((state, element, downstream) -> { 41 | var mappedValue = mapper.apply(element); 42 | var elementList = state.computeIfAbsent(mappedValue, _ -> new ArrayList<>()); 43 | var count = elementList.size(); 44 | if (count + 1 == atLeast) { 45 | for (var t : elementList) { 46 | var res = downstream.push(t); 47 | if (!res) { 48 | return false; 49 | } 50 | } 51 | elementList.add(element); 52 | return downstream.push(element); 53 | } else if (count + 1 > atLeast) { 54 | return downstream.push(element); 55 | } else { 56 | elementList.add(element); 57 | } 58 | return !downstream.isRejecting(); 59 | }); 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/AtMostGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.function.BiConsumer; 9 | import java.util.function.Function; 10 | import java.util.function.Supplier; 11 | import java.util.stream.Gatherer; 12 | 13 | /** 14 | * Returns elements that appear at most n times in the stream. 15 | * 16 | * @param element type 17 | * @param mapped element type 18 | * @author jhspetersson 19 | */ 20 | class AtMostGatherer implements Gatherer>, T> { 21 | private final long atMost; 22 | private final Function mapper; 23 | 24 | AtMostGatherer(long atMost, Function mapper) { 25 | if (atMost < 0) { 26 | throw new IllegalArgumentException("atMost must be a positive number"); 27 | } 28 | Objects.requireNonNull(mapper, "mapper cannot be null"); 29 | 30 | this.atMost = atMost; 31 | this.mapper = mapper; 32 | } 33 | 34 | @Override 35 | public Supplier>> initializer() { 36 | return HashMap::new; 37 | } 38 | 39 | @Override 40 | public Integrator>, T, T> integrator() { 41 | return Integrator.ofGreedy((state, element, downstream) -> { 42 | var mappedValue = mapper.apply(element); 43 | var elementList = state.computeIfAbsent(mappedValue, _ -> new ArrayList<>()); 44 | if (elementList.size() <= atMost) { 45 | elementList.add(element); 46 | } 47 | return !downstream.isRejecting(); 48 | }); 49 | } 50 | 51 | @Override 52 | public BiConsumer>, Downstream> finisher() { 53 | return (state, downstream) -> { 54 | for (var entry : state.entrySet()) { 55 | var elementList = entry.getValue(); 56 | if (elementList.size() <= atMost) { 57 | for (var element : elementList) { 58 | if (!downstream.push(element)) { 59 | return; 60 | } 61 | } 62 | } 63 | } 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/BreakingGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.text.BreakIterator; 4 | import java.util.Objects; 5 | import java.util.stream.Gatherer; 6 | 7 | /** 8 | * Returns strings such as graphemes, words, lines or sentences parsed from the stream elements. 9 | * 10 | * @param element type 11 | * @author jhspetersson 12 | */ 13 | class BreakingGatherer implements Gatherer { 14 | private final BreakIterator breakIterator; 15 | private final boolean skipBlanks; 16 | 17 | BreakingGatherer(BreakIterator breakIterator) { 18 | this(breakIterator, false); 19 | } 20 | 21 | BreakingGatherer(BreakIterator breakIterator, boolean skipBlanks) { 22 | Objects.requireNonNull(breakIterator, "breakIterator cannot be null"); 23 | 24 | this.breakIterator = breakIterator; 25 | this.skipBlanks = skipBlanks; 26 | } 27 | 28 | @Override 29 | public Integrator integrator() { 30 | return Integrator.of((_, element, downstream) -> { 31 | if (element == null) { 32 | return downstream.push(null); 33 | } else { 34 | var str = element.toString(); 35 | breakIterator.setText(str); 36 | 37 | var idx = breakIterator.first(); 38 | var prevIdx = -1; 39 | while (idx != BreakIterator.DONE) { 40 | if (prevIdx != -1) { 41 | var part = str.substring(prevIdx, idx); 42 | if (!skipBlanks || !part.isBlank()) { 43 | var res = downstream.push(part); 44 | if (!res) { 45 | return false; 46 | } 47 | } 48 | } 49 | 50 | prevIdx = idx; 51 | idx = breakIterator.next(); 52 | } 53 | } 54 | return !downstream.isRejecting(); 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/CollectingGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.BiConsumer; 5 | import java.util.function.BinaryOperator; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Collector; 8 | import java.util.stream.Gatherer; 9 | 10 | /** 11 | * Provides the result of the supplied collector as a single element into the stream. 12 | * Effectively converts any Collector into a Gatherer. 13 | * 14 | * @param element type 15 | * @param state type 16 | * @param result type 17 | * @author jhspetersson 18 | */ 19 | public class CollectingGatherer implements Gatherer { 20 | private final Collector collector; 21 | 22 | /** 23 | * Constructs a new CollectingGatherer instance. 24 | * 25 | * @param collector the collector to be used for gathering elements, must not be null 26 | * @throws NullPointerException if the collector is null 27 | */ 28 | public CollectingGatherer(Collector collector) { 29 | Objects.requireNonNull(collector, "collector cannot be null"); 30 | 31 | this.collector = collector; 32 | } 33 | 34 | @Override 35 | public Supplier initializer() { 36 | return collector.supplier(); 37 | } 38 | 39 | @Override 40 | public Integrator integrator() { 41 | return Integrator.ofGreedy((state, element, downstream) -> { 42 | collector.accumulator().accept(state, element); 43 | return !downstream.isRejecting(); 44 | }); 45 | } 46 | 47 | @Override 48 | public BinaryOperator combiner() { 49 | return collector.combiner(); 50 | } 51 | 52 | @Override 53 | public BiConsumer> finisher() { 54 | return (state, downstream) -> downstream.push(collector.finisher().apply(state)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/DistinctByGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.HashSet; 4 | import java.util.Objects; 5 | import java.util.Set; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | import java.util.stream.Gatherer; 9 | 10 | /** 11 | * Returns elements with distinct values that result from a mapping by the supplied function. 12 | * 13 | * @param element type 14 | * @param mapped element type 15 | * @author jhspetersson 16 | */ 17 | class DistinctByGatherer implements Gatherer, T> { 18 | private final Function mapper; 19 | 20 | DistinctByGatherer(Function mapper) { 21 | Objects.requireNonNull(mapper, "mapper cannot be null"); 22 | 23 | this.mapper = mapper; 24 | } 25 | 26 | @Override 27 | public Supplier> initializer() { 28 | return HashSet::new; 29 | } 30 | 31 | @Override 32 | public Integrator, T, T> integrator() { 33 | return Integrator.of((state, element, downstream) -> { 34 | var mappedValue = mapper.apply(element); 35 | if (!state.contains(mappedValue)) { 36 | state.add(mappedValue); 37 | return downstream.push(element); 38 | } 39 | return !downstream.isRejecting(); 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/DropLastNGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Deque; 4 | import java.util.function.BiConsumer; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Drops last n elements. 10 | * 11 | * @param element type 12 | * @author jhspetersson 13 | */ 14 | class DropLastNGatherer implements Gatherer, T> { 15 | private final long n; 16 | 17 | DropLastNGatherer(long n) { 18 | if (n < 0) { 19 | throw new IllegalArgumentException("n must be a non-negative number"); 20 | } 21 | 22 | this.n = n; 23 | } 24 | 25 | @Override 26 | public Supplier> initializer() { 27 | return () -> new FixedSizeDeque<>((int) Math.max(n, 16)); 28 | } 29 | 30 | @Override 31 | public Integrator, T, T> integrator() { 32 | if (n == 0) { 33 | return Integrator.of((_, element, downstream) -> downstream.push(element)); 34 | } 35 | 36 | return Integrator.ofGreedy((deque, element, downstream) -> { 37 | deque.add(element); 38 | return !downstream.isRejecting(); 39 | }); 40 | } 41 | 42 | @Override 43 | public BiConsumer, Downstream> finisher() { 44 | return (deque, downstream) -> { 45 | var elementsToSkip = (int) Math.min(n, deque.size()); 46 | var elementsToProcess = deque.size() - elementsToSkip; 47 | 48 | for (var i = 0; i < elementsToProcess; i++) { 49 | if (!downstream.push(deque.removeFirst())) { 50 | break; 51 | } 52 | } 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/DropNthGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.function.Supplier; 4 | import java.util.stream.Gatherer; 5 | 6 | /** 7 | * Drops every nth element from the stream. 8 | * 9 | * @param element type 10 | * @author jhspetersson 11 | */ 12 | class DropNthGatherer implements Gatherer { 13 | private final int n; 14 | 15 | DropNthGatherer(int n) { 16 | if (n <= 0) { 17 | throw new IllegalArgumentException("n must be a positive number"); 18 | } 19 | 20 | this.n = n; 21 | } 22 | 23 | @Override 24 | public Supplier initializer() { 25 | return () -> new int[1]; 26 | } 27 | 28 | @Override 29 | public Integrator integrator() { 30 | return Integrator.of((state, element, downstream) -> { 31 | if (++state[0] % n != 0) { 32 | return downstream.push(element); 33 | } 34 | return !downstream.isRejecting(); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/EqualChunksGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.function.BiConsumer; 8 | import java.util.function.Function; 9 | import java.util.function.Supplier; 10 | import java.util.stream.Gatherer; 11 | 12 | /** 13 | * Returns lists ("chunks") of elements, where all elements in a chunk are equal after applying the mapper function. 14 | * 15 | * @param element type 16 | * @param mapped type for comparison 17 | * @author jhspetersson 18 | */ 19 | class EqualChunksGatherer implements Gatherer, List> { 20 | private final Function mapper; 21 | private final Comparator comparator; 22 | private U currentValue; 23 | private boolean first = true; 24 | 25 | EqualChunksGatherer(Function mapper) { 26 | this(mapper, null); 27 | } 28 | 29 | EqualChunksGatherer(Function mapper, Comparator comparator) { 30 | Objects.requireNonNull(mapper, "mapper cannot be null"); 31 | this.mapper = mapper; 32 | this.comparator = comparator; 33 | } 34 | 35 | @Override 36 | public Supplier> initializer() { 37 | return ArrayList::new; 38 | } 39 | 40 | @Override 41 | public Integrator, T, List> integrator() { 42 | return Integrator.of((state, element, downstream) -> { 43 | var mappedValue = mapper.apply(element); 44 | 45 | if (first) { 46 | first = false; 47 | currentValue = mappedValue; 48 | state.add(element); 49 | } else { 50 | boolean areEqual; 51 | if (comparator != null) { 52 | areEqual = comparator.compare(currentValue, mappedValue) == 0; 53 | } else { 54 | areEqual = Objects.equals(currentValue, mappedValue); 55 | } 56 | 57 | if (areEqual) { 58 | state.add(element); 59 | } else { 60 | var chunk = List.copyOf(state); 61 | state.clear(); 62 | state.add(element); 63 | currentValue = mappedValue; 64 | return downstream.push(chunk); 65 | } 66 | } 67 | 68 | return !downstream.isRejecting(); 69 | }); 70 | } 71 | 72 | @Override 73 | public BiConsumer, Downstream>> finisher() { 74 | return (state, downstream) -> { 75 | if (!state.isEmpty()) { 76 | var chunk = List.copyOf(state); 77 | downstream.push(chunk); 78 | } 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/FilterEntriesGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Map; 4 | import java.util.Objects; 5 | import java.util.function.BiPredicate; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Filters Map.Entry elements using a BiPredicate that tests the key and value of each entry. 10 | * 11 | * @param key type 12 | * @param value type 13 | * @author jhspetersson 14 | */ 15 | class FilterEntriesGatherer implements Gatherer, Void, Map.Entry> { 16 | private final BiPredicate predicate; 17 | private final boolean invert; 18 | 19 | FilterEntriesGatherer(BiPredicate predicate) { 20 | this(predicate, false); 21 | } 22 | 23 | FilterEntriesGatherer(BiPredicate predicate, boolean invert) { 24 | Objects.requireNonNull(predicate, "predicate cannot be null"); 25 | 26 | this.predicate = predicate; 27 | this.invert = invert; 28 | } 29 | 30 | @Override 31 | public Integrator, Map.Entry> integrator() { 32 | return Integrator.of((_, element, downstream) -> { 33 | var testResult = predicate.test(element.getKey(), element.getValue()); 34 | if (testResult ^ invert) { 35 | return downstream.push(element); 36 | } 37 | return !downstream.isRejecting(); 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/FilteringGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.BiPredicate; 5 | import java.util.function.Function; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Filters all the elements with some predicate or based on their equality to the specific value. 10 | * 11 | * @param element type 12 | * @param mapped element type 13 | * @author jhspetersson 14 | */ 15 | class FilteringGatherer implements Gatherer { 16 | private final Function mapper; 17 | private final U value; 18 | private final BiPredicate predicate; 19 | private final boolean invert; 20 | 21 | FilteringGatherer(Function mapper, U value, BiPredicate predicate) { 22 | this(mapper, value, predicate, false); 23 | } 24 | 25 | FilteringGatherer(Function mapper, U value, BiPredicate predicate, boolean invert) { 26 | Objects.requireNonNull(mapper, "mapper cannot be null"); 27 | Objects.requireNonNull(predicate, "predicate cannot be null"); 28 | 29 | this.mapper = mapper; 30 | this.value = value; 31 | this.predicate = predicate; 32 | this.invert = invert; 33 | } 34 | 35 | @Override 36 | public Integrator integrator() { 37 | return Integrator.of((_, element, downstream) -> { 38 | var mappedValue = mapper.apply(element); 39 | var testResult = predicate.test(mappedValue, value); 40 | if (testResult ^ invert) { 41 | return downstream.push(element); 42 | } 43 | return !downstream.isRejecting(); 44 | }); 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/FilteringWithIndexGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.BiPredicate; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Filters elements based on their index and a predicate. 10 | * 11 | * @param element type 12 | * @author jhspetersson 13 | */ 14 | class FilteringWithIndexGatherer implements Gatherer { 15 | private final BiPredicate predicate; 16 | private final long startIndex; 17 | private final boolean invert; 18 | 19 | FilteringWithIndexGatherer(BiPredicate predicate, long startIndex) { 20 | this(predicate, startIndex, false); 21 | } 22 | 23 | FilteringWithIndexGatherer(BiPredicate predicate, long startIndex, boolean invert) { 24 | Objects.requireNonNull(predicate, "predicate cannot be null"); 25 | 26 | this.predicate = predicate; 27 | this.startIndex = startIndex; 28 | this.invert = invert; 29 | } 30 | 31 | @Override 32 | public Supplier initializer() { 33 | return () -> new long[] { startIndex }; 34 | } 35 | 36 | @Override 37 | public Integrator integrator() { 38 | return Integrator.of((state, element, downstream) -> { 39 | var currentIndex = state[0]++; 40 | var testResult = predicate.test(currentIndex, element); 41 | if (testResult ^ invert) { 42 | return downstream.push(element); 43 | } 44 | return !downstream.isRejecting(); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/FixedSizeDeque.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.Collection; 5 | import java.util.Deque; 6 | import java.util.Iterator; 7 | 8 | /** 9 | * A fixed-size deque implementation that automatically removes the oldest element 10 | * when the maximum size is reached and a new element is added. 11 | * 12 | * @param the type of elements held in this deque 13 | * @author jhspetersson 14 | */ 15 | public class FixedSizeDeque implements Deque { 16 | private final Deque delegate; 17 | private final int maxSize; 18 | 19 | /** 20 | * Creates a new fixed-size deque with the specified maximum size. 21 | * 22 | * @param maxSize the maximum size of the deque 23 | */ 24 | public FixedSizeDeque(int maxSize) { 25 | if (maxSize <= 0) { 26 | throw new IllegalArgumentException("maxSize must be positive"); 27 | } 28 | 29 | this.maxSize = maxSize; 30 | this.delegate = new ArrayDeque<>(maxSize); 31 | } 32 | 33 | @Override 34 | public boolean add(E e) { 35 | if (delegate.size() >= maxSize) { 36 | delegate.removeFirst(); 37 | } 38 | 39 | return delegate.add(e); 40 | } 41 | 42 | @Override 43 | public void addFirst(E e) { 44 | if (delegate.size() >= maxSize) { 45 | delegate.removeLast(); 46 | } 47 | 48 | delegate.addFirst(e); 49 | } 50 | 51 | @Override 52 | public void addLast(E e) { 53 | if (delegate.size() >= maxSize) { 54 | delegate.removeFirst(); 55 | } 56 | 57 | delegate.addLast(e); 58 | } 59 | 60 | @Override 61 | public boolean offer(E e) { 62 | if (delegate.size() >= maxSize) { 63 | delegate.removeFirst(); 64 | } 65 | 66 | return delegate.offer(e); 67 | } 68 | 69 | @Override 70 | public boolean offerFirst(E e) { 71 | if (delegate.size() >= maxSize) { 72 | delegate.removeLast(); 73 | } 74 | 75 | return delegate.offerFirst(e); 76 | } 77 | 78 | @Override 79 | public boolean offerLast(E e) { 80 | if (delegate.size() >= maxSize) { 81 | delegate.removeFirst(); 82 | } 83 | 84 | return delegate.offerLast(e); 85 | } 86 | 87 | @Override 88 | public E removeFirst() { 89 | return delegate.removeFirst(); 90 | } 91 | 92 | @Override 93 | public E removeLast() { 94 | return delegate.removeLast(); 95 | } 96 | 97 | @Override 98 | public E pollFirst() { 99 | return delegate.pollFirst(); 100 | } 101 | 102 | @Override 103 | public E pollLast() { 104 | return delegate.pollLast(); 105 | } 106 | 107 | @Override 108 | public E getFirst() { 109 | return delegate.getFirst(); 110 | } 111 | 112 | @Override 113 | public E getLast() { 114 | return delegate.getLast(); 115 | } 116 | 117 | @Override 118 | public E peekFirst() { 119 | return delegate.peekFirst(); 120 | } 121 | 122 | @Override 123 | public E peekLast() { 124 | return delegate.peekLast(); 125 | } 126 | 127 | @Override 128 | public boolean removeFirstOccurrence(Object o) { 129 | return delegate.removeFirstOccurrence(o); 130 | } 131 | 132 | @Override 133 | public boolean removeLastOccurrence(Object o) { 134 | return delegate.removeLastOccurrence(o); 135 | } 136 | 137 | @Override 138 | public boolean addAll(Collection c) { 139 | boolean modified = false; 140 | for (E e : c) { 141 | if (add(e)) { 142 | modified = true; 143 | } 144 | } 145 | return modified; 146 | } 147 | 148 | @Override 149 | public void push(E e) { 150 | addFirst(e); 151 | } 152 | 153 | @Override 154 | public E pop() { 155 | return removeFirst(); 156 | } 157 | 158 | @Override 159 | public boolean remove(Object o) { 160 | return delegate.remove(o); 161 | } 162 | 163 | @Override 164 | public boolean contains(Object o) { 165 | return delegate.contains(o); 166 | } 167 | 168 | @Override 169 | public int size() { 170 | return delegate.size(); 171 | } 172 | 173 | @Override 174 | public Iterator iterator() { 175 | return delegate.iterator(); 176 | } 177 | 178 | @Override 179 | public Iterator descendingIterator() { 180 | return delegate.descendingIterator(); 181 | } 182 | 183 | @Override 184 | public boolean isEmpty() { 185 | return delegate.isEmpty(); 186 | } 187 | 188 | @Override 189 | public Object[] toArray() { 190 | return delegate.toArray(); 191 | } 192 | 193 | @Override 194 | public T[] toArray(T[] a) { 195 | return delegate.toArray(a); 196 | } 197 | 198 | @Override 199 | public boolean containsAll(Collection c) { 200 | return delegate.containsAll(c); 201 | } 202 | 203 | @Override 204 | public boolean removeAll(Collection c) { 205 | return delegate.removeAll(c); 206 | } 207 | 208 | @Override 209 | public boolean retainAll(Collection c) { 210 | return delegate.retainAll(c); 211 | } 212 | 213 | @Override 214 | public void clear() { 215 | delegate.clear(); 216 | } 217 | 218 | @Override 219 | public E remove() { 220 | return delegate.remove(); 221 | } 222 | 223 | @Override 224 | public E poll() { 225 | return delegate.poll(); 226 | } 227 | 228 | @Override 229 | public E element() { 230 | return delegate.element(); 231 | } 232 | 233 | @Override 234 | public E peek() { 235 | return delegate.peek(); 236 | } 237 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/FlatMapGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | import java.util.function.Predicate; 6 | import java.util.stream.Gatherer; 7 | import java.util.stream.Stream; 8 | 9 | /** 10 | * Optionally flattens elements mapped to streams depending on the supplied predicate. 11 | * 12 | * @param element type 13 | * @author jhspetersson 14 | */ 15 | class FlatMapGatherer implements Gatherer { 16 | private final Function> mapper; 17 | private final Predicate predicate; 18 | 19 | FlatMapGatherer(Function> mapper, Predicate predicate) { 20 | Objects.requireNonNull(mapper, "mapper cannot be null"); 21 | Objects.requireNonNull(predicate, "predicate cannot be null"); 22 | 23 | this.mapper = mapper; 24 | this.predicate = predicate; 25 | } 26 | 27 | @Override 28 | public Integrator integrator() { 29 | return Integrator.of((_, element, downstream) -> { 30 | if (predicate.test(element)) { 31 | var stream = mapper.apply(element); 32 | for (var it = stream.iterator(); it.hasNext(); ) { 33 | var elem = it.next(); 34 | var res = downstream.push(elem); 35 | if (!res) { 36 | return false; 37 | } 38 | } 39 | return !downstream.isRejecting(); 40 | } else { 41 | return downstream.push(element); 42 | } 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/IdentityGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.stream.Gatherer; 4 | 5 | /** 6 | * A gatherer that passes elements through unchanged. 7 | * This is an identity operation that doesn't modify the stream elements. 8 | * 9 | * @param element type 10 | * @author jhspetersson 11 | */ 12 | public class IdentityGatherer implements Gatherer { 13 | @Override 14 | public Integrator integrator() { 15 | return Integrator.of((_, element, downstream) -> downstream.push(element)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/IncreasingDecreasingChunksGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.function.BiConsumer; 8 | import java.util.function.Predicate; 9 | import java.util.function.Supplier; 10 | import java.util.stream.Gatherer; 11 | 12 | /** 13 | * Returns lists ("chunks") of elements, where each next element is less/greater and, optionally equal than the previous one. 14 | * Comparison is done with the supplied comparator. 15 | * 16 | * @param element type 17 | * @author jhspetersson 18 | */ 19 | class IncreasingDecreasingChunksGatherer implements Gatherer, List> { 20 | private final Comparator comparator; 21 | private final Predicate predicate; 22 | private T value; 23 | 24 | IncreasingDecreasingChunksGatherer(Comparator comparator, Predicate predicate) { 25 | Objects.requireNonNull(comparator, "comparator cannot be null"); 26 | Objects.requireNonNull(predicate, "predicate cannot be null"); 27 | 28 | this.comparator = comparator; 29 | this.predicate = predicate; 30 | } 31 | 32 | @Override 33 | public Supplier> initializer() { 34 | return ArrayList::new; 35 | } 36 | 37 | @Override 38 | public Integrator, T, List> integrator() { 39 | return Integrator.of((state, element, downstream) -> { 40 | if (value == null) { 41 | value = element; 42 | state.add(element); 43 | } else { 44 | var result = comparator.compare(value, element); 45 | value = element; 46 | if (predicate.test(result)) { 47 | state.add(element); 48 | } else { 49 | var chunk = List.copyOf(state); 50 | state.clear(); 51 | state.add(element); 52 | return downstream.push(chunk); 53 | } 54 | } 55 | 56 | return !downstream.isRejecting(); 57 | }); 58 | } 59 | 60 | @Override 61 | public BiConsumer, Downstream>> finisher() { 62 | return (state, downstream) -> { 63 | if (!state.isEmpty()) { 64 | var chunk = List.copyOf(state); 65 | downstream.push(chunk); 66 | } 67 | }; 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/IncreasingDecreasingGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Comparator; 4 | import java.util.Objects; 5 | import java.util.function.Predicate; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Filters all the elements going in some order specified by the supplied comparator. 10 | * 11 | * @param element type 12 | * @author jhspetersson 13 | */ 14 | class IncreasingDecreasingGatherer implements Gatherer { 15 | private final Comparator comparator; 16 | private final Predicate predicate; 17 | private T value; 18 | 19 | IncreasingDecreasingGatherer(Comparator comparator, Predicate predicate) { 20 | Objects.requireNonNull(comparator, "comparator cannot be null"); 21 | Objects.requireNonNull(predicate, "predicate cannot be null"); 22 | 23 | this.comparator = comparator; 24 | this.predicate = predicate; 25 | } 26 | 27 | @Override 28 | public Integrator integrator() { 29 | return Integrator.of((_, element, downstream) -> { 30 | if (value == null) { 31 | value = element; 32 | return downstream.push(element); 33 | } 34 | 35 | var result = comparator.compare(value, element); 36 | if (predicate.test(result)) { 37 | value = element; 38 | return downstream.push(element); 39 | } 40 | 41 | return !downstream.isRejecting(); 42 | }); 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/IntoListGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.function.BiConsumer; 7 | import java.util.function.BinaryOperator; 8 | import java.util.function.Consumer; 9 | import java.util.function.Supplier; 10 | import java.util.stream.Gatherer; 11 | 12 | /** 13 | * Collects the entire stream into a list, applies some consumer to this list and then passes all the elements down the stream. 14 | * 15 | * @param element type 16 | * @author jhspetersson 17 | */ 18 | class IntoListGatherer implements Gatherer, T> { 19 | private final Consumer> consumer; 20 | 21 | IntoListGatherer(Consumer> consumer) { 22 | Objects.requireNonNull(consumer, "consumer cannot be null"); 23 | 24 | this.consumer = consumer; 25 | } 26 | 27 | @Override 28 | public Supplier> initializer() { 29 | return ArrayList::new; 30 | } 31 | 32 | @Override 33 | public Integrator, T, T> integrator() { 34 | return Integrator.ofGreedy((state, element, downstream) -> { 35 | state.add(element); 36 | return !downstream.isRejecting(); 37 | }); 38 | } 39 | 40 | @Override 41 | public BinaryOperator> combiner() { 42 | return (state, state2) -> { 43 | state.addAll(state2); 44 | return state; 45 | }; 46 | } 47 | 48 | @Override 49 | public BiConsumer, Downstream> finisher() { 50 | return (state, downstream) -> { 51 | consumer.accept(state); 52 | for (var element : state) { 53 | if (!downstream.push(element)) { 54 | break; 55 | } 56 | } 57 | }; 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/LastingGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.Deque; 5 | import java.util.HashSet; 6 | import java.util.Iterator; 7 | import java.util.Set; 8 | import java.util.function.BiConsumer; 9 | import java.util.function.Supplier; 10 | import java.util.stream.Gatherer; 11 | 12 | /** 13 | * Returns last n elements. 14 | * 15 | * @param element type 16 | * @author jhspetersson 17 | */ 18 | class LastingGatherer implements Gatherer, T> { 19 | private final long n; 20 | private final boolean unique; 21 | 22 | LastingGatherer(long n) { 23 | this(n, false); 24 | } 25 | 26 | LastingGatherer(long n, boolean unique) { 27 | if (n < 0) { 28 | throw new IllegalArgumentException("n must be a non-negative number"); 29 | } 30 | 31 | this.n = n; 32 | this.unique = unique; 33 | } 34 | 35 | @Override 36 | public Supplier> initializer() { 37 | return () -> new State<>(new ArrayDeque<>(), new HashSet<>()); 38 | } 39 | 40 | @Override 41 | public Integrator, T, T> integrator() { 42 | if (n == 0) { 43 | return Integrator.of((_, _, _) -> false); 44 | } 45 | 46 | return Integrator.ofGreedy((state, element, downstream) -> { 47 | if (unique && state.containsElement(element)) { 48 | return true; 49 | } 50 | 51 | if (state.sizeEquals(n)) { 52 | state.removeElement(element); 53 | } 54 | 55 | state.addElement(element); 56 | 57 | return !downstream.isRejecting(); 58 | }); 59 | } 60 | 61 | @Override 62 | public BiConsumer, Downstream> finisher() { 63 | return (state, downstream) -> { 64 | for (var element : state) { 65 | if (!downstream.push(element)) { 66 | break; 67 | } 68 | } 69 | }; 70 | } 71 | 72 | static class State implements Iterable { 73 | Deque deque; 74 | Set elements; 75 | 76 | State(Deque deque, Set elements) { 77 | this.deque = deque; 78 | this.elements = elements; 79 | } 80 | 81 | boolean sizeEquals(long size) { 82 | return deque.size() == size; 83 | } 84 | 85 | boolean containsElement(T element) { 86 | return elements.contains(element); 87 | } 88 | 89 | void addElement(T element) { 90 | deque.add(element); 91 | elements.add(element); 92 | } 93 | 94 | void removeElement(T element) { 95 | var removedElement = deque.removeFirst(); 96 | elements.remove(removedElement); 97 | } 98 | 99 | @Override 100 | public Iterator iterator() { 101 | return deque.iterator(); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/MapWhileUntilGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | import java.util.function.Predicate; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | /** 10 | * Maps the elements until the condition holds (or not, if is inverted) 11 | * 12 | * @param element type 13 | * @author jhspetersson 14 | */ 15 | class MapWhileUntilGatherer implements Gatherer { 16 | private final Function mapper; 17 | private final Predicate whilePredicate; 18 | private final Predicate untilPredicate; 19 | 20 | MapWhileUntilGatherer(Function mapper, Predicate whilePredicate) { 21 | this(mapper, whilePredicate, null); 22 | } 23 | 24 | MapWhileUntilGatherer(Function mapper, Predicate whilePredicate, Predicate untilPredicate) { 25 | Objects.requireNonNull(mapper, "mapper cannot be null"); 26 | 27 | this.mapper = mapper; 28 | this.whilePredicate = whilePredicate; 29 | this.untilPredicate = untilPredicate; 30 | } 31 | 32 | @Override 33 | public Supplier initializer() { 34 | return () -> new boolean[] { true, false }; 35 | } 36 | 37 | @Override 38 | public Integrator integrator() { 39 | return Integrator.of((state, element, downstream) -> { 40 | if (state[0] && whilePredicate != null) { 41 | var testWhile = whilePredicate.test(element); 42 | if (!testWhile) { 43 | state[0] = false; 44 | } 45 | } 46 | 47 | if (!state[1] && untilPredicate != null) { 48 | var testUntil = untilPredicate.test(element); 49 | if (testUntil) { 50 | state[1] = true; 51 | } 52 | } 53 | 54 | var value = state[0] && !state[1] 55 | ? mapper.apply(element) 56 | : element; 57 | return downstream.push(value); 58 | }); 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/MappingGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Returns elements with optional mapping applied to the specified number of them. 10 | * 11 | * @param element type 12 | * @author jhspetersson 13 | */ 14 | class MappingGatherer implements Gatherer { 15 | private final long skipN; 16 | private final long mapN; 17 | private final Function mapper; 18 | 19 | MappingGatherer(long skipN, long mapN, Function mapper) { 20 | Objects.requireNonNull(mapper, "mapper cannot be null"); 21 | 22 | this.skipN = skipN; 23 | this.mapN = mapN; 24 | this.mapper = mapper; 25 | } 26 | 27 | @Override 28 | public Supplier initializer() { 29 | return () -> new Long[] { 0L, 0L }; 30 | } 31 | 32 | @Override 33 | public Integrator integrator() { 34 | return Integrator.of((state, element, downstream) -> { 35 | if (skipN > 0 && state[0] < skipN) { 36 | state[0] += 1; 37 | return downstream.push(element); 38 | } else if (mapN < 0 || state[1] < mapN) { 39 | state[1] += 1; 40 | var mappedValue = mapper.apply(element); 41 | return downstream.push(mappedValue); 42 | } else { 43 | return downstream.push(element); 44 | } 45 | }); 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/MinMaxGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Comparator; 4 | import java.util.Objects; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.Function; 7 | import java.util.function.Predicate; 8 | import java.util.function.Supplier; 9 | import java.util.stream.Gatherer; 10 | 11 | /** 12 | * Outputs the greatest or the smallest element in the stream, comparing is done after mapping function applied. 13 | * 14 | * @param element type 15 | * @param mapped element type 16 | * @author jhspetersson 17 | */ 18 | class MinMaxGatherer implements Gatherer, T> { 19 | private final Function mapper; 20 | private final Comparator comparator; 21 | private final Predicate predicate; 22 | 23 | MinMaxGatherer(Function mapper, Comparator comparator, Predicate predicate) { 24 | Objects.requireNonNull(mapper, "mapper cannot be null"); 25 | Objects.requireNonNull(comparator, "comparator cannot be null"); 26 | Objects.requireNonNull(predicate, "predicate cannot be null"); 27 | 28 | this.mapper = mapper; 29 | this.comparator = comparator; 30 | this.predicate = predicate; 31 | } 32 | 33 | @Override 34 | public Supplier> initializer() { 35 | return State::new; 36 | } 37 | 38 | @Override 39 | public Integrator, T, T> integrator() { 40 | return Integrator.ofGreedy((state, element, downstream) -> { 41 | var mappedValue = mapper.apply(element); 42 | var stateMappedElement = state.mappedElement; 43 | if (stateMappedElement != null) { 44 | var result = comparator.compare(mappedValue, stateMappedElement); 45 | if (predicate.test(result)) { 46 | state.element = element; 47 | state.mappedElement = mappedValue; 48 | } 49 | } else { 50 | state.element = element; 51 | state.mappedElement = mappedValue; 52 | } 53 | return !downstream.isRejecting(); 54 | }); 55 | } 56 | 57 | @Override 58 | public BiConsumer, Downstream> finisher() { 59 | return (state, downstream) -> { 60 | var element = state.element; 61 | if (element != null) { 62 | downstream.push(element); 63 | } 64 | }; 65 | } 66 | 67 | static class State { 68 | T element; 69 | U mappedElement; 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/NCopiesGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.stream.Gatherer; 4 | 5 | /** 6 | * Returns n copies of every element. 7 | * 8 | * @param element type 9 | * @author jhspetersson 10 | */ 11 | class NCopiesGatherer implements Gatherer { 12 | private final long n; 13 | 14 | /** 15 | * Creates a new instance of {@link NCopiesGatherer}. 16 | * 17 | * @param n the number of copies to return 18 | * @throws IllegalArgumentException if n is negative 19 | */ 20 | NCopiesGatherer(long n) { 21 | if (n < 0) { 22 | throw new IllegalArgumentException("n must be a non-negative number"); 23 | } 24 | 25 | this.n = n; 26 | } 27 | 28 | @Override 29 | public Integrator integrator() { 30 | return Integrator.of((_, element, downstream) -> { 31 | for (var i = 1L; i <= n; i++) { 32 | if (!downstream.push(element)) { 33 | return false; 34 | } 35 | } 36 | return !downstream.isRejecting(); 37 | }); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/NthGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.function.Supplier; 4 | import java.util.stream.Gatherer; 5 | 6 | /** 7 | * Returns every nth element from the stream. 8 | * 9 | * @param element type 10 | * @author jhspetersson 11 | */ 12 | class NthGatherer implements Gatherer { 13 | private final int n; 14 | 15 | NthGatherer(int n) { 16 | if (n <= 0) { 17 | throw new IllegalArgumentException("n must be a positive number"); 18 | } 19 | 20 | this.n = n; 21 | } 22 | 23 | @Override 24 | public Supplier initializer() { 25 | return () -> new int[1]; 26 | } 27 | 28 | @Override 29 | public Integrator integrator() { 30 | return Integrator.of((state, element, downstream) -> { 31 | if (++state[0] % n == 0) { 32 | return downstream.push(element); 33 | } 34 | return !downstream.isRejecting(); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/PeekWithIndexGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.BiConsumer; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Peeks at each element along with its index, but passes the original element downstream. 10 | * 11 | * @param element type 12 | * @author jhspetersson 13 | */ 14 | class PeekWithIndexGatherer implements Gatherer { 15 | private final BiConsumer consumer; 16 | private final long startIndex; 17 | 18 | PeekWithIndexGatherer(BiConsumer consumer, long startIndex) { 19 | Objects.requireNonNull(consumer, "consumer cannot be null"); 20 | 21 | this.consumer = consumer; 22 | this.startIndex = startIndex; 23 | } 24 | 25 | @Override 26 | public Supplier initializer() { 27 | return () -> new long[] { startIndex }; 28 | } 29 | 30 | @Override 31 | public Integrator integrator() { 32 | return Integrator.of((state, element, downstream) -> { 33 | consumer.accept(state[0]++, element); 34 | return downstream.push(element); 35 | }); 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/RemoveDuplicatesGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | import java.util.stream.Gatherer; 9 | 10 | /** 11 | * Removes consecutive duplicates from a stream based on a mapping function. 12 | * Only adjacent elements that have equal mapped values will be considered duplicates. 13 | * If no mapping function is provided, elements are compared directly. 14 | * 15 | * @param element type 16 | * @param mapped element type 17 | * @author jhspetersson 18 | */ 19 | public class RemoveDuplicatesGatherer implements Gatherer, T> { 20 | private final Function mapper; 21 | 22 | RemoveDuplicatesGatherer(Function mapper) { 23 | Objects.requireNonNull(mapper, "mapper cannot be null"); 24 | this.mapper = mapper; 25 | } 26 | 27 | @Override 28 | public Supplier> initializer() { 29 | return ArrayList::new; 30 | } 31 | 32 | @Override 33 | public Integrator, T, T> integrator() { 34 | return Integrator.of((state, element, downstream) -> { 35 | var mappedValue = mapper.apply(element); 36 | if (!state.isEmpty() && Objects.equals(state.getFirst(), mappedValue)) { 37 | return !downstream.isRejecting(); 38 | } else { 39 | state.clear(); 40 | state.add(mappedValue); 41 | return downstream.push(element); 42 | } 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/RepeatGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | /** 10 | * Collects the whole stream and repeats it n times. 11 | * 12 | * @param element type 13 | * @author jhspetersson 14 | */ 15 | public class RepeatGatherer implements Gatherer, T> { 16 | private final long n; 17 | 18 | /** 19 | * Constructs a new RepeatGatherer instance. 20 | * 21 | * @param n how many times to repeat the stream, value equal to zero effectively empties the stream 22 | * @throws IllegalArgumentException if n is negative 23 | */ 24 | public RepeatGatherer(long n) { 25 | if (n < 0) { 26 | throw new IllegalArgumentException("n must be a non-negative number"); 27 | } 28 | this.n = n; 29 | } 30 | 31 | @Override 32 | public Supplier> initializer() { 33 | return ArrayList::new; 34 | } 35 | 36 | @Override 37 | public Integrator, T, T> integrator() { 38 | return Integrator.ofGreedy((state, element, downstream) -> { 39 | state.add(element); 40 | return !downstream.isRejecting(); 41 | }); 42 | } 43 | 44 | @Override 45 | public BiConsumer, Gatherer.Downstream> finisher() { 46 | return (state, downstream) -> { 47 | for (var i = 0L; i < n; i++) { 48 | for (T element : state) { 49 | if (!downstream.push(element)) { 50 | return; 51 | } 52 | } 53 | } 54 | }; 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/RotateLeftGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.Supplier; 7 | import java.util.stream.Gatherer; 8 | 9 | /** 10 | * Optimized gatherer for left rotation of elements in a stream. 11 | * It saves the first N elements (where N is the rotation distance) and then 12 | * outputs the rest of the elements first, followed by the saved elements. 13 | * 14 | * @param element type 15 | * @author jhspetersson 16 | */ 17 | class RotateLeftGatherer implements Gatherer, T> { 18 | private final int distance; 19 | 20 | RotateLeftGatherer(int distance) { 21 | if (distance <= 0) { 22 | throw new IllegalArgumentException("distance must be positive"); 23 | } 24 | this.distance = distance; 25 | } 26 | 27 | @Override 28 | public Supplier> initializer() { 29 | return ArrayList::new; 30 | } 31 | 32 | @Override 33 | public Integrator, T, T> integrator() { 34 | return (state, element, downstream) -> { 35 | if (state.size() < distance) { 36 | state.add(element); 37 | } else { 38 | if (!downstream.push(element)) { 39 | return false; 40 | } 41 | } 42 | return !downstream.isRejecting(); 43 | }; 44 | } 45 | 46 | @Override 47 | public BiConsumer, Downstream> finisher() { 48 | return (state, downstream) -> { 49 | for (var element : state) { 50 | if (!downstream.push(element)) { 51 | break; 52 | } 53 | } 54 | }; 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/SamplingGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.concurrent.ThreadLocalRandom; 6 | import java.util.function.BiConsumer; 7 | import java.util.function.BinaryOperator; 8 | import java.util.function.Supplier; 9 | import java.util.stream.Gatherer; 10 | 11 | /** 12 | * Returns sample of the specified size from the stream. 13 | * 14 | * @param element type 15 | * @author jhspetersson 16 | */ 17 | class SamplingGatherer implements Gatherer, T> { 18 | private final int n; 19 | private final int maxSpan; 20 | 21 | SamplingGatherer(int n, int maxSpan) { 22 | if (n < 0) { 23 | throw new IllegalArgumentException("n must be a positive number"); 24 | } 25 | 26 | if (maxSpan <= 0) { 27 | throw new IllegalArgumentException("n must be a positive number"); 28 | } 29 | 30 | if (n >= maxSpan) { 31 | throw new IllegalArgumentException("n must be less than minSpan"); 32 | } 33 | 34 | this.n = n; 35 | this.maxSpan = maxSpan; 36 | } 37 | 38 | @Override 39 | public Supplier> initializer() { 40 | return () -> new State<>(new ArrayList<>(), 0); 41 | } 42 | 43 | @Override 44 | public Integrator, T, T> integrator() { 45 | var random = ThreadLocalRandom.current(); 46 | return Integrator.of((state, element, downstream) -> { 47 | if (state.list.size() < n) { 48 | state.list.add(element); 49 | } else if (state.counter <= maxSpan - n) { 50 | if (random.nextDouble(1.0, 2.0) <= 1.1) { 51 | var remove = random.nextInt(state.list.size()); 52 | state.list.remove(remove); 53 | state.list.add(element); 54 | } 55 | state.counter++; 56 | } 57 | return !downstream.isRejecting(); 58 | }); 59 | } 60 | 61 | @Override 62 | public BinaryOperator> combiner() { 63 | return (state, state2) -> { 64 | state.list.addAll(state2.list); 65 | return state; 66 | }; 67 | } 68 | 69 | @Override 70 | public BiConsumer, Downstream> finisher() { 71 | return (state, downstream) -> { 72 | for (var element : state.list) { 73 | if (!downstream.push(element)) { 74 | break; 75 | } 76 | } 77 | }; 78 | } 79 | 80 | static class State { 81 | List list; 82 | int counter; 83 | 84 | public State(List list, int counter) { 85 | this.list = list; 86 | this.counter = counter; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/WindowFixedWithIndexGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.function.BiFunction; 7 | import java.util.function.Supplier; 8 | import java.util.stream.Gatherer; 9 | 10 | /** 11 | * Returns fixed-size non-overlapping windows of elements along with their indices. 12 | * Each window contains a fixed number of elements and is emitted as a list. 13 | * Windows are not sliding but go one after another with a step equal to the window's size. 14 | * 15 | * @param element type 16 | * @param result type 17 | * @author jhspetersson 18 | */ 19 | class WindowFixedWithIndexGatherer implements Gatherer, R> { 20 | private final int windowSize; 21 | private final BiFunction, ? extends R> mapper; 22 | private final long startIndex; 23 | 24 | /** 25 | * Creates a new WindowFixedWithIndexGatherer with the specified window size and mapper function. 26 | * 27 | * @param windowSize the size of each window 28 | * @param mapper the function to map each window with its index to a result 29 | * @param startIndex the starting index 30 | */ 31 | WindowFixedWithIndexGatherer(int windowSize, BiFunction, ? extends R> mapper, long startIndex) { 32 | if (windowSize <= 0) { 33 | throw new IllegalArgumentException("windowSize must be positive"); 34 | } 35 | Objects.requireNonNull(mapper, "mapper cannot be null"); 36 | 37 | this.windowSize = windowSize; 38 | this.mapper = mapper; 39 | this.startIndex = startIndex; 40 | } 41 | 42 | @Override 43 | public Supplier> initializer() { 44 | return () -> new State<>(windowSize, startIndex); 45 | } 46 | 47 | @Override 48 | public Integrator, T, R> integrator() { 49 | return Integrator.of((state, element, downstream) -> { 50 | state.window.add(element); 51 | 52 | if (state.window.size() == windowSize) { 53 | var windowCopy = new ArrayList<>(state.window); 54 | var result = mapper.apply(state.index++, windowCopy); 55 | state.window.clear(); 56 | 57 | return downstream.push(result); 58 | } 59 | 60 | return !downstream.isRejecting(); 61 | }); 62 | } 63 | 64 | /** 65 | * State class for the WindowFixedWithIndexGatherer. 66 | * 67 | * @param element type 68 | */ 69 | static class State { 70 | private final List window; 71 | private long index; 72 | 73 | State(int windowSize, long startIndex) { 74 | this.window = new ArrayList<>(windowSize); 75 | this.index = startIndex; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/WindowSlidingWithIndexGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.function.BiFunction; 7 | import java.util.function.Supplier; 8 | import java.util.stream.Gatherer; 9 | 10 | /** 11 | * Returns fixed-size windows of elements along with their indices. 12 | * Each window contains a fixed number of elements and is emitted as a list. 13 | * 14 | * @param element type 15 | * @param result type 16 | * @author jhspetersson 17 | */ 18 | class WindowSlidingWithIndexGatherer implements Gatherer, R> { 19 | private final int windowSize; 20 | private final BiFunction, ? extends R> mapper; 21 | private final long startIndex; 22 | 23 | /** 24 | * Creates a new WindowSlidingWithIndexGatherer with the specified window size and mapper function. 25 | * 26 | * @param windowSize the size of each window 27 | * @param mapper the function to map each window with its index to a result 28 | * @param startIndex the starting index 29 | */ 30 | WindowSlidingWithIndexGatherer(int windowSize, BiFunction, ? extends R> mapper, long startIndex) { 31 | if (windowSize <= 0) { 32 | throw new IllegalArgumentException("windowSize must be positive"); 33 | } 34 | Objects.requireNonNull(mapper, "mapper cannot be null"); 35 | 36 | this.windowSize = windowSize; 37 | this.mapper = mapper; 38 | this.startIndex = startIndex; 39 | } 40 | 41 | @Override 42 | public Supplier> initializer() { 43 | return () -> new State<>(windowSize, startIndex); 44 | } 45 | 46 | @Override 47 | public Integrator, T, R> integrator() { 48 | return Integrator.of((state, element, downstream) -> { 49 | state.window.add(element); 50 | 51 | if (!state.windowFilled && state.window.size() == windowSize) { 52 | state.windowFilled = true; 53 | } 54 | 55 | if (state.windowFilled) { 56 | var windowCopy = new ArrayList<>(state.window); 57 | var result = mapper.apply(state.index++, windowCopy); 58 | 59 | return downstream.push(result); 60 | } 61 | 62 | return !downstream.isRejecting(); 63 | }); 64 | } 65 | 66 | /** 67 | * State class for the WindowSlidingWithIndexGatherer. 68 | * 69 | * @param element type 70 | */ 71 | static class State { 72 | private final FixedSizeDeque window; 73 | private long index; 74 | private boolean windowFilled; 75 | 76 | State(int windowSize, long startIndex) { 77 | this.window = new FixedSizeDeque<>(windowSize); 78 | this.index = startIndex; 79 | this.windowFilled = false; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/ZipGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Iterator; 4 | import java.util.Objects; 5 | import java.util.function.BiFunction; 6 | import java.util.stream.Gatherer; 7 | import java.util.stream.Stream; 8 | 9 | /** 10 | * Returns elements mapped ("zipped") with the values from some other stream, iterable or iterator. 11 | * 12 | * @param element type 13 | * @param element type of the supplied stream or iterable 14 | * @param mapped element type 15 | * @author jhspetersson 16 | */ 17 | class ZipGatherer implements Gatherer { 18 | private final Iterator iterator; 19 | private final BiFunction mapper; 20 | 21 | ZipGatherer(Iterable input, BiFunction mapper) { 22 | this(input.iterator(), mapper); 23 | } 24 | 25 | ZipGatherer(Stream input, BiFunction mapper) { 26 | this(input.iterator(), mapper); 27 | } 28 | 29 | ZipGatherer(Iterator iterator, BiFunction mapper) { 30 | Objects.requireNonNull(iterator, "iterator cannot be null"); 31 | Objects.requireNonNull(mapper, "mapper cannot be null"); 32 | 33 | this.iterator = iterator; 34 | this.mapper = mapper; 35 | } 36 | 37 | @Override 38 | public Integrator integrator() { 39 | return Integrator.ofGreedy((_, element, downstream) -> { 40 | if (iterator.hasNext()) { 41 | var mappedValue = mapper.apply(element, iterator.next()); 42 | return downstream.push(mappedValue); 43 | } 44 | return false; 45 | }); 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/ZipWithIndexGatherer.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.Objects; 4 | import java.util.function.BiFunction; 5 | import java.util.function.Supplier; 6 | import java.util.stream.Gatherer; 7 | 8 | /** 9 | * Returns elements mapped ("zipped") with an increasing index. 10 | * 11 | * @param element type 12 | * @param mapped element type 13 | * @author jhspetersson 14 | */ 15 | class ZipWithIndexGatherer implements Gatherer { 16 | private final BiFunction mapper; 17 | private final long startIndex; 18 | 19 | ZipWithIndexGatherer(BiFunction mapper, long startIndex) { 20 | Objects.requireNonNull(mapper, "mapper cannot be null"); 21 | 22 | this.mapper = mapper; 23 | this.startIndex = startIndex; 24 | } 25 | 26 | @Override 27 | public Supplier initializer() { 28 | return () -> new long[] { startIndex }; 29 | } 30 | 31 | @Override 32 | public Integrator integrator() { 33 | return Integrator.of((state, element, downstream) -> { 34 | var mappedValue = mapper.apply(state[0]++, element); 35 | return downstream.push(mappedValue); 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/io/github/jhspetersson/packrat/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Packrat is a Java library that provides various Gatherer implementations for the Stream API. 3 | * Gatherers can enhance streams with custom intermediate operations. 4 | * 5 | * @author jhspetersson 6 | * @see https://github.com/jhspetersson/packrat 7 | */ 8 | package io.github.jhspetersson.packrat; -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/AtLeastTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | public class AtLeastTest { 12 | @Test 13 | public void atLeastTest() { 14 | var numbers = Stream.of(1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 8, 8, 8, 8, 9, 10); 15 | var result = numbers.gather(Packrat.atLeast(3)).toList(); 16 | assertEquals(List.of(3, 3, 3, 8, 8, 8, 8), result); 17 | } 18 | 19 | @Test 20 | public void atLeastUnorderedTest() { 21 | var numbers = Stream.of(1, 10, 3, 2, 3, 8, 4, 9, 5, 6, 7, 8, 3, 8, 8, 5); 22 | var result = numbers.gather(Packrat.atLeast(3)).toList(); 23 | assertEquals(List.of(3, 3, 3, 8, 8, 8, 8), result); 24 | } 25 | 26 | @Test 27 | public void atLeastEmptyTest() { 28 | var numbers = Stream.of(1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 8, 8, 8, 8, 9, 10); 29 | var result = numbers.gather(Packrat.atLeast(10)).toList(); 30 | assertTrue(result.isEmpty()); 31 | } 32 | 33 | @Test 34 | public void atLeastEmptySourceTest() { 35 | var numbers = Stream.of(); 36 | var result = numbers.gather(Packrat.atLeast(10)).toList(); 37 | assertTrue(result.isEmpty()); 38 | } 39 | 40 | @Test 41 | public void atLeastSingleValueTest() { 42 | var numbers = Stream.of(1, 1, 1); 43 | var result = numbers.gather(Packrat.atLeast(3)).toList(); 44 | assertEquals(List.of(1, 1, 1), result); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/AtMostTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | public class AtMostTest { 14 | @Test 15 | public void atMostTest() { 16 | var numbers = Stream.of(1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 8, 8, 8, 8, 9, 10); 17 | var result = numbers.gather(Packrat.atMost(2)).toList(); 18 | assertEquals(List.of(1, 2, 4, 5, 5, 6, 7, 9, 10), result); 19 | } 20 | 21 | @Test 22 | public void atMostUnorderedTest() { 23 | var numbers = Stream.of(1, 10, 3, 2, 3, 8, 4, 9, 5, 6, 7, 8, 3, 8, 8, 5); 24 | var result = numbers.gather(Packrat.atMost(2)).toList(); 25 | var expected = List.of(1, 10, 2, 4, 5, 5, 6, 7, 9); 26 | assertTrue(result.size() == expected.size() && result.containsAll(expected) && expected.containsAll(result)); 27 | } 28 | 29 | @Test 30 | public void atMostEmptyTest() { 31 | var numbers = Stream.of(1, 1, 1, 2, 2, 2, 3, 3, 3); 32 | var result = numbers.gather(Packrat.atMost(0)).toList(); 33 | assertTrue(result.isEmpty()); 34 | } 35 | 36 | @Test 37 | public void atMostEmptySourceTest() { 38 | var numbers = Stream.of(); 39 | var result = numbers.gather(Packrat.atMost(10)).toList(); 40 | assertTrue(result.isEmpty()); 41 | } 42 | 43 | @Test 44 | public void atMostSingleValueTest() { 45 | var numbers = Stream.of(1, 1, 1); 46 | var result = numbers.gather(Packrat.atMost(3)).toList(); 47 | assertEquals(List.of(1, 1, 1), result); 48 | } 49 | 50 | @Test 51 | public void atMostAllValuesTest() { 52 | var numbers = Stream.of(1, 2, 3, 4, 5); 53 | var result = numbers.gather(Packrat.atMost(1)).toList(); 54 | assertEquals(List.of(1, 2, 3, 4, 5), result); 55 | } 56 | 57 | @Test 58 | public void atMostByTest() { 59 | var strings = Stream.of("apple", "banana", "cherry", "date", "elderberry", "fig", "grape"); 60 | var result = strings.gather(Packrat.atMostBy(1, String::length)).collect(Collectors.toSet()); 61 | // "date", "fig", and "elderberry" have unique lengths (4, 3, and 10 characters) 62 | var expected = Set.of("date", "elderberry", "fig"); 63 | assertEquals(result, expected); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/BreakingTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Locale; 6 | import java.util.stream.Stream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertFalse; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | public class BreakingTest { 13 | private static final String TEST_STRING = "Test ✋\uD83C\uDFFF\uD83D\uDC22 日本語しゃべるか \uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\uD83D\uDC68\uD83C\uDFFC\u200D❤️\u200D\uD83D\uDC68\uD83C\uDFFE"; 14 | private static final String JAPANESE_TEXT = "こんにちは世界。日本語のテストです。"; 15 | private static final String FRENCH_TEXT = "Bonjour le monde! C'est un test en français."; 16 | 17 | @Test 18 | public void charsTest() { 19 | var chars = Stream.of(TEST_STRING).gather(Packrat.chars()).toList(); 20 | assertEquals(20, chars.size()); 21 | } 22 | 23 | @Test 24 | public void charsWithLocaleTest() { 25 | var charsUS = Stream.of(TEST_STRING).gather(Packrat.chars(Locale.US)).toList(); 26 | assertEquals(20, charsUS.size()); 27 | 28 | var charsJP = Stream.of(JAPANESE_TEXT).gather(Packrat.chars(Locale.JAPAN)).toList(); 29 | // Character breaking can vary by JVM implementation and locale 30 | assertTrue(charsJP.size() >= 17 && charsJP.size() <= 18, 31 | "Expected between 17 and 18 characters, but got " + charsJP.size()); 32 | 33 | var charsFR = Stream.of(FRENCH_TEXT).gather(Packrat.chars(Locale.FRANCE)).toList(); 34 | // Character breaking can vary by JVM implementation and locale 35 | assertTrue(charsFR.size() >= 44 && charsFR.size() <= 45, 36 | "Expected between 44 and 45 characters, but got " + charsFR.size()); 37 | } 38 | 39 | @Test 40 | public void wordsTest() { 41 | var words = Stream.of("Another test").gather(Packrat.words()).toList(); 42 | 43 | assertEquals(2, words.size()); 44 | } 45 | 46 | @Test 47 | public void wordsWithLocaleTest() { 48 | var wordsUS = Stream.of("Another test").gather(Packrat.words(Locale.US)).toList(); 49 | assertEquals(2, wordsUS.size()); 50 | 51 | var wordsJP = Stream.of(JAPANESE_TEXT).gather(Packrat.words(Locale.JAPAN)).toList(); 52 | // Japanese word breaking can vary by implementation, so we check it's at least breaking something 53 | assertFalse(wordsJP.isEmpty()); 54 | 55 | var wordsFR = Stream.of(FRENCH_TEXT).gather(Packrat.words(Locale.FRANCE)).toList(); 56 | // Word breaking can vary by JVM implementation and locale 57 | assertTrue(wordsFR.size() >= 9 && wordsFR.size() <= 10, 58 | "Expected between 9 and 10 words, but got " + wordsFR.size()); 59 | } 60 | 61 | @Test 62 | public void sentencesTest() { 63 | var sentences = Stream.of("What a sentence! Another test...\nI can't handle this, what is your proposition?").gather(Packrat.sentences()).toList(); 64 | 65 | assertEquals(3, sentences.size()); 66 | } 67 | 68 | @Test 69 | public void sentencesWithLocaleTest() { 70 | var sentencesUS = Stream.of("What a sentence! Another test...\nI can't handle this, what is your proposition?").gather(Packrat.sentences(Locale.US)).toList(); 71 | assertEquals(3, sentencesUS.size()); 72 | 73 | var sentencesJP = Stream.of(JAPANESE_TEXT).gather(Packrat.sentences(Locale.JAPAN)).toList(); 74 | assertEquals(2, sentencesJP.size()); 75 | 76 | var sentencesFR = Stream.of(FRENCH_TEXT).gather(Packrat.sentences(Locale.FRANCE)).toList(); 77 | assertEquals(2, sentencesFR.size()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/CollectingTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.Stream; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | public class CollectingTest { 12 | @Test 13 | void collectingTest() { 14 | var numbers = Stream.of(1, 2, 3, 4, 5); 15 | var result = numbers.gather(Packrat.asGatherer(Collectors.toList())).toList(); 16 | assertEquals(List.of(List.of(1, 2, 3, 4, 5)), result); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/DistinctByTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static io.github.jhspetersson.packrat.TestUtils.getEmployees; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | public class DistinctByTest { 9 | @Test 10 | public void distinctByTest() { 11 | var allEmployees = getEmployees().count(); 12 | assertEquals(5, allEmployees); 13 | var distinctAge = getEmployees().gather(Packrat.distinctBy(Employee::age)).count(); 14 | assertEquals(4, distinctAge); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/DropLastNTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.stream.IntStream; 8 | 9 | import static io.github.jhspetersson.packrat.TestUtils.isOrderedSequence; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | public class DropLastNTest { 14 | @Test 15 | public void dropLastTest() { 16 | var before = new ArrayList(); 17 | IntStream.range(0, 10).forEach(before::add); 18 | 19 | assertTrue(isOrderedSequence(before)); 20 | 21 | var after = before.stream().gather(Packrat.dropLast(3)).toList(); 22 | 23 | assertEquals(List.of(0, 1, 2, 3, 4, 5, 6), after); 24 | } 25 | 26 | @Test 27 | public void dropLastEmptyTest() { 28 | var before = new ArrayList(); 29 | 30 | var after = before.stream().gather(Packrat.dropLast(3)).toList(); 31 | 32 | assertEquals(List.of(), after); 33 | } 34 | 35 | @Test 36 | public void dropLastAllTest() { 37 | var before = new ArrayList(); 38 | IntStream.range(0, 5).forEach(before::add); 39 | 40 | var after = before.stream().gather(Packrat.dropLast(5)).toList(); 41 | 42 | assertEquals(List.of(), after); 43 | } 44 | 45 | @Test 46 | public void dropLastMoreThanSizeTest() { 47 | var before = new ArrayList(); 48 | IntStream.range(0, 5).forEach(before::add); 49 | 50 | var after = before.stream().gather(Packrat.dropLast(10)).toList(); 51 | 52 | assertEquals(List.of(), after); 53 | } 54 | 55 | @Test 56 | public void dropLastDefaultTest() { 57 | var before = new ArrayList(); 58 | IntStream.range(0, 5).forEach(before::add); 59 | 60 | var after = before.stream().gather(Packrat.dropLast()).toList(); 61 | 62 | assertEquals(List.of(0, 1, 2, 3), after); 63 | } 64 | 65 | @Test 66 | public void dropLastZeroTest() { 67 | var before = new ArrayList(); 68 | IntStream.range(0, 5).forEach(before::add); 69 | 70 | var after = before.stream().gather(Packrat.dropLast(0)).toList(); 71 | 72 | assertEquals(List.of(0, 1, 2, 3, 4), after); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/DropNthGathererTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | public class DropNthGathererTest { 12 | @Test 13 | public void dropEveryThirdElement() { 14 | var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 15 | 16 | var result = numbers.stream().gather(Packrat.dropNth(3)).toList(); 17 | 18 | assertEquals(List.of(1, 2, 4, 5, 7, 8, 10), result); 19 | } 20 | 21 | @Test 22 | public void dropEverySecondElement() { 23 | var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 24 | 25 | var result = numbers.stream().gather(Packrat.dropNth(2)).toList(); 26 | 27 | assertEquals(List.of(1, 3, 5, 7, 9), result); 28 | } 29 | 30 | @Test 31 | public void dropEveryElement() { 32 | var numbers = List.of(1, 2, 3, 4, 5); 33 | 34 | var result = numbers.stream().gather(Packrat.dropNth(1)).toList(); 35 | 36 | assertEquals(List.of(), result); 37 | } 38 | 39 | @Test 40 | public void invalidNValueThrowsException() { 41 | assertThrows(IllegalArgumentException.class, () -> Packrat.dropNth(0)); 42 | assertThrows(IllegalArgumentException.class, () -> Packrat.dropNth(-1)); 43 | } 44 | 45 | @Test 46 | public void emptyStreamReturnsEmptyList() { 47 | var result = IntStream.range(0, 0).boxed().gather(Packrat.dropNth(3)).toList(); 48 | 49 | assertEquals(List.of(), result); 50 | } 51 | } -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/Employee.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | record Employee(String name, int age) { 4 | } 5 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/EqualChunksGathererTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class EqualChunksGathererTest { 11 | @Test 12 | void equalChunksTest() { 13 | var numbers = Stream.of(1, 1, 2, 2, 2, 3, 4, 4, 5, 5, 5, 5, 6); 14 | var result = numbers.gather(Packrat.equalChunks()).toList(); 15 | 16 | assertEquals(List.of(List.of(1, 1), List.of(2, 2, 2), List.of(3), List.of(4, 4), List.of(5, 5, 5, 5), List.of(6)), result); 17 | } 18 | 19 | @Test 20 | void equalChunksWithMapperTest() { 21 | var strings = Stream.of("apple", "apricot", "banana", "blueberry", "cherry", "date"); 22 | var result = strings.gather(Packrat.equalChunksBy(s -> s.charAt(0))).toList(); 23 | 24 | assertEquals(List.of(List.of("apple", "apricot"), List.of("banana", "blueberry"), List.of("cherry"), List.of("date")), result); 25 | } 26 | 27 | @Test 28 | void equalChunksWithObjectsTest() { 29 | record Person(String name, int age) {} 30 | 31 | var people = Stream.of( 32 | new Person("John", 25), 33 | new Person("Alice", 25), 34 | new Person("Bob", 30), 35 | new Person("Charlie", 30), 36 | new Person("David", 40), 37 | new Person("Eve", 40), 38 | new Person("Frank", 25) 39 | ); 40 | 41 | var result = people.gather(Packrat.equalChunksBy(Person::age)).toList(); 42 | 43 | assertEquals(List.of( 44 | List.of(new Person("John", 25), new Person("Alice", 25)), 45 | List.of(new Person("Bob", 30), new Person("Charlie", 30)), 46 | List.of(new Person("David", 40), new Person("Eve", 40)), 47 | List.of(new Person("Frank", 25)) 48 | ), result); 49 | } 50 | 51 | @Test 52 | void emptyStreamTest() { 53 | var emptyStream = Stream.empty(); 54 | var result = emptyStream.gather(Packrat.equalChunks()).toList(); 55 | 56 | assertEquals(List.of(), result); 57 | } 58 | 59 | @Test 60 | void singleElementTest() { 61 | var singleElement = Stream.of(42); 62 | var result = singleElement.gather(Packrat.equalChunks()).toList(); 63 | 64 | assertEquals(List.of(List.of(42)), result); 65 | } 66 | 67 | @Test 68 | void equalChunksWithComparatorTest() { 69 | // Case-insensitive string comparison 70 | var strings = Stream.of("Apple", "apple", "Banana", "banana", "Cherry", "cherry"); 71 | var result = strings.gather(Packrat.equalChunks(String.CASE_INSENSITIVE_ORDER)).toList(); 72 | 73 | assertEquals(List.of( 74 | List.of("Apple", "apple"), 75 | List.of("Banana", "banana"), 76 | List.of("Cherry", "cherry") 77 | ), result); 78 | } 79 | 80 | @Test 81 | void equalChunksWithMapperAndComparatorTest() { 82 | record Person(String name, String id) {} 83 | 84 | // Group people by the first letter of their ID, case-insensitive 85 | var people = Stream.of( 86 | new Person("John", "A123"), 87 | new Person("Alice", "a456"), 88 | new Person("Bob", "B789"), 89 | new Person("Charlie", "b012"), 90 | new Person("David", "C345"), 91 | new Person("Eve", "c678") 92 | ); 93 | 94 | var result = people.gather(Packrat.equalChunksBy( 95 | p -> p.id().substring(0, 1), // Map to the first letter of ID 96 | String.CASE_INSENSITIVE_ORDER // Compare case-insensitive 97 | )).toList(); 98 | 99 | assertEquals(List.of( 100 | List.of(new Person("John", "A123"), new Person("Alice", "a456")), 101 | List.of(new Person("Bob", "B789"), new Person("Charlie", "b012")), 102 | List.of(new Person("David", "C345"), new Person("Eve", "c678")) 103 | ), result); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/FilterEntriesTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.stream.Collectors; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | public class FilterEntriesTest { 13 | @Test 14 | public void filterEntriesTest() { 15 | var map = new HashMap(); 16 | map.put("one", 1); 17 | map.put("two", 2); 18 | map.put("three", 3); 19 | map.put("four", 4); 20 | map.put("five", 5); 21 | 22 | // Filter entries where the value is even 23 | var evenValues = map.entrySet().stream() 24 | .gather(Packrat.filterEntries((key, value) -> value % 2 == 0)) 25 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 26 | 27 | assertEquals(2, evenValues.size()); 28 | assertTrue(evenValues.containsKey("two")); 29 | assertTrue(evenValues.containsKey("four")); 30 | assertEquals(2, evenValues.get("two")); 31 | assertEquals(4, evenValues.get("four")); 32 | 33 | // Filter entries where the key starts with 't' 34 | var tKeys = map.entrySet().stream() 35 | .gather(Packrat.filterEntries((key, value) -> key.startsWith("t"))) 36 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 37 | 38 | assertEquals(2, tKeys.size()); 39 | assertTrue(tKeys.containsKey("two")); 40 | assertTrue(tKeys.containsKey("three")); 41 | assertEquals(2, tKeys.get("two")); 42 | assertEquals(3, tKeys.get("three")); 43 | 44 | // Filter entries where key length equals value 45 | var keyLengthEqualsValue = map.entrySet().stream() 46 | .gather(Packrat.filterEntries((key, value) -> key.length() == value)) 47 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 48 | 49 | assertEquals(1, keyLengthEqualsValue.size()); 50 | assertTrue(keyLengthEqualsValue.containsKey("four")); 51 | assertEquals(4, keyLengthEqualsValue.get("four")); 52 | } 53 | 54 | @Test 55 | public void removeEntriesTest() { 56 | var map = new HashMap(); 57 | map.put("one", 1); 58 | map.put("two", 2); 59 | map.put("three", 3); 60 | map.put("four", 4); 61 | map.put("five", 5); 62 | 63 | // Remove entries where the value is even 64 | var oddValues = map.entrySet().stream() 65 | .gather(Packrat.removeEntries((key, value) -> value % 2 == 0)) 66 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 67 | 68 | assertEquals(3, oddValues.size()); 69 | assertTrue(oddValues.containsKey("one")); 70 | assertTrue(oddValues.containsKey("three")); 71 | assertTrue(oddValues.containsKey("five")); 72 | assertEquals(1, oddValues.get("one")); 73 | assertEquals(3, oddValues.get("three")); 74 | assertEquals(5, oddValues.get("five")); 75 | 76 | // Remove entries where the key starts with 'f' 77 | var nonFKeys = map.entrySet().stream() 78 | .gather(Packrat.removeEntries((key, value) -> key.startsWith("f"))) 79 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 80 | 81 | assertEquals(3, nonFKeys.size()); 82 | assertTrue(nonFKeys.containsKey("one")); 83 | assertTrue(nonFKeys.containsKey("two")); 84 | assertTrue(nonFKeys.containsKey("three")); 85 | assertEquals(1, nonFKeys.get("one")); 86 | assertEquals(2, nonFKeys.get("two")); 87 | assertEquals(3, nonFKeys.get("three")); 88 | } 89 | 90 | @Test 91 | public void emptyMapTest() { 92 | var map = new HashMap(); 93 | 94 | var filtered = map.entrySet().stream() 95 | .gather(Packrat.filterEntries((key, value) -> true)) 96 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 97 | 98 | assertTrue(filtered.isEmpty()); 99 | 100 | var removed = map.entrySet().stream() 101 | .gather(Packrat.removeEntries((key, value) -> true)) 102 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 103 | 104 | assertTrue(removed.isEmpty()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/FilterWithIndexTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | import java.util.stream.Stream; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | public class FilterWithIndexTest { 13 | @Test 14 | public void filterByIndexTest() { 15 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 16 | var result = numbers.gather(Packrat.filterWithIndex((index, _) -> index % 2 == 0)).toList(); 17 | assertEquals(List.of(1, 3, 5, 7, 9), result); 18 | } 19 | 20 | @Test 21 | public void filterByElementTest() { 22 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 23 | var result = numbers.gather(Packrat.filterWithIndex((_, element) -> element % 2 == 0)).toList(); 24 | assertEquals(List.of(2, 4, 6, 8, 10), result); 25 | } 26 | 27 | @Test 28 | public void filterByIndexAndElementTest() { 29 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 30 | var result = numbers.gather(Packrat.filterWithIndex((index, element) -> index % 2 == 1 && element % 2 == 0)).toList(); 31 | assertEquals(List.of(2, 4, 6, 8, 10), result); 32 | } 33 | 34 | @Test 35 | public void filterWithStartIndexTest() { 36 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 37 | var result = numbers.gather(Packrat.filterWithIndex((index, element) -> index % 2 == 0, 1)).toList(); 38 | assertEquals(List.of(2, 4, 6, 8, 10), result); 39 | } 40 | 41 | @Test 42 | public void removeWithIndexTest() { 43 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 44 | var result = numbers.gather(Packrat.removeWithIndex((index, element) -> index % 2 == 0)).toList(); 45 | assertEquals(List.of(2, 4, 6, 8, 10), result); 46 | } 47 | 48 | @Test 49 | public void emptyStreamTest() { 50 | var numbers = Stream.of(); 51 | var result = numbers.gather(Packrat.filterWithIndex((index, element) -> index % 2 == 0)).toList(); 52 | assertTrue(result.isEmpty()); 53 | } 54 | 55 | @Test 56 | public void largeStreamTest() { 57 | var numbers = IntStream.rangeClosed(1, 100).boxed(); 58 | var result = numbers.gather(Packrat.filterWithIndex((index, element) -> index % 10 == 0)).toList(); 59 | assertEquals(List.of(1, 11, 21, 31, 41, 51, 61, 71, 81, 91), result); 60 | } 61 | 62 | @Test 63 | public void stringStreamTest() { 64 | var strings = Stream.of("apple", "banana", "cherry", "date", "elderberry"); 65 | var result = strings.gather(Packrat.filterWithIndex((index, element) -> element.length() > index + 1)).toList(); 66 | assertEquals(List.of("apple", "banana", "cherry", "elderberry"), result); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/FilteringTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Objects; 6 | import java.util.stream.Stream; 7 | 8 | import static io.github.jhspetersson.packrat.Packrat.filterBy; 9 | import static io.github.jhspetersson.packrat.Packrat.removeBy; 10 | import static io.github.jhspetersson.packrat.TestUtils.getEmployees; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | public class FilteringTest { 15 | @Test 16 | void filterByTest() { 17 | var age40 = getEmployees().gather(filterBy(Employee::age, 40)).toList(); 18 | 19 | assertEquals(1, age40.size()); 20 | assertEquals("John Rodgers", age40.getFirst().name()); 21 | } 22 | 23 | @Test 24 | void filterByWithPredicateTest() { 25 | var under30 = getEmployees().gather(filterBy(Employee::age, 30, (age, threshold) -> age <= threshold)).toList(); 26 | 27 | assertEquals(3, under30.size()); 28 | assertEquals("Mark Bloom", under30.getFirst().name()); 29 | assertEquals("Rebecca Schneider", under30.get(1).name()); 30 | } 31 | 32 | @Test 33 | void removeByTest() { 34 | var noAge40 = getEmployees().gather(removeBy(Employee::age, 40)).toList(); 35 | 36 | assertEquals(4, noAge40.size()); 37 | assertTrue(noAge40.stream().noneMatch(employee -> employee.age() == 40)); 38 | } 39 | 40 | @Test 41 | void removeByWithPredicateTest() { 42 | var under30 = getEmployees().gather(removeBy(Employee::age, 30, (age, threshold) -> age >= threshold)).toList(); 43 | 44 | assertEquals(3, under30.size()); 45 | assertEquals("Mark Bloom", under30.getFirst().name()); 46 | assertEquals("Rebecca Schneider", under30.get(1).name()); 47 | } 48 | 49 | @Test 50 | void filterByWithNullValueTest() { 51 | Stream employees = Stream.of( 52 | new Employee("Ann Smith", 35), 53 | new Employee(null, 40), 54 | new Employee("Mark Bloom", 21), 55 | new Employee(null, 24) 56 | ); 57 | 58 | var nullNameEmployees = employees.gather(filterBy(Employee::name, null, Objects::equals)).toList(); 59 | 60 | assertEquals(2, nullNameEmployees.size()); 61 | assertEquals(40, nullNameEmployees.get(0).age()); 62 | assertEquals(24, nullNameEmployees.get(1).age()); 63 | } 64 | 65 | @Test 66 | void removeByWithNullValueTest() { 67 | Stream employees = Stream.of( 68 | new Employee("Ann Smith", 35), 69 | new Employee(null, 40), 70 | new Employee("Mark Bloom", 21), 71 | new Employee(null, 24) 72 | ); 73 | 74 | var nonNullNameEmployees = employees.gather(removeBy(Employee::name, null)).toList(); 75 | 76 | assertEquals(2, nonNullNameEmployees.size()); 77 | assertEquals("Ann Smith", nonNullNameEmployees.get(0).name()); 78 | assertEquals("Mark Bloom", nonNullNameEmployees.get(1).name()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/FlatMapTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.stream.Stream; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | 13 | public class FlatMapTest { 14 | @Test 15 | public void flatMapIfTest() { 16 | var strings = Stream.of("A", "B", "CDE", "FG", "H", "IJ", "KL", "M", "NOP"); 17 | var result = strings.gather(Packrat.flatMapIf(s -> Arrays.stream(s.split("")), s -> s.length() >= 3)).toList(); 18 | assertEquals(List.of("A", "B", "C", "D", "E", "FG", "H", "IJ", "KL", "M", "N", "O", "P"), result); 19 | } 20 | 21 | @Test 22 | public void emptyStreamTest() { 23 | var emptyStream = Stream.empty(); 24 | var result = emptyStream.gather(Packrat.flatMapIf(s -> Arrays.stream(s.split("")), s -> s.length() > 1)).toList(); 25 | assertTrue(result.isEmpty(), "Result of empty stream should be empty"); 26 | } 27 | 28 | @Test 29 | public void singleElementTest() { 30 | var singleElement = Stream.of("ABC"); 31 | var result = singleElement.gather(Packrat.flatMapIf(s -> Arrays.stream(s.split("")), s -> s.length() > 1)).toList(); 32 | assertEquals(List.of("A", "B", "C"), result, "Single multi-character element should be flattened"); 33 | 34 | var singleCharElement = Stream.of("X"); 35 | var resultSingleChar = singleCharElement.gather(Packrat.flatMapIf(s -> Arrays.stream(s.split("")), s -> s.length() > 1)).toList(); 36 | assertEquals(List.of("X"), resultSingleChar, "Single character element should remain unchanged"); 37 | } 38 | 39 | @Test 40 | public void alwaysTruePredicateTest() { 41 | var strings = Stream.of("AB", "CD", "EF"); 42 | var result = strings.gather(Packrat.flatMapIf(s -> Arrays.stream(s.split("")), _ -> true)).toList(); 43 | assertEquals(List.of("A", "B", "C", "D", "E", "F"), result, "With always true predicate, all elements should be flattened"); 44 | } 45 | 46 | @Test 47 | public void alwaysFalsePredicateTest() { 48 | var strings = Stream.of("ABC", "DEF", "GHI"); 49 | var result = strings.gather(Packrat.flatMapIf(s -> Arrays.stream(s.split("")), _ -> false)).toList(); 50 | assertEquals(List.of("ABC", "DEF", "GHI"), result, "With always false predicate, no elements should be flattened"); 51 | } 52 | 53 | @Test 54 | public void differentMapperTest() { 55 | var numbers = Stream.of(1, 2, 3, 4, 5); 56 | var result = numbers.gather(Packrat.flatMapIf( 57 | n -> Stream.of(n * 10, n * 100), 58 | n -> n % 2 == 0 59 | )).toList(); 60 | assertEquals(List.of(1, 20, 200, 3, 40, 400, 5), result, "Even numbers should be mapped to multiples"); 61 | } 62 | 63 | @Test 64 | public void emptyResultTest() { 65 | var strings = Stream.of("A", "B", "C"); 66 | 67 | var result = strings.gather(Packrat.flatMapIf( 68 | _ -> Stream.empty(), 69 | s -> s.equals("B") 70 | )).toList(); 71 | 72 | assertEquals(List.of("A", "C"), result, 73 | "When mapper returns empty stream, the element should be effectively removed"); 74 | } 75 | 76 | @Test 77 | public void multipleElementsMapperTest() { 78 | var strings = Stream.of("A", "BB", "CCC"); 79 | 80 | var result = strings.gather(Packrat.flatMapIf( 81 | s -> Stream.of(s, s.repeat(2)), 82 | s -> s.length() > 1 83 | )).toList(); 84 | 85 | assertEquals(List.of("A", "BB", "BBBB", "CCC", "CCCCCC"), result, 86 | "Elements matching predicate should be duplicated with the mapper"); 87 | } 88 | 89 | @Test 90 | public void nullChecksTest() { 91 | assertThrows(NullPointerException.class, () -> Packrat.flatMapIf(null, _ -> true), "Null mapper should throw NullPointerException"); 92 | assertThrows(NullPointerException.class, () -> Packrat.flatMapIf(_ -> Stream.empty(), null), "Null predicate should throw NullPointerException"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/IdentityGathererTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.stream.IntStream; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | public class IdentityGathererTest { 14 | 15 | @Test 16 | void identityTest() { 17 | var original = new ArrayList(); 18 | IntStream.range(0, 100).forEach(original::add); 19 | 20 | var result = original.stream().gather(new IdentityGatherer<>()).toList(); 21 | 22 | assertEquals(original.size(), result.size()); 23 | for (int i = 0; i < original.size(); i++) { 24 | assertEquals(original.get(i), result.get(i)); 25 | } 26 | } 27 | 28 | @Test 29 | void identityEmptyTest() { 30 | var original = new ArrayList(); 31 | 32 | var result = original.stream().gather(new IdentityGatherer<>()).toList(); 33 | 34 | assertTrue(result.isEmpty()); 35 | } 36 | 37 | @Test 38 | void identityParallelTest() { 39 | var size = 100000; 40 | var original = new ArrayList(); 41 | IntStream.range(0, size).forEach(original::add); 42 | 43 | var result = original.parallelStream().gather(new IdentityGatherer<>()).toList(); 44 | 45 | assertEquals(size, result.size()); 46 | assertEquals(size, Set.copyOf(result).size()); 47 | 48 | // Sort both lists to ensure they contain the same elements 49 | var sortedOriginal = new ArrayList<>(original); 50 | sortedOriginal.sort(Integer::compareTo); 51 | 52 | var sortedResult = new ArrayList<>(result); 53 | sortedResult.sort(Integer::compareTo); 54 | 55 | assertEquals(sortedOriginal, sortedResult); 56 | } 57 | 58 | @Test 59 | void identityViaPackratRotateTest() { 60 | // Test the IdentityGatherer through Packrat.rotate(0) 61 | var original = new ArrayList(); 62 | IntStream.range(0, 10).forEach(original::add); 63 | 64 | var result = original.stream().gather(Packrat.rotate(0)).toList(); 65 | 66 | assertEquals(original, result); 67 | } 68 | 69 | @Test 70 | void identityViaPackratIdentityTest() { 71 | // Test the IdentityGatherer through Packrat.identity() 72 | var original = new ArrayList(); 73 | IntStream.range(0, 10).forEach(original::add); 74 | 75 | var result = original.stream().gather(Packrat.identity()).toList(); 76 | 77 | assertEquals(original, result); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/IncreasingDecreasingChunksTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class IncreasingDecreasingChunksTest { 11 | @Test 12 | void increasingChunksTest() { 13 | var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20); 14 | var result = numbers.gather(Packrat.increasingChunks()).toList(); 15 | 16 | assertEquals(List.of(List.of(1, 2), List.of(2, 5), List.of(4), List.of(2, 6, 9), List.of(3, 11), List.of(0, 1, 20)), result); 17 | } 18 | 19 | @Test 20 | void increasingOrEqualChunksTest() { 21 | var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20); 22 | var result = numbers.gather(Packrat.increasingOrEqualChunks()).toList(); 23 | 24 | assertEquals(List.of(List.of(1, 2, 2, 5), List.of(4), List.of(2, 6, 9), List.of(3, 11), List.of(0, 1, 20)), result); 25 | } 26 | 27 | @Test 28 | void decreasingChunksTest() { 29 | var numbers = Stream.of(20, 17, 18, 15, 11, 11, 14, 9, 11, 11, 7, 7, 0); 30 | var result = numbers.gather(Packrat.decreasingChunks()).toList(); 31 | 32 | assertEquals(List.of(List.of(20, 17), List.of(18, 15, 11), List.of(11), List.of(14, 9), List.of(11), List.of(11, 7), List.of(7, 0)), result); 33 | } 34 | 35 | @Test 36 | void decreasingOrEqualChunksTest() { 37 | var numbers = Stream.of(20, 17, 18, 15, 11, 11, 14, 9, 11, 11, 7, 7, 0); 38 | var result = numbers.gather(Packrat.decreasingOrEqualChunks()).toList(); 39 | 40 | assertEquals(List.of(List.of(20, 17), List.of(18, 15, 11, 11), List.of(14, 9), List.of(11, 11, 7, 7, 0)), result); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/IncreasingDecreasingTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class IncreasingDecreasingTest { 11 | @Test 12 | void increasingTest() { 13 | var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20); 14 | var result = numbers.gather(Packrat.increasing()).toList(); 15 | 16 | assertEquals(List.of(1, 2, 5, 6, 9, 11, 20), result); 17 | } 18 | 19 | @Test 20 | void increasingOrEqualTest() { 21 | var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20); 22 | var result = numbers.gather(Packrat.increasingOrEqual()).toList(); 23 | 24 | assertEquals(List.of(1, 2, 2, 5, 6, 9, 11, 20), result); 25 | } 26 | 27 | @Test 28 | void decreasingTest() { 29 | var numbers = Stream.of(20, 17, 18, 15, 11, 11, 14, 9, 11, 11, 7, 7, 0); 30 | var result = numbers.gather(Packrat.decreasing()).toList(); 31 | 32 | assertEquals(List.of(20, 17, 15, 11, 9, 7, 0), result); 33 | } 34 | 35 | @Test 36 | void decreasingOrEqualTest() { 37 | var numbers = Stream.of(20, 17, 18, 15, 11, 11, 14, 9, 11, 11, 7, 7, 0); 38 | var result = numbers.gather(Packrat.decreasingOrEqual()).toList(); 39 | 40 | assertEquals(List.of(20, 17, 15, 11, 11, 9, 7, 7, 0), result); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/IntoListTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.stream.IntStream; 9 | 10 | import static io.github.jhspetersson.packrat.TestUtils.isOrderedSequence; 11 | import static io.github.jhspetersson.packrat.TestUtils.isReverseOrderedSequence; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertFalse; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | 16 | public class IntoListTest { 17 | @Test 18 | void shuffleTest() { 19 | var before = new ArrayList(); 20 | IntStream.range(0, 100).forEach(before::add); 21 | 22 | assertTrue(isOrderedSequence(before)); 23 | 24 | var after = before.stream().gather(Packrat.shuffle()).toList(); 25 | 26 | assertFalse(isOrderedSequence(after)); 27 | } 28 | 29 | @Test 30 | void parallelShuffleTest() { 31 | var size = 100000; 32 | var before = new ArrayList(); 33 | IntStream.range(0, size).forEach(before::add); 34 | 35 | assertTrue(isOrderedSequence(before)); 36 | assertEquals(size, before.size()); 37 | 38 | var after = before.parallelStream().gather(Packrat.shuffle()).toList(); 39 | 40 | assertFalse(isOrderedSequence(after)); 41 | assertEquals(size, after.size()); 42 | assertEquals(size, Set.copyOf(after).size()); 43 | } 44 | 45 | @Test 46 | void reverseTest() { 47 | var before = new ArrayList(); 48 | IntStream.range(0, 100).forEach(before::add); 49 | 50 | assertTrue(isOrderedSequence(before)); 51 | 52 | var after = before.stream().gather(Packrat.reverse()).toList(); 53 | 54 | assertTrue(isReverseOrderedSequence(after)); 55 | } 56 | 57 | @Test 58 | void rotateTest() { 59 | var before = new ArrayList(); 60 | IntStream.range(0, 10).forEach(before::add); 61 | 62 | assertTrue(isOrderedSequence(before)); 63 | 64 | var after = before.stream().gather(Packrat.rotate(3)).toList(); 65 | 66 | assertEquals(List.of(7, 8, 9, 0, 1, 2, 3, 4, 5, 6), after); 67 | 68 | var after2 = before.stream().gather(Packrat.rotate(-4)).toList(); 69 | 70 | assertEquals(List.of(4, 5, 6, 7, 8, 9, 0, 1, 2, 3), after2); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/LastNTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.stream.IntStream; 8 | 9 | import static io.github.jhspetersson.packrat.TestUtils.isOrderedSequence; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | public class LastNTest { 14 | @Test 15 | public void lastTest() { 16 | var before = new ArrayList(); 17 | IntStream.range(0, 100).forEach(before::add); 18 | 19 | assertTrue(isOrderedSequence(before)); 20 | 21 | var after = before.stream().gather(Packrat.last()).toList(); 22 | 23 | assertEquals(List.of(99), after); 24 | } 25 | 26 | @Test 27 | public void lastNTest() { 28 | var before = new ArrayList(); 29 | IntStream.range(0, 100).forEach(before::add); 30 | 31 | assertTrue(isOrderedSequence(before)); 32 | 33 | var after = before.stream().gather(Packrat.last(10)).toList(); 34 | 35 | assertEquals(List.of(90, 91, 92, 93, 94, 95, 96, 97, 98, 99), after); 36 | } 37 | 38 | @Test 39 | public void lastZeroTest() { 40 | var before = new ArrayList(); 41 | IntStream.range(0, 100).forEach(before::add); 42 | 43 | assertTrue(isOrderedSequence(before)); 44 | 45 | var after = before.stream().gather(Packrat.last(0)).toList(); 46 | 47 | assertTrue(after.isEmpty()); 48 | } 49 | 50 | @Test 51 | public void lastNUniqueTest() { 52 | var integers = List.of(1, 2, 3, 4, 5, 4, 1, 1, 1, 2, 2, 6).stream().gather(Packrat.lastUnique(3)).toList(); 53 | 54 | assertEquals(List.of(1, 2, 6), integers); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/MapWhileUntilTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class MapWhileUntilTest { 11 | @Test 12 | public void mapWhileTest() { 13 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(Packrat.mapWhile(n -> n * 10, n -> n <= 3)).toList(); 14 | assertEquals(List.of(10, 20, 30, 4, 5, 6, 7, 8, 9, 10), mapped); 15 | } 16 | 17 | @Test 18 | public void mapUntilTest() { 19 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(Packrat.mapUntil(n -> n * 10, n -> n == 6)).toList(); 20 | assertEquals(List.of(10, 20, 30, 40, 50, 6, 7, 8, 9, 10), mapped); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/MapWithIndexTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class MapWithIndexTest { 10 | @Test 11 | public void mapperTest() { 12 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 13 | 14 | var users = names.stream().gather(Packrat.mapWithIndex(User::new)).toList(); 15 | 16 | assertEquals(names.size(), users.size()); 17 | assertEquals(new User(0L, "Anna"), users.getFirst()); 18 | assertEquals(new User(4L, "Monica"), users.getLast()); 19 | } 20 | 21 | @Test 22 | public void mapperWithStartIndexTest() { 23 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 24 | 25 | var users = names.stream().gather(Packrat.mapWithIndex(User::new, 10)).toList(); 26 | 27 | assertEquals(names.size(), users.size()); 28 | assertEquals(new User(10L, "Anna"), users.getFirst()); 29 | assertEquals(new User(14L, "Monica"), users.getLast()); 30 | } 31 | 32 | record User(long index, String name) {} 33 | } -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/MappingTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class MappingTest { 11 | @Test 12 | public void mapFirstTest() { 13 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(Packrat.mapFirst(n -> n * 10)).toList(); 14 | assertEquals(List.of(10, 2, 3, 4, 5, 6, 7, 8, 9, 10), mapped); 15 | } 16 | 17 | @Test 18 | public void mapNTest() { 19 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(Packrat.mapN(5, n -> n * 10)).toList(); 20 | assertEquals(List.of(10, 20, 30, 40, 50, 6, 7, 8, 9, 10), mapped); 21 | } 22 | 23 | @Test 24 | public void skipAndMapTest() { 25 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(Packrat.skipAndMap(3, n -> n * 10)).toList(); 26 | assertEquals(List.of(1, 2, 3, 40, 50, 60, 70, 80, 90, 100), mapped); 27 | } 28 | 29 | @Test 30 | public void skipAndMapNTest() { 31 | var mapped = IntStream.rangeClosed(1, 10).boxed().gather(Packrat.skipAndMapN(3, 5, n -> n * 10)).toList(); 32 | assertEquals(List.of(1, 2, 3, 40, 50, 60, 70, 80, 9, 10), mapped); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/MinMaxTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.Stream; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | public class MinMaxTest { 11 | @Test 12 | void emptyTest() { 13 | var check = Stream.empty().gather(Packrat.minBy(element -> Long.parseLong(element.toString()))).toList(); 14 | 15 | assertTrue(check.isEmpty()); 16 | } 17 | 18 | @Test 19 | void minTest() { 20 | var check = Stream.of("10", "2", "1", "-12", "22", "35", "66", "77", "123", "4", "7", "29") 21 | .gather(Packrat.minBy(Long::parseLong)) 22 | .toList(); 23 | 24 | assertEquals(1, check.size()); 25 | assertEquals("-12", check.getFirst()); 26 | } 27 | 28 | @Test 29 | void maxTest() { 30 | var check = Stream.of("10", "2", "1", "-12", "22", "35", "66", "77", "123", "4", "7", "29") 31 | .gather(Packrat.maxBy(Long::parseLong)) 32 | .toList(); 33 | 34 | assertEquals(1, check.size()); 35 | assertEquals("123", check.getFirst()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/NCopiesTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.stream.IntStream; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class NCopiesTest { 10 | @Test 11 | public void nCopiesTest() { 12 | var sum = IntStream.of(5).boxed().gather(Packrat.nCopies(20)).reduce(Integer::sum).orElseThrow(); 13 | assertEquals(100, sum); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/NthTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | import java.util.stream.Stream; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | public class NthTest { 13 | @Test 14 | public void nthTest() { 15 | var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 16 | var result = numbers.gather(Packrat.nth(3)).toList(); 17 | assertEquals(List.of(3, 6, 9), result); 18 | } 19 | 20 | @Test 21 | public void nthWithLargeStreamTest() { 22 | var numbers = IntStream.rangeClosed(1, 100).boxed(); 23 | var result = numbers.gather(Packrat.nth(10)).toList(); 24 | assertEquals(List.of(10, 20, 30, 40, 50, 60, 70, 80, 90, 100), result); 25 | } 26 | 27 | @Test 28 | public void nthWithEmptyStreamTest() { 29 | var numbers = Stream.of(); 30 | var result = numbers.gather(Packrat.nth(5)).toList(); 31 | assertTrue(result.isEmpty()); 32 | } 33 | 34 | @Test 35 | public void nthWithSingleElementTest() { 36 | var numbers = Stream.of(42); 37 | var result = numbers.gather(Packrat.nth(1)).toList(); 38 | assertEquals(List.of(42), result); 39 | } 40 | 41 | @Test 42 | public void nthWithFewerElementsThanNTest() { 43 | var numbers = Stream.of(1, 2, 3, 4); 44 | var result = numbers.gather(Packrat.nth(5)).toList(); 45 | assertEquals(List.of(), result); 46 | } 47 | 48 | @Test 49 | public void nthWithExactlyNElementsTest() { 50 | var numbers = Stream.of(1, 2, 3, 4, 5); 51 | var result = numbers.gather(Packrat.nth(5)).toList(); 52 | assertEquals(List.of(5), result); 53 | } 54 | 55 | @Test 56 | public void nthWithStringTest() { 57 | var strings = Stream.of("apple", "banana", "cherry", "date", "elderberry", "fig", "grape"); 58 | var result = strings.gather(Packrat.nth(2)).toList(); 59 | assertEquals(List.of("banana", "date", "fig"), result); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/PeekWithIndexTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class PeekWithIndexTest { 11 | @Test 12 | public void defaultTest() { 13 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 14 | var indices = new ArrayList(); 15 | var elements = new ArrayList(); 16 | 17 | var result = names.stream() 18 | .gather(Packrat.peekWithIndex((index, element) -> { 19 | indices.add(index); 20 | elements.add(element); 21 | })) 22 | .toList(); 23 | 24 | // Verify original elements are passed through 25 | assertEquals(names, result); 26 | 27 | // Verify consumer was called with correct indices and elements 28 | assertEquals(List.of(0L, 1L, 2L, 3L, 4L), indices); 29 | assertEquals(names, elements); 30 | } 31 | 32 | @Test 33 | public void startIndexTest() { 34 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 35 | var indices = new ArrayList(); 36 | var elements = new ArrayList(); 37 | long startIndex = 10; 38 | 39 | var result = names.stream() 40 | .gather(Packrat.peekWithIndex((index, element) -> { 41 | indices.add(index); 42 | elements.add(element); 43 | }, startIndex)) 44 | .toList(); 45 | 46 | // Verify original elements are passed through 47 | assertEquals(names, result); 48 | 49 | // Verify consumer was called with correct indices and elements 50 | assertEquals(List.of(10L, 11L, 12L, 13L, 14L), indices); 51 | assertEquals(names, elements); 52 | } 53 | } -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/RemoveDuplicatesTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class RemoveDuplicatesTest { 10 | @Test 11 | public void removeDuplicatesTest() { 12 | var listWithCopies = List.of(0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 8, 8, 9, 8, 7, 7, 6, 5, 4, 4, 4, 3, 2, 1, 0); 13 | var unique = listWithCopies.stream().gather(Packrat.removeDuplicates()).toList(); 14 | 15 | var expected = List.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0); 16 | 17 | assertEquals(expected, unique); 18 | } 19 | 20 | @Test 21 | public void emptyListTest() { 22 | var emptyList = List.of(); 23 | var result = emptyList.stream().gather(Packrat.removeDuplicates()).toList(); 24 | 25 | assertEquals(List.of(), result); 26 | } 27 | 28 | @Test 29 | public void singleElementTest() { 30 | var singleElement = List.of(42); 31 | var result = singleElement.stream().gather(Packrat.removeDuplicates()).toList(); 32 | 33 | assertEquals(List.of(42), result); 34 | } 35 | 36 | @Test 37 | public void allDuplicatesTest() { 38 | var allDuplicates = List.of(1, 1, 1, 1, 1); 39 | var result = allDuplicates.stream().gather(Packrat.removeDuplicates()).toList(); 40 | 41 | assertEquals(List.of(1), result); 42 | } 43 | 44 | @Test 45 | public void noDuplicatesTest() { 46 | var noDuplicates = List.of(1, 2, 3, 4, 5); 47 | var result = noDuplicates.stream().gather(Packrat.removeDuplicates()).toList(); 48 | 49 | assertEquals(List.of(1, 2, 3, 4, 5), result); 50 | } 51 | 52 | @Test 53 | public void removeDuplicatesByTest() { 54 | var listWithCopies = List.of( 55 | new Person("John", 25), 56 | new Person("Alice", 30), 57 | new Person("Bob", 30), 58 | new Person("Charlie", 30), 59 | new Person("David", 40), 60 | new Person("Eve", 40), 61 | new Person("Frank", 40) 62 | ); 63 | 64 | var uniqueByAge = listWithCopies.stream().gather(Packrat.removeDuplicatesBy(Person::age)).toList(); 65 | 66 | var expectedByAge = List.of( 67 | new Person("John", 25), 68 | new Person("Alice", 30), 69 | new Person("David", 40) 70 | ); 71 | 72 | assertEquals(expectedByAge, uniqueByAge); 73 | } 74 | 75 | @Test 76 | public void removeDuplicatesByWithNullsTest() { 77 | var listWithNulls = java.util.Arrays.asList("a", "b", null, null, "c", "d", "d"); 78 | 79 | var result = listWithNulls.stream().gather(Packrat.removeDuplicatesBy(s -> s)).toList(); 80 | 81 | var expected = java.util.Arrays.asList("a", "b", null, "c", "d"); 82 | 83 | assertEquals(expected, result); 84 | } 85 | 86 | @Test 87 | public void removeDuplicatesByStringLengthTest() { 88 | var strings = List.of("a", "b", "cc", "d", "ee", "fff", "gg", "h", "ii"); 89 | 90 | var uniqueByLength = strings.stream().gather(Packrat.removeDuplicatesBy(String::length)).toList(); 91 | 92 | var expectedByLength = List.of("a", "cc", "d", "ee", "fff", "gg", "h", "ii"); 93 | 94 | assertEquals(expectedByLength, uniqueByLength); 95 | } 96 | 97 | private record Person(String name, int age) {} 98 | } -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/RepeatGathererTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | public class RepeatGathererTest { 12 | @Test 13 | public void repeatTest() { 14 | var numbers = Stream.of(1, 2, 3); 15 | var result = numbers.gather(Packrat.repeat(3)).toList(); 16 | assertEquals(List.of(1, 2, 3, 1, 2, 3, 1, 2, 3), result); 17 | } 18 | 19 | @Test 20 | public void repeatZeroTest() { 21 | var numbers = Stream.of(1, 2, 3); 22 | var result = numbers.gather(Packrat.repeat(0)).toList(); 23 | assertTrue(result.isEmpty()); 24 | } 25 | 26 | @Test 27 | public void repeatOnceTest() { 28 | var numbers = Stream.of(1, 2, 3); 29 | var result = numbers.gather(Packrat.repeat(1)).toList(); 30 | assertEquals(List.of(1, 2, 3), result); 31 | } 32 | 33 | @Test 34 | public void repeatEmptyStreamTest() { 35 | var numbers = Stream.empty(); 36 | var result = numbers.gather(Packrat.repeat(5)).toList(); 37 | assertTrue(result.isEmpty()); 38 | } 39 | } -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/SamplingTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.stream.IntStream; 8 | 9 | import static io.github.jhspetersson.packrat.TestUtils.isOrdered; 10 | import static io.github.jhspetersson.packrat.TestUtils.isOrderedSequence; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | public class SamplingTest { 15 | @Test 16 | void emptyTest() { 17 | var before = Collections.emptyList(); 18 | var after = before.stream().gather(Packrat.sample(100)).toList(); 19 | 20 | assertTrue(after.isEmpty()); 21 | } 22 | 23 | @Test 24 | void notEnoughTest() { 25 | var size = 10; 26 | var before = new ArrayList(); 27 | IntStream.range(0, size).forEach(before::add); 28 | 29 | assertTrue(isOrderedSequence(before)); 30 | assertEquals(size, before.size()); 31 | 32 | var after = before.stream().gather(Packrat.sample(100)).toList(); 33 | 34 | assertTrue(isOrderedSequence(after)); 35 | assertEquals(size, after.size()); 36 | } 37 | 38 | @Test 39 | void exactTest() { 40 | var size = 100; 41 | var before = new ArrayList(); 42 | IntStream.range(0, size).forEach(before::add); 43 | 44 | assertTrue(isOrderedSequence(before)); 45 | assertEquals(size, before.size()); 46 | 47 | var after = before.stream().gather(Packrat.sample(100)).toList(); 48 | 49 | assertTrue(isOrderedSequence(after)); 50 | assertEquals(size, after.size()); 51 | } 52 | 53 | @Test 54 | void normalTest() { 55 | var size = 100000; 56 | var before = new ArrayList(); 57 | IntStream.range(0, size).forEach(before::add); 58 | 59 | assertTrue(isOrderedSequence(before)); 60 | assertEquals(size, before.size()); 61 | 62 | var after = before.stream().gather(Packrat.sample(100)).toList(); 63 | 64 | assertTrue(isOrdered(after)); 65 | assertEquals(100, after.size()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/TestUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import java.util.List; 4 | import java.util.stream.Stream; 5 | 6 | class TestUtils { 7 | 8 | static Stream getEmployees() { 9 | return Stream.of( 10 | new Employee("Ann Smith", 35), 11 | new Employee("John Rodgers", 40), 12 | new Employee("Mark Bloom", 21), 13 | new Employee("Rebecca Schneider", 24), 14 | new Employee("Luke Norman", 21) 15 | ); 16 | } 17 | 18 | static boolean isOrdered(List list) { 19 | var prev = list.getFirst(); 20 | for (var i = 1; i < list.size() - 1; i++) { 21 | if (list.get(i) <= prev) { 22 | return false; 23 | } 24 | prev = list.get(i); 25 | } 26 | return true; 27 | } 28 | 29 | static boolean isOrderedSequence(List list) { 30 | for (var i = 0; i < list.size() - 1; i++) { 31 | if (i != list.get(i)) { 32 | return false; 33 | } 34 | } 35 | return true; 36 | } 37 | 38 | static boolean isReverseOrderedSequence(List list) { 39 | for (var i = 0; i < list.size() - 1; i++) { 40 | if (list.size() - 1 - i != list.get(i)) { 41 | return false; 42 | } 43 | } 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/WindowFixedWithIndexTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | import java.util.stream.Stream; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | 12 | public class WindowFixedWithIndexTest { 13 | @Test 14 | public void windowFixedWithIndexTest() { 15 | var numbers = IntStream.rangeClosed(1, 10).boxed(); 16 | var result = numbers.gather(Packrat.windowFixedWithIndex(3)).toList(); 17 | 18 | assertEquals(3, result.size()); 19 | 20 | assertEquals(0L, result.get(0).getKey()); 21 | assertEquals(List.of(1, 2, 3), result.get(0).getValue()); 22 | 23 | assertEquals(1L, result.get(1).getKey()); 24 | assertEquals(List.of(4, 5, 6), result.get(1).getValue()); 25 | 26 | assertEquals(2L, result.get(2).getKey()); 27 | assertEquals(List.of(7, 8, 9), result.get(2).getValue()); 28 | } 29 | 30 | @Test 31 | public void windowFixedWithIndexRemainingElementsTest() { 32 | var numbers = IntStream.rangeClosed(1, 11).boxed(); 33 | var result = numbers.gather(Packrat.windowFixedWithIndex(3)).toList(); 34 | 35 | assertEquals(3, result.size()); 36 | 37 | // The last element (11) is not included in any window because it doesn't form a complete window 38 | assertEquals(0L, result.get(0).getKey()); 39 | assertEquals(List.of(1, 2, 3), result.get(0).getValue()); 40 | 41 | assertEquals(1L, result.get(1).getKey()); 42 | assertEquals(List.of(4, 5, 6), result.get(1).getValue()); 43 | 44 | assertEquals(2L, result.get(2).getKey()); 45 | assertEquals(List.of(7, 8, 9), result.get(2).getValue()); 46 | } 47 | 48 | @Test 49 | public void windowFixedWithIndexCustomStartTest() { 50 | var numbers = IntStream.rangeClosed(1, 6).boxed(); 51 | var result = numbers.gather(Packrat.windowFixedWithIndex(2, 10)).toList(); 52 | 53 | assertEquals(3, result.size()); 54 | 55 | assertEquals(10L, result.get(0).getKey()); 56 | assertEquals(List.of(1, 2), result.get(0).getValue()); 57 | 58 | assertEquals(11L, result.get(1).getKey()); 59 | assertEquals(List.of(3, 4), result.get(1).getValue()); 60 | 61 | assertEquals(12L, result.get(2).getKey()); 62 | assertEquals(List.of(5, 6), result.get(2).getValue()); 63 | } 64 | 65 | @Test 66 | public void windowFixedWithIndexCustomMapperTest() { 67 | var numbers = IntStream.rangeClosed(1, 6).boxed(); 68 | var result = numbers.gather(Packrat.windowFixedWithIndex(3, (index, window) -> 69 | "Window " + index + ": " + window)).toList(); 70 | 71 | assertEquals(2, result.size()); 72 | assertEquals("Window 0: [1, 2, 3]", result.get(0)); 73 | assertEquals("Window 1: [4, 5, 6]", result.get(1)); 74 | } 75 | 76 | @Test 77 | public void windowFixedWithIndexCustomMapperAndStartTest() { 78 | var numbers = IntStream.rangeClosed(1, 6).boxed(); 79 | var result = numbers.gather(Packrat.windowFixedWithIndex(3, (index, window) -> 80 | "Window " + index + ": " + window, 100)).toList(); 81 | 82 | assertEquals(2, result.size()); 83 | assertEquals("Window 100: [1, 2, 3]", result.get(0)); 84 | assertEquals("Window 101: [4, 5, 6]", result.get(1)); 85 | } 86 | 87 | @Test 88 | public void windowFixedWithIndexEmptySourceTest() { 89 | var numbers = Stream.empty(); 90 | var result = numbers.gather(Packrat.windowFixedWithIndex(3)).toList(); 91 | 92 | assertEquals(0, result.size()); 93 | } 94 | 95 | @Test 96 | public void windowFixedWithIndexInsufficientElementsTest() { 97 | var numbers = Stream.of(1, 2); 98 | var result = numbers.gather(Packrat.windowFixedWithIndex(3)).toList(); 99 | 100 | assertEquals(0, result.size()); 101 | } 102 | 103 | @Test 104 | public void windowFixedWithIndexInvalidWindowSizeTest() { 105 | assertThrows(IllegalArgumentException.class, () -> Packrat.windowFixedWithIndex(0)); 106 | assertThrows(IllegalArgumentException.class, () -> Packrat.windowFixedWithIndex(-1)); 107 | } 108 | } -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/WindowSlidingWithIndexTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.stream.IntStream; 7 | import java.util.stream.Stream; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | 12 | public class WindowSlidingWithIndexTest { 13 | @Test 14 | public void windowSlidingWithIndexTest() { 15 | var numbers = IntStream.rangeClosed(1, 10).boxed(); 16 | var result = numbers.gather(Packrat.windowSlidingWithIndex(3)).toList(); 17 | 18 | assertEquals(8, result.size()); 19 | 20 | assertEquals(0L, result.get(0).getKey()); 21 | assertEquals(List.of(1, 2, 3), result.get(0).getValue()); 22 | 23 | assertEquals(1L, result.get(1).getKey()); 24 | assertEquals(List.of(2, 3, 4), result.get(1).getValue()); 25 | 26 | assertEquals(7L, result.get(7).getKey()); 27 | assertEquals(List.of(8, 9, 10), result.get(7).getValue()); 28 | } 29 | 30 | @Test 31 | public void windowSlidingWithIndexCustomStartTest() { 32 | var numbers = IntStream.rangeClosed(1, 5).boxed(); 33 | var result = numbers.gather(Packrat.windowSlidingWithIndex(2, 10)).toList(); 34 | 35 | assertEquals(4, result.size()); 36 | 37 | assertEquals(10L, result.get(0).getKey()); 38 | assertEquals(List.of(1, 2), result.get(0).getValue()); 39 | 40 | assertEquals(13L, result.get(3).getKey()); 41 | assertEquals(List.of(4, 5), result.get(3).getValue()); 42 | } 43 | 44 | @Test 45 | public void windowSlidingWithIndexCustomMapperTest() { 46 | var numbers = IntStream.rangeClosed(1, 5).boxed(); 47 | var result = numbers.gather(Packrat.windowSlidingWithIndex(3, (index, window) -> 48 | "Window " + index + ": " + window)).toList(); 49 | 50 | assertEquals(3, result.size()); 51 | assertEquals("Window 0: [1, 2, 3]", result.get(0)); 52 | assertEquals("Window 1: [2, 3, 4]", result.get(1)); 53 | assertEquals("Window 2: [3, 4, 5]", result.get(2)); 54 | } 55 | 56 | @Test 57 | public void windowSlidingWithIndexCustomMapperAndStartTest() { 58 | var numbers = IntStream.rangeClosed(1, 5).boxed(); 59 | var result = numbers.gather(Packrat.windowSlidingWithIndex(3, (index, window) -> 60 | "Window " + index + ": " + window, 100)).toList(); 61 | 62 | assertEquals(3, result.size()); 63 | assertEquals("Window 100: [1, 2, 3]", result.get(0)); 64 | assertEquals("Window 101: [2, 3, 4]", result.get(1)); 65 | assertEquals("Window 102: [3, 4, 5]", result.get(2)); 66 | } 67 | 68 | @Test 69 | public void windowSlidingWithIndexEmptySourceTest() { 70 | var numbers = Stream.empty(); 71 | var result = numbers.gather(Packrat.windowSlidingWithIndex(3)).toList(); 72 | 73 | assertEquals(0, result.size()); 74 | } 75 | 76 | @Test 77 | public void windowSlidingWithIndexInsufficientElementsTest() { 78 | var numbers = Stream.of(1, 2); 79 | var result = numbers.gather(Packrat.windowSlidingWithIndex(3)).toList(); 80 | 81 | assertEquals(0, result.size()); 82 | } 83 | 84 | @Test 85 | public void windowSlidingWithIndexInvalidWindowSizeTest() { 86 | assertThrows(IllegalArgumentException.class, () -> Packrat.windowSlidingWithIndex(0)); 87 | assertThrows(IllegalArgumentException.class, () -> Packrat.windowSlidingWithIndex(-1)); 88 | } 89 | } -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/ZipTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.Stream; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class ZipTest { 14 | @Test 15 | public void zipWithIterableTest() { 16 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 17 | var ages = List.of(20, 30, 40, 50, 60, 70, 80, 90); 18 | 19 | var users = names.stream().gather(Packrat.zip(ages, User::new)).toList(); 20 | 21 | assertEquals(names.size(), users.size()); 22 | assertEquals(new User("Anna", 20), users.getFirst()); 23 | assertEquals(new User("Monica", 60), users.getLast()); 24 | } 25 | 26 | @Test 27 | public void zipWithStreamTest() { 28 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 29 | var ages = Stream.of(20, 30, 40, 50, 60, 70, 80, 90); 30 | 31 | var users = names.stream().gather(Packrat.zip(ages, User::new)).toList(); 32 | 33 | assertEquals(names.size(), users.size()); 34 | assertEquals(new User("Anna", 20), users.getFirst()); 35 | assertEquals(new User("Monica", 60), users.getLast()); 36 | } 37 | 38 | @Test 39 | public void zipToMapTest() { 40 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 41 | var ages = List.of(20, 30, 40, 50, 60, 70, 80, 90); 42 | 43 | var users = names.stream().gather(Packrat.zip(ages)).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 44 | System.out.println(users); 45 | assertEquals(names.size(), users.size()); 46 | assertEquals(20, users.get("Anna")); 47 | assertEquals(60, users.get("Monica")); 48 | } 49 | 50 | @Test 51 | public void zipStreamToMapTest() { 52 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 53 | var ages = Stream.of(20, 30, 40, 50, 60, 70, 80, 90); 54 | 55 | var users = names.stream().gather(Packrat.zip(ages)).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 56 | 57 | assertEquals(names.size(), users.size()); 58 | assertEquals(20, users.get("Anna")); 59 | assertEquals(60, users.get("Monica")); 60 | } 61 | 62 | @Test 63 | public void zipLongerWithShorterTest() { 64 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 65 | var ages = List.of(20, 30, 40); 66 | 67 | var users = names.stream().gather(Packrat.zip(ages, User::new)).toList(); 68 | 69 | assertEquals(ages.size(), users.size()); 70 | assertEquals(new User("Anna", 20), users.getFirst()); 71 | assertEquals(new User("Sandra", 40), users.getLast()); 72 | } 73 | 74 | record User(String name, int age) {} 75 | 76 | @Test 77 | public void zipWithIteratorTest() { 78 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 79 | var ages = List.of(20, 30, 40, 50, 60, 70, 80, 90); 80 | Iterator agesIterator = ages.iterator(); 81 | 82 | var users = names.stream().gather(Packrat.zip(agesIterator)).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 83 | 84 | assertEquals(names.size(), users.size()); 85 | assertEquals(20, users.get("Anna")); 86 | assertEquals(60, users.get("Monica")); 87 | } 88 | 89 | @Test 90 | public void zipWithIteratorAndMapperTest() { 91 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 92 | var ages = List.of(20, 30, 40, 50, 60, 70, 80, 90); 93 | Iterator agesIterator = ages.iterator(); 94 | 95 | var users = names.stream().gather(Packrat.zip(agesIterator, User::new)).toList(); 96 | 97 | assertEquals(names.size(), users.size()); 98 | assertEquals(new User("Anna", 20), users.getFirst()); 99 | assertEquals(new User("Monica", 60), users.getLast()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/io/github/jhspetersson/packrat/ZipWithIndexTest.java: -------------------------------------------------------------------------------- 1 | package io.github.jhspetersson.packrat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class ZipWithIndexTest { 11 | @Test 12 | public void defaultTest() { 13 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 14 | 15 | var users = names.stream().gather(Packrat.zipWithIndex()).toList(); 16 | 17 | assertEquals(names.size(), users.size()); 18 | assertEquals(Map.entry(0L, "Anna"), users.getFirst()); 19 | assertEquals(Map.entry(4L, "Monica"), users.getLast()); 20 | } 21 | 22 | @Test 23 | public void startIndexTest() { 24 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 25 | 26 | var users = names.stream().gather(Packrat.zipWithIndex(10)).toList(); 27 | 28 | assertEquals(names.size(), users.size()); 29 | assertEquals(Map.entry(10L, "Anna"), users.getFirst()); 30 | assertEquals(Map.entry(14L, "Monica"), users.getLast()); 31 | } 32 | 33 | @Test 34 | public void mapperTest() { 35 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 36 | 37 | var users = names.stream().gather(Packrat.zipWithIndex(User::new)).toList(); 38 | 39 | assertEquals(names.size(), users.size()); 40 | assertEquals(new User(0L, "Anna"), users.getFirst()); 41 | assertEquals(new User(4L, "Monica"), users.getLast()); 42 | 43 | System.out.println(users); 44 | } 45 | 46 | @Test 47 | public void mapperWithStartIndexTest() { 48 | var names = List.of("Anna", "Mike", "Sandra", "Rudolf", "Monica"); 49 | 50 | var users = names.stream().gather(Packrat.zipWithIndex(User::new, 10)).toList(); 51 | 52 | assertEquals(names.size(), users.size()); 53 | assertEquals(new User(10L, "Anna"), users.getFirst()); 54 | assertEquals(new User(14L, "Monica"), users.getLast()); 55 | } 56 | 57 | record User(long index, String name) {} 58 | } --------------------------------------------------------------------------------