├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml ├── push-javadoc-to-gh-pages.sh ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── rschmitt │ │ └── collider │ │ ├── ClojureList.java │ │ ├── ClojureMap.java │ │ ├── ClojureSet.java │ │ ├── Collider.java │ │ ├── TransientList.java │ │ ├── TransientMap.java │ │ └── TransientSet.java └── test │ └── java │ └── com │ └── github │ └── rschmitt │ └── collider │ ├── ClojureListTest.java │ ├── ClojureMapTest.java │ ├── ClojureSetTest.java │ ├── FactoryMethodTest.java │ ├── TransientListTest.java │ ├── TransientMapTest.java │ └── TransientSetTest.java └── test-all-versions.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | /*.iml 4 | /pom-*.xml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: openjdk8 3 | script: ./test-all-versions.sh 4 | install: mvn install -DskipTests=true -Dgpg.skip=true 5 | 6 | env: 7 | global: 8 | - secure: "KQv0YbutcCv3GA1VOAUnqWaIn68o7yKEIpL2E5ux/rGpEJtOoh9+Qp+hf9eZaTKWD5Rb40FUzaTzshmWN5I1JibEc1OxlbIOJ1tnbwrb6sONzl7XUSzWndcWUEY98HWclodj1g0z3pBloTUn3s1DrHIiHLm08n9/ZJb+t0M/mgAOYeC3r7jji1MxUDGOkliFxPjT0ZrEuPOcF2/inINJEapw8HnVy2dnWqIOtPTzLKvrfE/KWKdNJklR6j4xY6c5k2mcCDDBDCq2xcZ+BR1aOaYlHICigA/nNzV2P+5jggL2hH4u1KPdE35MkvcO7mOdn/HzEhLr/UwBoG//8Jsr/K3WYlDz4m5uZxdt5IBB/F3Sh4R9Qr+ZmBMiQJ7d1twFcVohaOIQr9rGd3XdnTB1ayNhzo4O01toBIP29p4P0mGTFOggEX7REgBEjVogz/au4Qtmdi/rk7brtNf642T3sXcJfBsjUDqHTOxZ4rM1Xlwk4uj9XCaPfOkKtG184xA0lhu04eXyxOOmMi7SjCWHmGFqCoUU8YA1fRaJYXzgqQC31MIEhwRWXiMa9IMAXJk7kM0t2TW6VnKaS3/xic3g//sBE6IzAgwIqFNuLkh6G2ggkFms2c/XZ8oFxyGo12E2Aq4xVFIHNYGeDl7C6F7UiVksX+KZrl0ugtckWE4blH4=" 9 | 10 | after_success: 11 | - ./push-javadoc-to-gh-pages.sh 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons CC0 1.0 Universal 2 | 3 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 4 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 5 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION 6 | ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE 7 | USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND 8 | DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT 9 | OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. 10 | 11 | Statement of Purpose 12 | 13 | The laws of most jurisdictions throughout the world automatically confer 14 | exclusive Copyright and Related Rights (defined below) upon the creator 15 | and subsequent owner(s) (each and all, an "owner") of an original work 16 | of authorship and/or a database (each, a "Work"). 17 | 18 | Certain owners wish to permanently relinquish those rights to a Work for 19 | the purpose of contributing to a commons of creative, cultural and 20 | scientific works ("Commons") that the public can reliably and without 21 | fear of later claims of infringement build upon, modify, incorporate in 22 | other works, reuse and redistribute as freely as possible in any form 23 | whatsoever and for any purposes, including without limitation commercial 24 | purposes. These owners may contribute to the Commons to promote the 25 | ideal of a free culture and the further production of creative, cultural 26 | and scientific works, or to gain reputation or greater distribution for 27 | their Work in part through the use and efforts of others. 28 | 29 | For these and/or other purposes and motivations, and without any 30 | expectation of additional consideration or compensation, the person 31 | associating CC0 with a Work (the "Affirmer"), to the extent that he or 32 | she is an owner of Copyright and Related Rights in the Work, voluntarily 33 | elects to apply CC0 to the Work and publicly distribute the Work under 34 | its terms, with knowledge of his or her Copyright and Related Rights in 35 | the Work and the meaning and intended legal effect of CC0 on those 36 | rights. 37 | 38 | 1. Copyright and Related Rights. A Work made available under CC0 may be 39 | protected by copyright and related or neighboring rights ("Copyright and 40 | Related Rights"). Copyright and Related Rights include, but are not 41 | limited to, the following: 42 | 43 | i. the right to reproduce, adapt, distribute, perform, display, 44 | communicate, and translate a Work; 45 | 46 | ii. moral rights retained by the original author(s) and/or performer(s); 47 | 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | 51 | iv. rights protecting against unfair competition in regards to a Work, 52 | subject to the limitations in paragraph 4(a), below; 53 | 54 | v. rights protecting the extraction, dissemination, use and reuse of 55 | data in a Work; 56 | 57 | vi. database rights (such as those arising under Directive 96/9/EC of 58 | the European Parliament and of the Council of 11 March 1996 on the legal 59 | protection of databases, and under any national implementation thereof, 60 | including any amended or successor version of such directive); and 61 | 62 | vii. other similar, equivalent or corresponding rights throughout the 63 | world based on applicable law or treaty, and any national 64 | implementations thereof. 65 | 66 | 2. Waiver. To the greatest extent permitted by, but not in contravention 67 | of, applicable law, Affirmer hereby overtly, fully, permanently, 68 | irrevocably and unconditionally waives, abandons, and surrenders all of 69 | Affirmer's Copyright and Related Rights and associated claims and causes 70 | of action, whether now known or unknown (including existing as well as 71 | future claims and causes of action), in the Work (i) in all territories 72 | worldwide, (ii) for the maximum duration provided by applicable law or 73 | treaty (including future time extensions), (iii) in any current or 74 | future medium and for any number of copies, and (iv) for any purpose 75 | whatsoever, including without limitation commercial, advertising or 76 | promotional purposes (the "Waiver"). Affirmer makes the Waiver for the 77 | benefit of each member of the public at large and to the detriment of 78 | Affirmer's heirs and successors, fully intending that such Waiver shall 79 | not be subject to revocation, rescission, cancellation, termination, or 80 | any other legal or equitable action to disrupt the quiet enjoyment of 81 | the Work by the public as contemplated by Affirmer's express Statement 82 | of Purpose. 83 | 84 | 3. Public License Fallback. Should any part of the Waiver for any reason 85 | be judged legally invalid or ineffective under applicable law, then the 86 | Waiver shall be preserved to the maximum extent permitted taking into 87 | account Affirmer's express Statement of Purpose. In addition, to the 88 | extent the Waiver is so judged Affirmer hereby grants to each affected 89 | person a royalty-free, non transferable, non sublicensable, non 90 | exclusive, irrevocable and unconditional license to exercise Affirmer's 91 | Copyright and Related Rights in the Work (i) in all territories 92 | worldwide, (ii) for the maximum duration provided by applicable law or 93 | treaty (including future time extensions), (iii) in any current or 94 | future medium and for any number of copies, and (iv) for any purpose 95 | whatsoever, including without limitation commercial, advertising or 96 | promotional purposes (the "License"). The License shall be deemed 97 | effective as of the date CC0 was applied by Affirmer to the Work. Should 98 | any part of the License for any reason be judged legally invalid or 99 | ineffective under applicable law, such partial invalidity or 100 | ineffectiveness shall not invalidate the remainder of the License, and 101 | in such case Affirmer hereby affirms that he or she will not (i) 102 | exercise any of his or her remaining Copyright and Related Rights in the 103 | Work or (ii) assert any associated claims and causes of action with 104 | respect to the Work, in either case contrary to Affirmer's express 105 | Statement of Purpose. 106 | 107 | 4. Limitations and Disclaimers. 108 | 109 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 110 | surrendered, licensed or otherwise affected by this document. 111 | 112 | b. Affirmer offers the Work as-is and makes no representations or 113 | warranties of any kind concerning the Work, express, implied, statutory 114 | or otherwise, including without limitation warranties of title, 115 | merchantability, fitness for a particular purpose, non infringement, or 116 | the absence of latent or other defects, accuracy, or the present or 117 | absence of errors, whether or not discoverable, all to the greatest 118 | extent permissible under applicable law. 119 | 120 | c. Affirmer disclaims responsibility for clearing rights of other 121 | persons that may apply to the Work or any use thereof, including without 122 | limitation any person's Copyright and Related Rights in the Work. 123 | Further, Affirmer disclaims responsibility for obtaining any necessary 124 | consents, permissions or other rights required for any use of the Work. 125 | 126 | d. Affirmer understands and acknowledges that Creative Commons is not a 127 | party to this document and has no duty or obligation with respect to 128 | this CC0 or use of the Work. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](http://img.shields.io/travis/rschmitt/collider.svg)](https://travis-ci.org/rschmitt/collider) 2 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.rschmitt/collider.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.rschmitt/collider) 3 | [![License](https://img.shields.io/github/license/rschmitt/collider.svg)](https://creativecommons.org/about/cc0) 4 | 5 | # Collider 6 | 7 | Collider is a tiny library that provides immutable persistent collections for 8 | Java. It does this by wrapping Clojure's collections in an object-oriented and 9 | type-safe facade. Browse the Javadoc [online](http://rschmitt.github.io/collider/javadoc/). 10 | 11 | ## Examples 12 | 13 | Collider collections can be created by calling one of the factory methods in 14 | the `Collider` class. The resulting collection can be used like any other 15 | unmodifiable Java collection--for instance, `ClojureList` implements the 16 | standard `java.util.List` interface. 17 | 18 | ```java 19 | List myStrings = clojureList("a", "b", "c"); 20 | assertEquals(3, myStrings.size()); 21 | assertEquals("a", myStrings.get(0)); 22 | ``` 23 | 24 | Collider collections can also be created by transforming another collection. 25 | The technique of persistent modification is used to efficiently create modified 26 | copies while leaving the immutable original untouched. 27 | 28 | ```java 29 | ClojureMap emptyMap = clojureMap(); 30 | 31 | ClojureMap assoc = emptyMap.assoc("key", "value"); 32 | assertTrue(assoc.containsKey("key")); 33 | assertEquals(assoc.size(), 1); 34 | assertEquals(assoc.get("key"), "value"); 35 | 36 | ClojureMap dissoc = assoc.dissoc("key"); 37 | assertEquals(dissoc, emptyMap); 38 | ``` 39 | 40 | Since Collider specifically targets Java 8, not only can its collections be 41 | used with the Stream API, but they also include some convenience methods to 42 | make common use cases (such as mapping a function over a list) more concise. 43 | 44 | ```java 45 | ClojureList evens = clojureList(0, 2, 4, 6, 8); // [0, 2, 4, 6, 8] 46 | ClojureList odds = evens.map(x -> x + 1); // [1, 3, 5, 7, 9] 47 | ``` 48 | 49 | Collider also provides collectors that work with the Stream API. 50 | 51 | ```java 52 | ClojureList singles = IntStream.range(0, 100).boxed().collect(toClojureList()); 53 | ``` 54 | 55 | Internally, these collectors use Clojure's [transient 56 | collections](http://clojure.org/transients) to efficiently accumulate a result. 57 | Transients are generally used as follows: 58 | 59 | 1. Obtain a transient version of a collection from a persistent collection in 60 | O(1) time 61 | 2. Mutate the transient collection 62 | 3. Turn the transient collection into a persistent collection in O(1) time 63 | 64 | Collider makes transients available directly. Continuing the above example: 65 | 66 | ```java 67 | TransientList tr = singles.asTransient(); 68 | range(100, 200).forEach(tr::append); 69 | ClojureList moreSingles = tr.toPersistent(); 70 | 71 | assertEquals(moreSingles.size(), 200); 72 | assertEquals(moreSingles, range(0, 200).boxed().collect(toClojureList())); 73 | ``` 74 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.rschmitt 8 | collider 9 | 1.0.0 10 | 11 | 12 | 13 | Ryan Schmitt 14 | rschmitt@pobox.com 15 | -8 16 | 17 | 18 | 19 | 20 | 21 | CC0 22 | http://creativecommons.org/publicdomain/zero/1.0/ 23 | 24 | 25 | 26 | 27 | org.sonatype.oss 28 | oss-parent 29 | 7 30 | 31 | 32 | 33 | 34 | org.clojure 35 | clojure 36 | [1.5.0,) 37 | compile 38 | 39 | 40 | org.junit.jupiter 41 | junit-jupiter-api 42 | 5.7.1 43 | test 44 | 45 | 46 | org.junit.jupiter 47 | junit-jupiter-engine 48 | 5.7.1 49 | test 50 | 51 | 52 | com.google.code.findbugs 53 | jsr305 54 | 2.0.1 55 | 56 | 57 | 58 | 59 | 60 | 61 | maven-compiler-plugin 62 | 3.3 63 | 64 | 1.8 65 | 1.8 66 | -Xlint:all 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-gpg-plugin 72 | 1.6 73 | 74 | 75 | sign-artifacts 76 | verify 77 | 78 | sign 79 | 80 | 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-source-plugin 86 | 2.4 87 | 88 | 89 | attach-sources 90 | 91 | jar 92 | 93 | 94 | 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-javadoc-plugin 99 | 2.10.3 100 | 101 | 102 | attach-javadocs 103 | 104 | jar 105 | 106 | 107 | 108 | 109 | 110 | org.codefx.maven.plugin 111 | jdeps-maven-plugin 112 | 0.1 113 | 114 | 115 | 116 | jdkinternals 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -Xdoclint:none 126 | 127 | 128 | -------------------------------------------------------------------------------- /push-javadoc-to-gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | if [ "$TRAVIS_REPO_SLUG" == "rschmitt/collider" ] && [ "$TRAVIS_JDK_VERSION" == "oraclejdk8" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then 6 | echo "Generating javadoc..." 7 | mvn javadoc:javadoc 8 | echo "Publishing javadoc..." 9 | 10 | cp -R target/site/apidocs $HOME/javadoc-latest 11 | 12 | cd $HOME 13 | git config --global user.email "travis@travis-ci.org" 14 | git config --global user.name "travis-ci" 15 | git clone --quiet --branch=gh-pages https://${GH_TOKEN}@github.com/rschmitt/collider gh-pages > /dev/null 16 | 17 | cd gh-pages 18 | git rm -rf ./javadoc 19 | cp -Rf $HOME/javadoc-latest ./javadoc 20 | git add -f . 21 | git commit -m "Lastest javadoc on successful travis build $TRAVIS_BUILD_NUMBER auto-pushed to gh-pages" 22 | git push -fq origin gh-pages > /dev/null 23 | 24 | echo "Published Javadoc to gh-pages." 25 | fi 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/collider/ClojureList.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.Comparator; 6 | import java.util.Iterator; 7 | import java.util.List; 8 | import java.util.ListIterator; 9 | import java.util.Spliterator; 10 | import java.util.function.Consumer; 11 | import java.util.function.Function; 12 | import java.util.function.Predicate; 13 | import java.util.function.UnaryOperator; 14 | import java.util.stream.Stream; 15 | 16 | import javax.annotation.concurrent.Immutable; 17 | 18 | import clojure.lang.IEditableCollection; 19 | import clojure.lang.IPersistentVector; 20 | import clojure.lang.ITransientCollection; 21 | import clojure.lang.ITransientVector; 22 | import clojure.lang.PersistentVector; 23 | 24 | import static com.github.rschmitt.collider.Collider.toClojureList; 25 | 26 | /** 27 | * A generic persistent immutable List implementation, with three types of methods: 28 | *
    29 | *
  1. Read methods from {@link List}, such as {@link #get}
  2. 30 | *
  3. Write methods from List, such as {@link #add}; these will throw {@link 31 | * UnsupportedOperationException}
  4. and have been marked as {@code @Deprecated} 32 | *
  5. Persistent "modification" methods, such as {@link #append}; these will efficiently create 33 | * modified copies of the current list
  6. 34 | *
35 | */ 36 | @Immutable 37 | public class ClojureList implements List { 38 | private final List delegate; 39 | 40 | @SuppressWarnings("unchecked") 41 | protected ClojureList(Object delegate) { 42 | this.delegate = (List) delegate; 43 | } 44 | 45 | @SafeVarargs 46 | static ClojureList create(T... ts) { 47 | return create(PersistentVector.create(ts)); 48 | } 49 | 50 | static ClojureList wrap(IPersistentVector vector) { 51 | return create(vector); 52 | } 53 | 54 | @SuppressWarnings("unchecked") 55 | private static ClojureList create(IPersistentVector ts) { 56 | return new ClojureList<>(ts); 57 | } 58 | 59 | public List unwrap() { 60 | return (List) delegate; 61 | } 62 | 63 | /** 64 | * Returns a copy of this list with {@code t} appended. 65 | */ 66 | public ClojureList append(T t) { 67 | return ClojureList.create(((IPersistentVector) delegate).cons(t)); 68 | } 69 | 70 | /** 71 | * Maps {@code f} over the elements in this list, returning a new list containing the result. 72 | */ 73 | public ClojureList map(Function f) { 74 | return stream().map(f).collect(toClojureList()); 75 | } 76 | 77 | /** 78 | * Returns a new list containing only the elements in this list matching {@code p}. 79 | */ 80 | public ClojureList filter(Predicate p) { 81 | return stream().filter(p).collect(toClojureList()); 82 | } 83 | 84 | /** 85 | * Returns a new list containing none of the elements in this list matching {@code p}. 86 | */ 87 | public ClojureList exclude(Predicate p) { 88 | return filter(p.negate()); 89 | } 90 | 91 | /** 92 | * Returns a mutable copy of this list. 93 | */ 94 | public List toMutableList() { 95 | return new ArrayList<>(this); 96 | } 97 | 98 | /** 99 | * Returns a transient version of this list in constant time. 100 | */ 101 | public TransientList asTransient() { 102 | IEditableCollection asEditable = (IEditableCollection) delegate; 103 | ITransientCollection asTransient = asEditable.asTransient(); 104 | return new TransientList<>((ITransientVector) asTransient); 105 | } 106 | 107 | @Override 108 | public ClojureList subList(int fromIndex, int toIndex) { 109 | return wrap((IPersistentVector) delegate.subList(fromIndex, toIndex)); 110 | } 111 | 112 | //////////////////////////////// 113 | // Mindless delegation goes here 114 | //////////////////////////////// 115 | 116 | @Override 117 | public int size() { 118 | return delegate.size(); 119 | } 120 | 121 | @Override 122 | public boolean isEmpty() { 123 | return delegate.isEmpty(); 124 | } 125 | 126 | @Override 127 | public boolean contains(Object o) { 128 | return delegate.contains(o); 129 | } 130 | 131 | @Override 132 | public Iterator iterator() { 133 | return delegate.iterator(); 134 | } 135 | 136 | @Override 137 | public Object[] toArray() { 138 | return delegate.toArray(); 139 | } 140 | 141 | @Override 142 | public T1[] toArray(T1[] a) { 143 | return delegate.toArray(a); 144 | } 145 | 146 | @Override 147 | public boolean containsAll(Collection c) { 148 | return delegate.containsAll(c); 149 | } 150 | 151 | @Override 152 | public boolean equals(Object o) { 153 | return delegate.equals(o); 154 | } 155 | 156 | @Override 157 | public int hashCode() { 158 | return delegate.hashCode(); 159 | } 160 | 161 | @Override 162 | public String toString() { 163 | return delegate.toString(); 164 | } 165 | 166 | @Override 167 | public T get(int index) { 168 | return delegate.get(index); 169 | } 170 | 171 | @Override 172 | public int indexOf(Object o) { 173 | return delegate.indexOf(o); 174 | } 175 | 176 | @Override 177 | public int lastIndexOf(Object o) { 178 | return delegate.lastIndexOf(o); 179 | } 180 | 181 | @Override 182 | public ListIterator listIterator() { 183 | return delegate.listIterator(); 184 | } 185 | 186 | @Override 187 | public ListIterator listIterator(int index) { 188 | return delegate.listIterator(index); 189 | } 190 | 191 | @Override 192 | public Spliterator spliterator() { 193 | return delegate.spliterator(); 194 | } 195 | 196 | @Override 197 | public Stream stream() { 198 | return delegate.stream(); 199 | } 200 | 201 | @Override 202 | public Stream parallelStream() { 203 | return delegate.parallelStream(); 204 | } 205 | 206 | @Override 207 | public void forEach(Consumer action) { 208 | delegate.forEach(action); 209 | } 210 | 211 | /** 212 | * @deprecated This operation will fail; use {@link #exclude} instead 213 | */ 214 | @Override 215 | @Deprecated 216 | public boolean removeIf(Predicate filter) { 217 | return delegate.removeIf(filter); 218 | } 219 | 220 | /** 221 | * @deprecated This operation will fail. 222 | */ 223 | @Override 224 | @Deprecated 225 | public boolean remove(Object o) { 226 | return delegate.remove(o); 227 | } 228 | 229 | /** 230 | * @deprecated This operation will fail; use {@link #append} instead 231 | */ 232 | @Override 233 | @Deprecated 234 | public boolean add(T t) { 235 | return delegate.add(t); 236 | } 237 | 238 | /** 239 | * @deprecated This operation will fail; use {@link #append} instead 240 | */ 241 | @Override 242 | @Deprecated 243 | public boolean addAll(Collection c) { 244 | return delegate.addAll(c); 245 | } 246 | 247 | /** 248 | * @deprecated This operation will fail; use {@link #append} instead 249 | */ 250 | @Override 251 | @Deprecated 252 | public boolean addAll(int index, Collection c) { 253 | return delegate.addAll(index, c); 254 | } 255 | 256 | /** 257 | * @deprecated This operation will fail; use {@link #exclude} instead 258 | */ 259 | @Override 260 | @Deprecated 261 | public boolean removeAll(Collection c) { 262 | return delegate.removeAll(c); 263 | } 264 | 265 | /** 266 | * @deprecated This operation will fail; use {@link #filter} instead 267 | */ 268 | @Override 269 | @Deprecated 270 | public boolean retainAll(Collection c) { 271 | return delegate.retainAll(c); 272 | } 273 | 274 | /** 275 | * @deprecated This operation will fail; use {@link #map} instead 276 | */ 277 | @Override 278 | @Deprecated 279 | public void replaceAll(UnaryOperator operator) { 280 | delegate.replaceAll(operator); 281 | } 282 | 283 | /** 284 | * @deprecated This operation will fail. 285 | */ 286 | @Override 287 | @Deprecated 288 | public void sort(Comparator c) { 289 | delegate.sort(c); 290 | } 291 | 292 | /** 293 | * @deprecated This operation will fail; use {@link Collider#clojureList(Object[])} instead 294 | */ 295 | @Override 296 | @Deprecated 297 | public void clear() { 298 | delegate.clear(); 299 | } 300 | 301 | /** 302 | * @deprecated This operation will fail. 303 | */ 304 | @Override 305 | @Deprecated 306 | public T set(int index, T element) { 307 | return delegate.set(index, element); 308 | } 309 | 310 | /** 311 | * @deprecated This operation will fail. 312 | */ 313 | @Override 314 | @Deprecated 315 | public void add(int index, T element) { 316 | delegate.add(index, element); 317 | } 318 | 319 | /** 320 | * @deprecated This operation will fail. 321 | */ 322 | @Override 323 | @Deprecated 324 | public T remove(int index) { 325 | return delegate.remove(index); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/collider/ClojureMap.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import java.util.Collection; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import java.util.function.BiConsumer; 8 | import java.util.function.BiFunction; 9 | import java.util.function.Function; 10 | import java.util.function.Predicate; 11 | import java.util.stream.Stream; 12 | 13 | import javax.annotation.concurrent.Immutable; 14 | 15 | import clojure.lang.Associative; 16 | import clojure.lang.IEditableCollection; 17 | import clojure.lang.IPersistentMap; 18 | import clojure.lang.ITransientCollection; 19 | import clojure.lang.ITransientMap; 20 | import clojure.lang.PersistentHashMap; 21 | import clojure.lang.RT; 22 | 23 | import static com.github.rschmitt.collider.Collider.toClojureMap; 24 | 25 | /** 26 | * A generic persistent immutable Map implementation, with three types of methods: 27 | *
    28 | *
  1. Read methods from {@link Map}, such as {@link #get}
  2. 29 | *
  3. Write methods from Map, such as {@link #put}; these will throw {@link 30 | * UnsupportedOperationException}
  4. and have been marked as {@code @Deprecated} 31 | *
  5. Persistent "modification" methods, such as {@link #assoc}; these will efficiently create 32 | * modified copies of the current map
  6. 33 | *
34 | */ 35 | @Immutable 36 | public class ClojureMap implements Map { 37 | private final Map delegate; 38 | 39 | @SuppressWarnings("unchecked") 40 | static ClojureMap create(Object... init) { 41 | return (ClojureMap) create(PersistentHashMap.create(init)); 42 | } 43 | 44 | @SuppressWarnings("unchecked") 45 | private static ClojureMap create(Map ts) { 46 | return new ClojureMap<>(ts); 47 | } 48 | 49 | @SuppressWarnings("unchecked") 50 | static ClojureMap wrap(IPersistentMap map) { 51 | return create((Map) map); 52 | } 53 | 54 | @SuppressWarnings("unchecked") 55 | protected ClojureMap(Object delegate) { 56 | this.delegate = (Map) delegate; 57 | } 58 | 59 | public Map unwrap() { 60 | return (Map) delegate; 61 | } 62 | 63 | /** 64 | * Returns a copy of this map which also contains a mapping from {@code key} to {@code value}. 65 | * If a mapping for {@code key} already exists in the current map, it will be overwritten. 66 | */ 67 | @SuppressWarnings("unchecked") 68 | public ClojureMap assoc(K key, V value) { 69 | Associative assoc = RT.assoc(delegate, key, value); 70 | return ClojureMap.create((Map) assoc); 71 | } 72 | 73 | /** 74 | * Returns a copy of this map without a mapping for {@code key}. 75 | */ 76 | @SuppressWarnings("unchecked") 77 | public ClojureMap dissoc(K key) { 78 | Object dissoc = RT.dissoc(delegate, key); 79 | return ClojureMap.create((Map) dissoc); 80 | } 81 | 82 | /** 83 | * Returns a map that consists of all bindings from the current map, as well as {@code maps}. 84 | * If a mapping occurs in more than one map, the mapping in the rightmost map will take 85 | * precedence. 86 | */ 87 | @SafeVarargs 88 | public final ClojureMap merge(ClojureMap... maps) { 89 | if (maps.length == 0) return this; 90 | if (Stream.of(maps).allMatch(Map::isEmpty)) return this; 91 | if (isEmpty() && maps.length == 1) return maps[0]; 92 | TransientMap ret = asTransient(); 93 | for (ClojureMap map : maps) { 94 | for (Entry entry : map.entrySet()) { 95 | ret.put(entry.getKey(), entry.getValue()); 96 | } 97 | } 98 | return ret.toPersistent(); 99 | } 100 | 101 | /** 102 | * Returns a mutable copy of this map. 103 | */ 104 | public Map toMutableMap() { 105 | return new HashMap<>(this); 106 | } 107 | 108 | /** 109 | * Returns a transient version of this map in constant time. 110 | */ 111 | public TransientMap asTransient() { 112 | IEditableCollection asEditable = (IEditableCollection) delegate; 113 | ITransientCollection asTransient = asEditable.asTransient(); 114 | return new TransientMap<>((ITransientMap) asTransient); 115 | } 116 | 117 | /** 118 | * Maps {@code f} over the keys in this map, returning a new map containing the result. If 119 | * {@code f} produces collisions, the result is undefined. 120 | */ 121 | public ClojureMap mapKeys(Function f) { 122 | return entrySet().stream().collect(toClojureMap(e -> f.apply(e.getKey()), Entry::getValue)); 123 | } 124 | 125 | /** 126 | * Maps {@code f} over the values in this map, returning a new map containing the result. 127 | */ 128 | public ClojureMap mapValues(Function f) { 129 | return entrySet().stream().collect(toClojureMap(Entry::getKey, e -> f.apply(e.getValue()))); 130 | } 131 | 132 | /** 133 | * Returns a new map containing only the mappings whose keys match {@code p}. 134 | */ 135 | public ClojureMap filterKeys(Predicate p) { 136 | return entrySet().stream().filter(e -> p.test(e.getKey())).collect(toClojureMap(Entry::getKey, Entry::getValue)); 137 | } 138 | 139 | /** 140 | * Returns a new map containing only the mappings whose values match {@code p}. 141 | */ 142 | public ClojureMap filterValues(Predicate p) { 143 | return entrySet().stream().filter(e -> p.test(e.getValue())).collect(toClojureMap(Entry::getKey, Entry::getValue)); 144 | } 145 | 146 | /** 147 | * Returns a new map containing none of the mappings whose keys match {@code p}. 148 | */ 149 | public ClojureMap excludeKeys(Predicate p) { 150 | return filterKeys(p.negate()); 151 | } 152 | 153 | /** 154 | * Returns a new map containing none of the mappings whose values match {@code p}. 155 | */ 156 | public ClojureMap excludeValues(Predicate p) { 157 | return filterValues(p.negate()); 158 | } 159 | 160 | //////////////////////////////// 161 | // Mindless delegation goes here 162 | //////////////////////////////// 163 | 164 | @Override 165 | public int size() { 166 | return delegate.size(); 167 | } 168 | 169 | @Override 170 | public boolean isEmpty() { 171 | return delegate.isEmpty(); 172 | } 173 | 174 | @Override 175 | public boolean containsKey(Object key) { 176 | return delegate.containsKey(key); 177 | } 178 | 179 | @Override 180 | public boolean containsValue(Object value) { 181 | return delegate.containsValue(value); 182 | } 183 | 184 | @Override 185 | public V get(Object key) { 186 | return delegate.get(key); 187 | } 188 | 189 | @Override 190 | public Set keySet() { 191 | return delegate.keySet(); 192 | } 193 | 194 | @Override 195 | public Collection values() { 196 | return delegate.values(); 197 | } 198 | 199 | @Override 200 | public Set> entrySet() { 201 | return delegate.entrySet(); 202 | } 203 | 204 | @Override 205 | public boolean equals(Object o) { 206 | return delegate.equals(o); 207 | } 208 | 209 | @Override 210 | public int hashCode() { 211 | return delegate.hashCode(); 212 | } 213 | 214 | @Override 215 | public String toString() { 216 | return delegate.toString(); 217 | } 218 | 219 | @Override 220 | public V getOrDefault(Object key, V defaultValue) { 221 | return delegate.getOrDefault(key, defaultValue); 222 | } 223 | 224 | @Override 225 | public void forEach(BiConsumer action) { 226 | delegate.forEach(action); 227 | } 228 | 229 | /** 230 | * @deprecated This operation will fail; use {@link #assoc} instead 231 | */ 232 | @Override 233 | @Deprecated 234 | public V put(K key, V value) { 235 | return delegate.put(key, value); 236 | } 237 | 238 | /** 239 | * @deprecated This operation will fail; use {@link #dissoc} instead 240 | */ 241 | @Override 242 | @Deprecated 243 | public V remove(Object key) { 244 | return delegate.remove(key); 245 | } 246 | 247 | /** 248 | * @deprecated This operation will fail; use {@link #merge(ClojureMap[])} instead 249 | */ 250 | @Override 251 | @Deprecated 252 | public void putAll(Map m) { 253 | delegate.putAll(m); 254 | } 255 | 256 | /** 257 | * @deprecated This operation will fail; use {@link Collider#clojureMap()} instead 258 | */ 259 | @Override 260 | @Deprecated 261 | public void clear() { 262 | delegate.clear(); 263 | } 264 | 265 | /** 266 | * @deprecated This operation will fail; use {@link #mapValues} instead 267 | */ 268 | @Override 269 | @Deprecated 270 | public void replaceAll(BiFunction function) { 271 | delegate.replaceAll(function); 272 | } 273 | 274 | /** 275 | * @deprecated This operation will fail. 276 | */ 277 | @Override 278 | @Deprecated 279 | public V putIfAbsent(K key, V value) { 280 | return delegate.putIfAbsent(key, value); 281 | } 282 | 283 | /** 284 | * @deprecated This operation will fail; use {@link #dissoc} instead 285 | */ 286 | @Override 287 | @Deprecated 288 | public boolean remove(Object key, Object value) { 289 | return delegate.remove(key, value); 290 | } 291 | 292 | /** 293 | * @deprecated This operation will fail; use {@link #assoc} instead 294 | */ 295 | @Override 296 | @Deprecated 297 | public boolean replace(K key, V oldValue, V newValue) { 298 | return delegate.replace(key, oldValue, newValue); 299 | } 300 | 301 | /** 302 | * @deprecated This operation will fail; use {@link #assoc} instead 303 | */ 304 | @Override 305 | @Deprecated 306 | public V replace(K key, V value) { 307 | return delegate.replace(key, value); 308 | } 309 | 310 | /** 311 | * @deprecated This operation will fail. 312 | */ 313 | @Override 314 | @Deprecated 315 | public V computeIfAbsent(K key, Function mappingFunction) { 316 | return delegate.computeIfAbsent(key, mappingFunction); 317 | } 318 | 319 | /** 320 | * @deprecated This operation will fail. 321 | */ 322 | @Override 323 | @Deprecated 324 | public V computeIfPresent(K key, BiFunction remappingFunction) { 325 | return delegate.computeIfPresent(key, remappingFunction); 326 | } 327 | 328 | /** 329 | * @deprecated This operation will fail. 330 | */ 331 | @Override 332 | @Deprecated 333 | public V compute(K key, BiFunction remappingFunction) { 334 | return delegate.compute(key, remappingFunction); 335 | } 336 | 337 | /** 338 | * @deprecated This operation will fail. 339 | */ 340 | @Override 341 | @Deprecated 342 | public V merge(K key, V value, BiFunction remappingFunction) { 343 | return delegate.merge(key, value, remappingFunction); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/collider/ClojureSet.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import java.util.Collection; 4 | import java.util.HashSet; 5 | import java.util.Iterator; 6 | import java.util.Set; 7 | import java.util.Spliterator; 8 | import java.util.function.Consumer; 9 | import java.util.function.Function; 10 | import java.util.function.Predicate; 11 | import java.util.stream.Stream; 12 | 13 | import javax.annotation.concurrent.Immutable; 14 | 15 | import clojure.lang.IEditableCollection; 16 | import clojure.lang.IPersistentSet; 17 | import clojure.lang.ITransientCollection; 18 | import clojure.lang.ITransientSet; 19 | import clojure.lang.PersistentHashSet; 20 | 21 | import static com.github.rschmitt.collider.Collider.toClojureSet; 22 | 23 | /** 24 | * A generic persistent immutable Set implementation, with three types of methods: 25 | *
    26 | *
  1. Read methods from {@link Set}, such as {@link #contains}
  2. 27 | *
  3. Write methods from Set, such as {@link #add}; these will throw {@link 28 | * UnsupportedOperationException}
  4. and have been marked as {@code @Deprecated} 29 | *
  5. Persistent "modification" methods, such as {@link #with}; these will efficiently create 30 | * modified copies of the current set
  6. 31 | *
32 | */ 33 | @Immutable 34 | public class ClojureSet implements Set { 35 | private final Set delegate; 36 | 37 | @SuppressWarnings("unchecked") 38 | protected ClojureSet(Object delegate) { 39 | this.delegate = (Set) delegate; 40 | } 41 | 42 | @SafeVarargs 43 | static ClojureSet create(T... ts) { 44 | return create(PersistentHashSet.create(ts)); 45 | } 46 | 47 | static ClojureSet wrap(IPersistentSet clojureSet) { 48 | return create(clojureSet); 49 | } 50 | 51 | @SuppressWarnings("unchecked") 52 | private static ClojureSet create(IPersistentSet ts) { 53 | return new ClojureSet<>(ts); 54 | } 55 | 56 | public Set unwrap() { 57 | return (Set) delegate; 58 | } 59 | 60 | /** 61 | * Returns a copy of this set that includes {@code t}. 62 | */ 63 | public ClojureSet with(T t) { 64 | return ClojureSet.create((IPersistentSet) ((IPersistentSet) delegate).cons(t)); 65 | } 66 | 67 | /** 68 | * Returns a copy of this set that does not include {@code t}. 69 | */ 70 | public ClojureSet without(T t) { 71 | return wrap(((IPersistentSet) delegate).disjoin(t)); 72 | } 73 | 74 | /** 75 | * Maps {@code f} over the elements in this set, returning a new set containing the result. 76 | */ 77 | public ClojureSet map(Function f) { 78 | return stream().map(f).collect(toClojureSet()); 79 | } 80 | 81 | /** 82 | * Returns a new set containing only the elements in this set matching {@code p}. 83 | */ 84 | public ClojureSet filter(Predicate p) { 85 | return stream().filter(p).collect(toClojureSet()); 86 | } 87 | 88 | /** 89 | * Returns a new set containing none of the elements in this set matching {@code p}. 90 | */ 91 | public ClojureSet exclude(Predicate p) { 92 | return filter(p.negate()); 93 | } 94 | 95 | /** 96 | * Returns a mutable copy of this set. 97 | */ 98 | public Set toMutableSet() { 99 | return new HashSet<>(this); 100 | } 101 | 102 | /** 103 | * Returns a transient version of this set in constant time. 104 | */ 105 | public TransientSet asTransient() { 106 | IEditableCollection asEditable = (IEditableCollection) delegate; 107 | ITransientCollection asTransient = asEditable.asTransient(); 108 | return new TransientSet<>((ITransientSet) asTransient); 109 | } 110 | 111 | //////////////////////////////// 112 | // Mindless delegation goes here 113 | //////////////////////////////// 114 | 115 | @Override 116 | public int size() { 117 | return delegate.size(); 118 | } 119 | 120 | @Override 121 | public boolean isEmpty() { 122 | return delegate.isEmpty(); 123 | } 124 | 125 | @Override 126 | public boolean contains(Object o) { 127 | return delegate.contains(o); 128 | } 129 | 130 | @Override 131 | public Iterator iterator() { 132 | return delegate.iterator(); 133 | } 134 | 135 | @Override 136 | public Object[] toArray() { 137 | return delegate.toArray(); 138 | } 139 | 140 | @Override 141 | public T1[] toArray(T1[] a) { 142 | return delegate.toArray(a); 143 | } 144 | 145 | @Override 146 | public boolean containsAll(Collection c) { 147 | return delegate.containsAll(c); 148 | } 149 | 150 | @Override 151 | public boolean equals(Object o) { 152 | return delegate.equals(o); 153 | } 154 | 155 | @Override 156 | public int hashCode() { 157 | return delegate.hashCode(); 158 | } 159 | 160 | @Override 161 | public String toString() { 162 | return delegate.toString(); 163 | } 164 | 165 | @Override 166 | public Spliterator spliterator() { 167 | return delegate.spliterator(); 168 | } 169 | 170 | @Override 171 | public Stream stream() { 172 | return delegate.stream(); 173 | } 174 | 175 | @Override 176 | public Stream parallelStream() { 177 | return delegate.parallelStream(); 178 | } 179 | 180 | @Override 181 | public void forEach(Consumer action) { 182 | delegate.forEach(action); 183 | } 184 | 185 | /** 186 | * @deprecated This operation will fail; use {@link #with} instead 187 | */ 188 | @Override 189 | @Deprecated 190 | public boolean add(T t) { 191 | return delegate.add(t); 192 | } 193 | 194 | /** 195 | * @deprecated This operation will fail; use {@link #without} instead 196 | */ 197 | @Override 198 | @Deprecated 199 | public boolean remove(Object o) { 200 | return delegate.remove(o); 201 | } 202 | 203 | /** 204 | * @deprecated This operation will fail; use {@link #with} instead 205 | */ 206 | @Override 207 | @Deprecated 208 | public boolean addAll(Collection c) { 209 | return delegate.addAll(c); 210 | } 211 | 212 | /** 213 | * @deprecated This operation will fail; use {@link #filter} instead 214 | */ 215 | @Override 216 | @Deprecated 217 | public boolean retainAll(Collection c) { 218 | return delegate.retainAll(c); 219 | } 220 | 221 | /** 222 | * @deprecated This operation will fail; use {@link #exclude} instead 223 | */ 224 | @Override 225 | @Deprecated 226 | public boolean removeAll(Collection c) { 227 | return delegate.removeAll(c); 228 | } 229 | 230 | /** 231 | * @deprecated This operation will fail; use {@link Collider#clojureSet(Object[])} instead 232 | */ 233 | @Override 234 | @Deprecated 235 | public void clear() { 236 | delegate.clear(); 237 | } 238 | 239 | /** 240 | * @deprecated This operation will fail; use {@link #exclude} instead 241 | */ 242 | @Override 243 | @Deprecated 244 | public boolean removeIf(Predicate filter) { 245 | return delegate.removeIf(filter); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/collider/Collider.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import java.util.Collections; 4 | import java.util.EnumSet; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | import java.util.Set; 9 | import java.util.function.BiConsumer; 10 | import java.util.function.BinaryOperator; 11 | import java.util.function.Function; 12 | import java.util.function.Supplier; 13 | import java.util.stream.Collector; 14 | 15 | import clojure.lang.IPersistentMap; 16 | import clojure.lang.IPersistentSet; 17 | import clojure.lang.IPersistentVector; 18 | 19 | import static java.util.stream.Collector.Characteristics.UNORDERED; 20 | 21 | /** 22 | * A collection of factory methods to create immutable collections. These methods are designed to be 23 | * imported statically, either individually or with a star import. 24 | */ 25 | public class Collider { 26 | private Collider() { 27 | } 28 | 29 | public static ClojureMap clojureMap() { 30 | return ClojureMap.create(); 31 | } 32 | 33 | public static ClojureMap clojureMap(K key, V value) { 34 | return ClojureMap.create(key, value); 35 | } 36 | 37 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2) { 38 | return ClojureMap.create(key1, val1, key2, val2); 39 | } 40 | 41 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3) { 42 | return ClojureMap.create(key1, val1, key2, val2, key3, val3); 43 | } 44 | 45 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3, K key4, V val4) { 46 | return ClojureMap.create(key1, val1, key2, val2, key3, val3, key4, val4); 47 | } 48 | 49 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3, K key4, V val4, K key5, V val5) { 50 | return ClojureMap.create(key1, val1, key2, val2, key3, val3, key4, val4, key5, val5); 51 | } 52 | 53 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3, K key4, V val4, K key5, V val5, K key6, V val6) { 54 | return ClojureMap.create(key1, val1, key2, val2, key3, val3, key4, val4, key5, val5, key6, val6); 55 | } 56 | 57 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3, K key4, V val4, K key5, V val5, K key6, V val6, K key7, V val7) { 58 | return ClojureMap.create(key1, val1, key2, val2, key3, val3, key4, val4, key5, val5, key6, val6, key7, val7); 59 | } 60 | 61 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3, K key4, V val4, K key5, V val5, K key6, V val6, K key7, V val7, K key8, V val8) { 62 | return ClojureMap.create(key1, val1, key2, val2, key3, val3, key4, val4, key5, val5, key6, val6, key7, val7, key8, val8); 63 | } 64 | 65 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3, K key4, V val4, K key5, V val5, K key6, V val6, K key7, V val7, K key8, V val8, K key9, V val9) { 66 | return ClojureMap.create(key1, val1, key2, val2, key3, val3, key4, val4, key5, val5, key6, val6, key7, val7, key8, val8, key9, val9); 67 | } 68 | 69 | public static ClojureMap clojureMap(K key1, V val1, K key2, V val2, K key3, V val3, K key4, V val4, K key5, V val5, K key6, V val6, K key7, V val7, K key8, V val8, K key9, V val9, K key10, V val10) { 70 | return ClojureMap.create(key1, val1, key2, val2, key3, val3, key4, val4, key5, val5, key6, val6, key7, val7, key8, val8, key9, val9, key10, val10); 71 | } 72 | 73 | @SafeVarargs 74 | public static ClojureList clojureList(T... elements) { 75 | if (elements == null) return ClojureList.create((T) null); 76 | return ClojureList.create(elements); 77 | } 78 | 79 | @SafeVarargs 80 | public static ClojureSet clojureSet(T... elements) { 81 | if (elements == null) return ClojureSet.create((T) null); 82 | return ClojureSet.create(elements); 83 | } 84 | 85 | public static TransientMap transientMap() { 86 | ClojureMap emptyMap = clojureMap(); 87 | return emptyMap.asTransient(); 88 | } 89 | 90 | public static TransientList transientList() { 91 | ClojureList emptyList = clojureList(); 92 | return emptyList.asTransient(); 93 | } 94 | 95 | public static TransientSet transientSet() { 96 | ClojureSet emptySet = clojureSet(); 97 | return emptySet.asTransient(); 98 | } 99 | 100 | @SuppressWarnings("unchecked") 101 | public static ClojureMap intoClojureMap(Map map) { 102 | if (map instanceof ClojureMap) return (ClojureMap) map; 103 | if (map instanceof IPersistentMap) return ClojureMap.wrap((IPersistentMap) map); 104 | return map.entrySet().stream().collect(toClojureMap(Entry::getKey, Entry::getValue)); 105 | } 106 | 107 | @SuppressWarnings("unchecked") 108 | public static ClojureList intoClojureList(List list) { 109 | if (list instanceof ClojureList) return (ClojureList) list; 110 | if (list instanceof IPersistentVector) return (ClojureList) ClojureList.wrap((IPersistentVector) list); 111 | 112 | // Work around an inference bug in some older JDKs 113 | Collector, ClojureList> collector = toClojureList(); 114 | 115 | return list.stream().collect(collector); 116 | } 117 | 118 | @SuppressWarnings("unchecked") 119 | public static ClojureSet intoClojureSet(Set set) { 120 | if (set instanceof ClojureSet) return (ClojureSet) set; 121 | if (set instanceof IPersistentSet) return (ClojureSet) ClojureSet.wrap((IPersistentSet) set); 122 | 123 | // Work around an inference bug in some older JDKs 124 | Collector, ClojureSet> collector = toClojureSet(); 125 | 126 | return set.stream().collect(collector); 127 | } 128 | 129 | /** 130 | * Returns a {@link Collector} that efficiently accumulates values into a ClojureMap. If 131 | * multiple mappings are produced for the same key, the last mapping produced will be the one in 132 | * the returned map. 133 | * 134 | * @param keyMapper a function from the input type to keys 135 | * @param valueMapper a function from the input type to values 136 | * @param the type of the input element in the stream 137 | * @param the key type for the map that will be returned 138 | * @param the value type for the map that will be returned 139 | */ 140 | public static Collector, ClojureMap> toClojureMap( 141 | Function keyMapper, 142 | Function valueMapper 143 | ) { 144 | return new Collector, ClojureMap>() { 145 | @Override 146 | public Supplier> supplier() { 147 | return TransientMap::new; 148 | } 149 | 150 | @Override 151 | public BiConsumer, T> accumulator() { 152 | return (map, t) -> map.put(keyMapper.apply(t), valueMapper.apply(t)); 153 | } 154 | 155 | @Override 156 | public BinaryOperator> combiner() { 157 | return (x, y) -> { 158 | x.putAll(y.toPersistent()); 159 | return x; 160 | }; 161 | } 162 | 163 | @Override 164 | public Function, ClojureMap> finisher() { 165 | return TransientMap::toPersistent; 166 | } 167 | 168 | @Override 169 | public Set characteristics() { 170 | return EnumSet.of(UNORDERED); 171 | } 172 | }; 173 | } 174 | 175 | /** 176 | * Returns a {@link Collector} that efficiently accumulates values into a ClojureMap while 177 | * detecting collisions. If multiple mappings are produced for the same key, the {@code 178 | * mergeFunction} will be invoked to determine which value to use. 179 | * 180 | * @param keyMapper a function from the input type to keys 181 | * @param valueMapper a function from the input type to values 182 | * @param mergeFunction a function used to resolve collisions between values associated with the 183 | * same key 184 | * @param the type of the input element in the stream 185 | * @param the key type for the map that will be returned 186 | * @param the value type for the map that will be returned 187 | */ 188 | public static Collector, ClojureMap> toStrictClojureMap( 189 | Function keyMapper, 190 | Function valueMapper, 191 | BinaryOperator mergeFunction 192 | ) { 193 | return new Collector, ClojureMap>() { 194 | @Override 195 | public Supplier> supplier() { 196 | return TransientMap::new; 197 | } 198 | 199 | @Override 200 | public BiConsumer, T> accumulator() { 201 | return (map, t) -> putUnique(map, keyMapper.apply(t), valueMapper.apply(t)); 202 | } 203 | 204 | @Override 205 | public BinaryOperator> combiner() { 206 | return (x, y) -> { 207 | ClojureMap source = y.toPersistent(); 208 | for (Entry entry : source.entrySet()) { 209 | putUnique(x, entry.getKey(), entry.getValue()); 210 | } 211 | return x; 212 | }; 213 | } 214 | 215 | private void putUnique(TransientMap map, K key, V value) { 216 | if (map.contains(key)) { 217 | value = mergeFunction.apply(value, map.get(key)); 218 | } 219 | map.put(key, value); 220 | } 221 | 222 | @Override 223 | public Function, ClojureMap> finisher() { 224 | return TransientMap::toPersistent; 225 | } 226 | 227 | @Override 228 | public Set characteristics() { 229 | return Collections.emptySet(); 230 | } 231 | }; 232 | } 233 | 234 | /** 235 | * Returns a {@link Collector} that efficiently accumulates values into a TransientList. 236 | * 237 | * @param the type of the input element in the stream 238 | */ 239 | public static Collector, ClojureList> toClojureList() { 240 | return new Collector, ClojureList>() { 241 | @Override 242 | public Supplier> supplier() { 243 | return TransientList::new; 244 | } 245 | 246 | @Override 247 | public BiConsumer, T> accumulator() { 248 | return TransientList::append; 249 | } 250 | 251 | @Override 252 | public BinaryOperator> combiner() { 253 | return (a, b) -> { 254 | a.appendAll(b.toPersistent()); 255 | return a; 256 | }; 257 | } 258 | 259 | @Override 260 | public Function, ClojureList> finisher() { 261 | return TransientList::toPersistent; 262 | } 263 | 264 | @Override 265 | public Set characteristics() { 266 | return Collections.emptySet(); 267 | } 268 | }; 269 | } 270 | 271 | /** 272 | * Returns a {@link Collector} that efficiently accumulates values into a ClojureSet. 273 | * 274 | * @param the type of the input element in the stream 275 | */ 276 | public static Collector, ClojureSet> toClojureSet() { 277 | return new Collector, ClojureSet>() { 278 | @Override 279 | public Supplier> supplier() { 280 | return TransientSet::new; 281 | } 282 | 283 | @Override 284 | public BiConsumer, T> accumulator() { 285 | return TransientSet::add; 286 | } 287 | 288 | @Override 289 | public BinaryOperator> combiner() { 290 | return (a, b) -> { 291 | a.addAll(b.toPersistent()); 292 | return a; 293 | }; 294 | } 295 | 296 | @Override 297 | public Function, ClojureSet> finisher() { 298 | return TransientSet::toPersistent; 299 | } 300 | 301 | @Override 302 | public Set characteristics() { 303 | return EnumSet.of(UNORDERED); 304 | } 305 | }; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/collider/TransientList.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import java.util.Collection; 4 | 5 | import javax.annotation.concurrent.NotThreadSafe; 6 | 7 | import clojure.lang.IPersistentCollection; 8 | import clojure.lang.IPersistentVector; 9 | import clojure.lang.ITransientVector; 10 | import clojure.lang.PersistentVector; 11 | 12 | /** 13 | * A list that can be modified in-place and then converted to a {@link ClojureList} in O(1) time. 14 | *

15 | * Instances of this class are not thread-safe; it is recommended that this class be used in a 16 | * thread-local fashion. In Clojure 1.7.0-RC1 and later, it is permitted to use this class from 17 | * multiple threads, and this is safe as long as access is correctly synchronized. In older versions 18 | * of Clojure, thread-local usage is enforced, and transients can only be modified by their owning 19 | * thread. 20 | */ 21 | @NotThreadSafe 22 | public class TransientList { 23 | private volatile ITransientVector delegate; 24 | 25 | TransientList() { 26 | this.delegate = PersistentVector.EMPTY.asTransient(); 27 | } 28 | 29 | TransientList(ITransientVector delegate) { 30 | this.delegate = delegate; 31 | } 32 | 33 | /** 34 | * Add {@code t} to the end of this list. 35 | */ 36 | public void append(T t) { 37 | this.delegate = (ITransientVector) delegate.conj(t); 38 | } 39 | 40 | /** 41 | * Add all elements in {@code c} to the end of this list. Elements will be added in the 42 | * iteration order of their collection. 43 | */ 44 | public void appendAll(Collection c) { 45 | for (T t : c) { 46 | append(t); 47 | } 48 | } 49 | 50 | /** 51 | * Returns the element currently at position {@code index} in this list. 52 | * 53 | * @throws IndexOutOfBoundsException if {@code index} is out of bounds (index < 0 || index >= size()) 54 | */ 55 | public void set(int index, T t) { 56 | delegate.assocN(index, t); 57 | } 58 | 59 | /** 60 | * Returns the element currently at position {@code index} in this list. 61 | * 62 | * @throws IndexOutOfBoundsException if {@code index} is out of bounds (index < 0 || index >= size()) 63 | */ 64 | public T get(int index) { 65 | return (T) delegate.nth(index); 66 | } 67 | 68 | /** 69 | * Returns the number of elements currently in this list. 70 | */ 71 | public int size() { 72 | return delegate.count(); 73 | } 74 | 75 | /** 76 | * Returns a persistent immutable version of this TransientList. This operation is performed in 77 | * constant time. Note that after this method is called, this transient instance will no longer 78 | * be usable and attempts to modify it will fail. 79 | */ 80 | public ClojureList toPersistent() { 81 | IPersistentCollection persistent = delegate.persistent(); 82 | IPersistentVector asVector = (IPersistentVector) persistent; 83 | return ClojureList.wrap(asVector); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/collider/TransientMap.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import java.util.Map; 4 | 5 | import javax.annotation.concurrent.NotThreadSafe; 6 | 7 | import clojure.lang.ITransientMap; 8 | import clojure.lang.PersistentArrayMap; 9 | 10 | /** 11 | * A map that can be modified in-place and then converted to a {@link ClojureMap} in O(1) time. 12 | *

13 | * Instances of this class are not thread-safe; it is recommended that this class be used in a 14 | * thread-local fashion. In Clojure 1.7.0-RC1 and later, it is permitted to use this class from 15 | * multiple threads, and this is safe as long as access is correctly synchronized. In older 16 | * versions of Clojure, thread-local usage is enforced, and transients can only be modified by their 17 | * owning thread. 18 | */ 19 | @NotThreadSafe 20 | public class TransientMap { 21 | private volatile ITransientMap delegate; 22 | 23 | TransientMap() { 24 | this.delegate = PersistentArrayMap.EMPTY.asTransient(); 25 | } 26 | 27 | TransientMap(ITransientMap delegate) { 28 | this.delegate = delegate; 29 | } 30 | 31 | /** 32 | * Add a binding from {@code key} to {@code value} to this map, overwriting any existing 33 | * binding for {@code key}. 34 | */ 35 | public void put(K key, V value) { 36 | delegate = delegate.assoc(key, value); 37 | } 38 | 39 | /** 40 | * Copy all bindings from {@code map} into this map. 41 | */ 42 | public void putAll(Map map) { 43 | for (Map.Entry entry : map.entrySet()) { 44 | put(entry.getKey(), entry.getValue()); 45 | } 46 | } 47 | 48 | /** 49 | * Removes {@code key} from this map. If {@code key} is not present, this operation does nothing. 50 | */ 51 | public void remove(K key) { 52 | delegate = delegate.without(key); 53 | } 54 | 55 | /** 56 | * Returns the value currently associated with {@code key} in this map, or {@code null} if none 57 | * exists. 58 | */ 59 | @SuppressWarnings("unchecked") 60 | public V get(K key) { 61 | return (V) delegate.valAt(key); 62 | } 63 | 64 | /** 65 | * Returns whether there is currently an entry for {@code key} in this map. 66 | */ 67 | public boolean contains(K key) { 68 | Object NOT_FOUND = new Object(); 69 | return delegate.valAt(key, NOT_FOUND) != NOT_FOUND; 70 | } 71 | 72 | /** 73 | * Returns the number of entries currently in this map. 74 | */ 75 | public int size() { 76 | return delegate.count(); 77 | } 78 | 79 | /** 80 | * Returns a persistent immutable version of this TransientMap. This operation is performed in 81 | * constant time. Note that after this method is called, this transient instance will no longer 82 | * be usable and attempts to modify it will fail. 83 | */ 84 | public ClojureMap toPersistent() { 85 | return ClojureMap.wrap(delegate.persistent()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/collider/TransientSet.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import java.util.Collection; 4 | 5 | import javax.annotation.concurrent.NotThreadSafe; 6 | 7 | import clojure.lang.IPersistentCollection; 8 | import clojure.lang.IPersistentSet; 9 | import clojure.lang.ITransientSet; 10 | import clojure.lang.PersistentHashSet; 11 | 12 | /** 13 | * A set that can be modified in-place and then converted to a {@link ClojureSet} in O(1) time. 14 | *

15 | * Instances of this class are not thread-safe; it is recommended that this class be used in a 16 | * thread-local fashion. In Clojure 1.7.0-RC1 and later, it is permitted to use this class from 17 | * multiple threads, and this is safe as long as access is correctly synchronized. In older versions 18 | * of Clojure, thread-local usage is enforced, and transients can only be modified by their owning 19 | * thread. 20 | */ 21 | @NotThreadSafe 22 | public class TransientSet { 23 | private volatile ITransientSet delegate; 24 | 25 | TransientSet() { 26 | this.delegate = (ITransientSet) PersistentHashSet.EMPTY.asTransient(); 27 | } 28 | 29 | TransientSet(ITransientSet delegate) { 30 | this.delegate = delegate; 31 | } 32 | 33 | /** 34 | * Idempotently adds {@code t} to this set. 35 | */ 36 | public void add(T t) { 37 | this.delegate = (ITransientSet) delegate.conj(t); 38 | } 39 | 40 | /** 41 | * Adds all members of {@code collection} to this set. 42 | */ 43 | public void addAll(Collection collection) { 44 | for (T t : collection) { 45 | add(t); 46 | } 47 | } 48 | 49 | /** 50 | * Returns whether {@code t} is currently a member of this set. 51 | */ 52 | public boolean contains(T t) { 53 | return delegate.contains(t); 54 | } 55 | 56 | /** 57 | * Removes {@code t} from this set. 58 | */ 59 | public void remove(T t) { 60 | this.delegate = delegate.disjoin(t); 61 | } 62 | 63 | /** 64 | * Returns the number of elements in this set. 65 | */ 66 | public int size() { 67 | return delegate.count(); 68 | } 69 | 70 | /** 71 | * Returns a persistent immutable version of this TransientSet. This operation is performed in 72 | * constant time. Note that after this method is called, this transient instance will no longer 73 | * be usable and attempts to modify it will fail. 74 | */ 75 | public ClojureSet toPersistent() { 76 | IPersistentCollection persistent = delegate.persistent(); 77 | IPersistentSet asSet = (IPersistentSet) persistent; 78 | return ClojureSet.wrap(asSet); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/collider/ClojureListTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | 7 | import static com.github.rschmitt.collider.Collider.clojureList; 8 | import static com.github.rschmitt.collider.Collider.toClojureList; 9 | import static java.util.stream.IntStream.range; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | 13 | public class ClojureListTest { 14 | @Test 15 | public void readOnlyOps() { 16 | List myStrings = clojureList("a", "b", "c"); 17 | assertEquals(3, myStrings.size()); 18 | assertEquals("a", myStrings.get(0)); 19 | } 20 | 21 | @Test 22 | public void persistentOps() { 23 | ClojureList strings = clojureList("a", "b", "c"); 24 | strings = strings.append("d"); 25 | assertEquals(4, strings.size()); 26 | assertEquals("d", strings.get(3)); 27 | } 28 | 29 | @Test 30 | public void streams() { 31 | ClojureList strings = clojureList("a", "b", "c"); 32 | strings = strings.append("d"); 33 | 34 | assertEquals(4, strings.stream().count()); 35 | } 36 | 37 | @Test 38 | public void collector() { 39 | ClojureList singles = range(0, 100).boxed().collect(toClojureList()); 40 | for (int i = 0; i < 100; i++) assertEquals(singles.get(i).intValue(), i); 41 | } 42 | 43 | @Test 44 | public void map() throws Exception { 45 | ClojureList initial = range(0, 100).boxed().collect(toClojureList()); 46 | 47 | ClojureList filtered = initial.filter(x -> x >= 50); 48 | 49 | assertEquals(filtered, range(50, 100).boxed().collect(toClojureList())); 50 | } 51 | 52 | @Test 53 | public void filter() throws Exception { 54 | ClojureList singles = range(0, 100).boxed().collect(toClojureList()); 55 | 56 | ClojureList doubles = singles.map(x -> x * 2); 57 | 58 | assertEquals(doubles.size(), singles.size()); 59 | for (int i = 0; i < 100; i++) assertEquals(doubles.get(i).intValue(), i * 2); 60 | } 61 | 62 | @Test 63 | public void exclude() throws Exception { 64 | ClojureList initial = range(0, 100).boxed().collect(toClojureList()); 65 | 66 | ClojureList filtered = initial.exclude(x -> x >= 50); 67 | 68 | assertEquals(filtered, range(0, 50).boxed().collect(toClojureList())); 69 | } 70 | 71 | @Test 72 | public void transientOps() throws Exception { 73 | ClojureList singles = range(0, 100).boxed().collect(toClojureList()); 74 | TransientList tr = singles.asTransient(); 75 | 76 | range(100, 200).forEach(tr::append); 77 | ClojureList moreSingles = tr.toPersistent(); 78 | 79 | assertEquals(moreSingles.size(), 200); 80 | assertEquals(moreSingles, range(0, 200).boxed().collect(toClojureList())); 81 | } 82 | 83 | @Test 84 | public void sublist() throws Exception { 85 | ClojureList singles = range(0, 100).boxed().collect(toClojureList()); 86 | ClojureList sublist = singles.subList(0, 50); 87 | for (int i = 50; i < 100; i++) { 88 | sublist = sublist.append(i); 89 | } 90 | assertEquals(sublist, singles); 91 | } 92 | 93 | @Test 94 | @SuppressWarnings("deprecation") 95 | public void destructiveUpdatesFail() { 96 | ClojureList list = clojureList(14); 97 | assertThrows(UnsupportedOperationException.class, list::clear); 98 | assertThrows(UnsupportedOperationException.class, () -> list.add(15)); 99 | assertThrows(UnsupportedOperationException.class, () -> list.add(15, 15)); 100 | assertThrows(UnsupportedOperationException.class, () -> list.set(1, 15)); 101 | assertThrows(UnsupportedOperationException.class, () -> list.remove(5)); 102 | assertThrows(UnsupportedOperationException.class, () -> list.remove(new Integer(5))); 103 | assertThrows(UnsupportedOperationException.class, () -> list.removeAll(clojureList(14))); 104 | assertThrows(UnsupportedOperationException.class, () -> list.replaceAll(x -> x * 2)); 105 | assertThrows(UnsupportedOperationException.class, () -> list.retainAll(clojureList(14))); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/collider/ClojureMapTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Map.Entry; 6 | import java.util.stream.Stream; 7 | 8 | import static com.github.rschmitt.collider.ClojureMap.create; 9 | import static com.github.rschmitt.collider.Collider.clojureMap; 10 | import static com.github.rschmitt.collider.Collider.toClojureMap; 11 | import static com.github.rschmitt.collider.Collider.toStrictClojureMap; 12 | import static java.util.function.Function.identity; 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertNull; 15 | import static org.junit.jupiter.api.Assertions.assertSame; 16 | import static org.junit.jupiter.api.Assertions.assertThrows; 17 | import static org.junit.jupiter.api.Assertions.assertTrue; 18 | 19 | public class ClojureMapTest { 20 | @Test 21 | public void smokeTest() { 22 | ClojureMap emptyMap = clojureMap(); 23 | 24 | ClojureMap assoc = emptyMap.assoc("key", "value"); 25 | ClojureMap dissoc = assoc.dissoc("key"); 26 | 27 | assertTrue(assoc.containsKey("key")); 28 | assertEquals(assoc.size(), 1); 29 | assertEquals(assoc.get("key"), "value"); 30 | assertEquals(dissoc, emptyMap); 31 | } 32 | 33 | @Test 34 | public void convenienceMethods() { 35 | ClojureMap collect = Stream.of("two", "three", "four").collect(toClojureMap(identity(), String::length)); 36 | 37 | ClojureMap doubled = collect.mapValues(x -> x * 2); 38 | assertEquals(doubled.get("two").intValue(), 6); 39 | 40 | ClojureMap uppercased = collect.mapKeys(String::toUpperCase); 41 | assertEquals(uppercased.get("THREE").intValue(), 5); 42 | assertEquals(uppercased.mapKeys(String::toLowerCase), collect); 43 | 44 | ClojureMap filteredKeys = collect.filterKeys(x -> x.startsWith("t")); 45 | assertEquals(filteredKeys.size(), 2); 46 | assertEquals(filteredKeys.get("two").intValue(), 3); 47 | assertEquals(filteredKeys.get("three").intValue(), 5); 48 | assertNull(filteredKeys.get("four")); 49 | 50 | ClojureMap filteredVals = collect.filterValues(v -> v >= 4); 51 | assertEquals(filteredVals.size(), 2); 52 | assertNull(filteredVals.get("two")); 53 | assertEquals(filteredVals.get("three").intValue(), 5); 54 | assertEquals(filteredVals.get("four").intValue(), 4); 55 | 56 | ClojureMap excludedKeys = collect.excludeKeys(x -> x.startsWith("t")); 57 | assertEquals(excludedKeys, clojureMap("four", 4)); 58 | 59 | ClojureMap excludedVals = collect.excludeValues(v -> v >= 4); 60 | assertEquals(excludedVals, clojureMap("two", 3)); 61 | } 62 | 63 | @Test 64 | public void factoryMethods() { 65 | ClojureMap expected = clojureMap(); 66 | 67 | expected = expected.assoc("a", 1); 68 | assertEquals(clojureMap("a", 1), expected); 69 | 70 | expected = expected.assoc("b", 2); 71 | assertEquals(clojureMap("a", 1, "b", 2), expected); 72 | 73 | expected = expected.assoc("c", 3); 74 | assertEquals(clojureMap("a", 1, "b", 2, "c", 3), expected); 75 | } 76 | 77 | @Test 78 | public void overwrite() { 79 | assertEquals(create("a", 1).assoc("a", 3), create("a", 3)); 80 | } 81 | 82 | @Test 83 | public void strictCollector() { 84 | ClojureMap initial = clojureMap("one", 1, "two", 2, "six", 6); 85 | 86 | ClojureMap collect = initial 87 | .entrySet() 88 | .stream() 89 | .collect(toStrictClojureMap(e -> e.getKey().length(), Entry::getValue, Math::max)); 90 | 91 | assertEquals(collect, clojureMap(3, 6)); 92 | } 93 | 94 | @Test 95 | public void merge() throws Exception { 96 | ClojureMap actual = clojureMap("a", 1) 97 | .merge(clojureMap("a", 2, "b", 4), 98 | clojureMap("a", 3, "d", 4), 99 | clojureMap("b", null, "e", null)); 100 | assertEquals(actual, clojureMap("a", 3, "b", null, "d", 4, "e", null)); 101 | } 102 | 103 | @Test 104 | public void mergeOptimizations() throws Exception { 105 | ClojureMap map = clojureMap("a", 1); 106 | ClojureMap empty = clojureMap(); 107 | 108 | assertSame(map, map.merge()); 109 | assertSame(map, map.merge(empty)); 110 | assertSame(map, map.merge(empty, empty)); 111 | assertSame(map, empty.merge(map)); 112 | } 113 | 114 | @Test 115 | @SuppressWarnings("deprecation") 116 | public void destructiveUpdatesFail() { 117 | ClojureMap map = clojureMap("a", 0); 118 | assertThrows(UnsupportedOperationException.class, map::clear); 119 | assertThrows(UnsupportedOperationException.class, () -> map.put("b", 15)); 120 | assertThrows(UnsupportedOperationException.class, () -> map.putAll(clojureMap())); 121 | assertThrows(UnsupportedOperationException.class, () -> map.remove("a")); 122 | assertThrows(UnsupportedOperationException.class, () -> map.remove("a", 0)); 123 | assertThrows(UnsupportedOperationException.class, () -> map.replace("a", 0)); 124 | assertThrows(UnsupportedOperationException.class, () -> map.replace("a", 0, 2)); 125 | assertThrows(UnsupportedOperationException.class, () -> map.putIfAbsent("b", 4)); 126 | assertThrows(UnsupportedOperationException.class, () -> map.compute("a", String::codePointAt)); 127 | assertThrows(UnsupportedOperationException.class, () -> map.computeIfAbsent("b", String::length)); 128 | assertThrows(UnsupportedOperationException.class, () -> map.computeIfPresent("a", String::codePointAt)); 129 | assertThrows(UnsupportedOperationException.class, () -> map.merge("a", 4, (x, y) -> x + y)); 130 | assertThrows(UnsupportedOperationException.class, () -> map.compute("a", String::codePointAt)); 131 | assertThrows(UnsupportedOperationException.class, () -> map.computeIfAbsent("b", String::length)); 132 | assertThrows(UnsupportedOperationException.class, () -> map.computeIfPresent("a", String::codePointAt)); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/collider/ClojureSetTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Set; 6 | 7 | import static com.github.rschmitt.collider.Collider.clojureSet; 8 | import static com.github.rschmitt.collider.Collider.toClojureSet; 9 | import static java.util.Collections.emptySet; 10 | import static java.util.stream.IntStream.range; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | public class ClojureSetTest { 16 | @Test 17 | public void persistentOps() throws Exception { 18 | ClojureSet initial = clojureSet(23); 19 | 20 | assertEquals(initial.with(23), initial); 21 | assertEquals(initial.without(23), initial.without(23)); 22 | assertEquals(initial.with(42), clojureSet(23, 42)); 23 | assertEquals(initial.with(42).size(), 2); 24 | } 25 | 26 | @Test 27 | public void collector() { 28 | ClojureSet collect = range(0, 10_000).boxed().collect(toClojureSet()); 29 | 30 | assertEquals(collect.size(), 10_000); 31 | for (int i = 0; i < 10_000; i++) assertTrue(collect.contains(i)); 32 | } 33 | 34 | @Test 35 | public void transients() { 36 | ClojureSet before = clojureSet("asdf"); 37 | TransientSet strings = before.asTransient(); 38 | 39 | strings.add("jkl;"); 40 | ClojureSet after = strings.toPersistent(); 41 | 42 | assertEquals(after.size(), 2); 43 | assertTrue(after.containsAll(before)); 44 | assertTrue(after.contains("jkl;")); 45 | } 46 | 47 | @Test 48 | public void transientAdd() { 49 | ClojureSet before = clojureSet("asdf"); 50 | TransientSet strings = before.asTransient(); 51 | 52 | strings.add("jkl;"); 53 | strings.add("jkl;"); 54 | 55 | assertEquals(strings.toPersistent().size(), 2); 56 | } 57 | 58 | @Test 59 | public void transientRemove() { 60 | ClojureSet before = clojureSet("asdf"); 61 | TransientSet strings = before.asTransient(); 62 | 63 | strings.remove("asdf"); 64 | strings.remove("asdf"); 65 | 66 | ClojureSet persistent = strings.toPersistent(); 67 | Set emptySet = emptySet(); 68 | assertEquals(persistent, emptySet); 69 | } 70 | 71 | @Test 72 | public void map() throws Exception { 73 | ClojureSet initial = range(0, 100).boxed().collect(toClojureSet()); 74 | 75 | ClojureSet mapped = initial.map(x -> x % 3); 76 | 77 | assertEquals(mapped, clojureSet(0, 1, 2)); 78 | } 79 | 80 | @Test 81 | public void filter() throws Exception { 82 | ClojureSet initial = range(0, 100).boxed().collect(toClojureSet()); 83 | 84 | ClojureSet filtered = initial.filter(x -> x >= 50); 85 | 86 | assertEquals(filtered, range(50, 100).boxed().collect(toClojureSet())); 87 | } 88 | 89 | @Test 90 | public void exclude() throws Exception { 91 | ClojureSet initial = range(0, 100).boxed().collect(toClojureSet()); 92 | 93 | ClojureSet filtered = initial.exclude(x -> x >= 50); 94 | 95 | assertEquals(filtered, range(0, 50).boxed().collect(toClojureSet())); 96 | } 97 | 98 | @Test 99 | @SuppressWarnings("deprecation") 100 | public void destructiveUpdatesFail() { 101 | ClojureSet set = clojureSet(14); 102 | assertThrows(UnsupportedOperationException.class, set::clear); 103 | assertThrows(UnsupportedOperationException.class, () -> set.add(15)); 104 | assertThrows(UnsupportedOperationException.class, () -> set.remove(5)); 105 | assertThrows(UnsupportedOperationException.class, () -> set.remove(new Integer(5))); 106 | assertThrows(UnsupportedOperationException.class, () -> set.removeAll(clojureSet(4))); 107 | assertThrows(UnsupportedOperationException.class, () -> set.retainAll(clojureSet())); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/collider/FactoryMethodTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.lang.reflect.Field; 6 | import java.util.ArrayList; 7 | import java.util.HashSet; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Set; 11 | 12 | import clojure.lang.PersistentArrayMap; 13 | import clojure.lang.PersistentHashSet; 14 | import clojure.lang.PersistentVector; 15 | 16 | import static com.github.rschmitt.collider.Collider.clojureList; 17 | import static com.github.rschmitt.collider.Collider.clojureMap; 18 | import static com.github.rschmitt.collider.Collider.clojureSet; 19 | import static com.github.rschmitt.collider.Collider.intoClojureList; 20 | import static com.github.rschmitt.collider.Collider.intoClojureMap; 21 | import static com.github.rschmitt.collider.Collider.intoClojureSet; 22 | import static java.util.stream.Collectors.toList; 23 | import static java.util.stream.Collectors.toMap; 24 | import static java.util.stream.Collectors.toSet; 25 | import static java.util.stream.IntStream.range; 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertSame; 28 | 29 | public class FactoryMethodTest { 30 | @Test 31 | public void downcasting() throws Exception { 32 | Set set = clojureSet(); 33 | List list = clojureList(); 34 | Map map = clojureMap(); 35 | 36 | assertSame(intoClojureSet(set), set); 37 | assertSame(intoClojureList(list), list); 38 | assertSame(intoClojureMap(map), map); 39 | } 40 | 41 | @Test 42 | public void wrapping() throws Exception { 43 | PersistentHashSet persistentHashSet = PersistentHashSet.create(); 44 | PersistentVector persistentVector = PersistentVector.create(); 45 | PersistentArrayMap persistentArrayMap = PersistentArrayMap.EMPTY; 46 | 47 | ClojureSet clojureSet = intoClojureSet(persistentHashSet); 48 | ClojureList clojureList = intoClojureList(persistentVector); 49 | ClojureMap clojureMap = intoClojureMap(persistentArrayMap); 50 | 51 | Field setDelegate = ClojureSet.class.getDeclaredField("delegate"); 52 | Field listDelegate = ClojureList.class.getDeclaredField("delegate"); 53 | Field mapDelegate = ClojureMap.class.getDeclaredField("delegate"); 54 | 55 | setDelegate.setAccessible(true); 56 | listDelegate.setAccessible(true); 57 | mapDelegate.setAccessible(true); 58 | 59 | assertSame(setDelegate.get(clojureSet), persistentHashSet); 60 | assertSame(listDelegate.get(clojureList), persistentVector); 61 | assertSame(mapDelegate.get(clojureMap), persistentArrayMap); 62 | } 63 | 64 | @Test 65 | public void copyConstructor() throws Exception { 66 | List list = range(0, 100).boxed().collect(toList()); 67 | Set set = range(0, 100).boxed().collect(toSet()); 68 | Map map = range(0, 100).boxed().collect(toMap(t -> t, String::valueOf)); 69 | 70 | assertEquals(intoClojureList(list), list); 71 | assertEquals(intoClojureSet(set), set); 72 | assertEquals(intoClojureMap(map), map); 73 | } 74 | 75 | @Test 76 | public void nulls() throws Exception { 77 | Set expectedSet = new HashSet(); 78 | expectedSet.add(null); 79 | 80 | List expectedList = new ArrayList(); 81 | expectedList.add(null); 82 | 83 | assertEquals(clojureList(null), expectedList); 84 | assertEquals(clojureSet(null), expectedSet); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/collider/TransientListTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.github.rschmitt.collider.Collider.clojureList; 7 | import static com.github.rschmitt.collider.Collider.transientList; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertNull; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | 12 | public class TransientListTest { 13 | @Test 14 | @SuppressWarnings("unchecked") 15 | public void reuseFails() throws Exception { 16 | TransientList transientList = transientList(); 17 | transientList.toPersistent(); 18 | assertThrows(IllegalAccessError.class, () -> transientList.append(new Object())); 19 | } 20 | 21 | @Test 22 | public void append() throws Exception { 23 | TransientList transientList = transientList(); 24 | 25 | transientList.append("a"); 26 | transientList.append("a"); 27 | transientList.append("a"); 28 | 29 | assertEquals(transientList.toPersistent(), clojureList("a", "a", "a")); 30 | } 31 | 32 | @Test 33 | public void readOperations() throws Exception { 34 | TransientList transientList = clojureList(0, 1, 2, 3, 4).asTransient(); 35 | for (int i = 0; i < 5; i++) assertEquals(transientList.get(i).intValue(), i); 36 | assertThrows(IndexOutOfBoundsException.class, () -> transientList.get(6)); 37 | } 38 | 39 | @Test 40 | public void setOperations() throws Exception { 41 | TransientList transientList = clojureList(0, 1, 2, 3, 4).asTransient(); 42 | assertThrows(IndexOutOfBoundsException.class, () -> transientList.set(6, 5)); 43 | 44 | transientList.append(5); 45 | transientList.append(6); 46 | assertEquals(transientList.get(6).intValue(), 6); 47 | 48 | transientList.set(6, 66); 49 | assertEquals(transientList.get(6).intValue(), 66); 50 | } 51 | 52 | @Test 53 | public void size() throws Exception { 54 | TransientList transientList = transientList(); 55 | assertEquals(transientList.size(), 0); 56 | 57 | transientList.append("a"); 58 | assertEquals(transientList.size(), 1); 59 | 60 | transientList.append("a"); 61 | assertEquals(transientList.size(), 2); 62 | 63 | transientList.append("a"); 64 | assertEquals(transientList.size(), 3); 65 | 66 | transientList.set(2, "b"); 67 | assertEquals(transientList.size(), 3); 68 | } 69 | 70 | @Test 71 | public void nulls() throws Exception { 72 | TransientList transientList = transientList(); 73 | 74 | transientList.append(null); 75 | assertEquals(transientList.size(), 1); 76 | 77 | transientList.append("a"); 78 | assertEquals(transientList.size(), 2); 79 | 80 | transientList.set(1, null); 81 | assertEquals(transientList.size(), 2); 82 | assertNull(transientList.get(0)); 83 | assertNull(transientList.get(1)); 84 | 85 | assertEquals(transientList.toPersistent(), clojureList(null, null)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/collider/TransientMapTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.rschmitt.collider.Collider.clojureMap; 6 | import static com.github.rschmitt.collider.Collider.transientMap; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | public class TransientMapTest { 12 | @Test 13 | @SuppressWarnings("unchecked") 14 | public void reuseFails() throws Exception { 15 | TransientMap transientMap = transientMap(); 16 | transientMap.toPersistent(); 17 | assertThrows(IllegalAccessError.class, () -> transientMap.put(new Object(), new Object())); 18 | } 19 | 20 | @Test 21 | public void putOverwrites() throws Exception { 22 | TransientMap transientMap = transientMap(); 23 | 24 | transientMap.put("a", 1); 25 | assertEquals(transientMap.get("a"), 1); 26 | 27 | transientMap.put("a", 2); 28 | assertEquals(transientMap.get("a"), 2); 29 | 30 | transientMap.put("b", 3); 31 | assertEquals(transientMap.get("b"), 3); 32 | 33 | assertEquals(transientMap.toPersistent(), clojureMap("a", 2, "b", 3)); 34 | } 35 | 36 | @Test 37 | public void remove() throws Exception { 38 | TransientMap transientMap = transientMap(); 39 | assertEquals(transientMap.size(), 0); 40 | 41 | transientMap.put("a", 1); 42 | assertEquals(transientMap.size(), 1); 43 | 44 | transientMap.remove("a"); 45 | assertEquals(transientMap.size(), 0); 46 | 47 | transientMap.remove("a"); 48 | assertEquals(transientMap.size(), 0); 49 | } 50 | 51 | @Test 52 | public void nullValue() throws Exception { 53 | TransientMap transientMap = transientMap(); 54 | 55 | transientMap.put("key", null); 56 | 57 | assertEquals(transientMap.size(), 1); 58 | assertNull(transientMap.get("key")); 59 | assertEquals(transientMap.toPersistent(), clojureMap("key", null)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/collider/TransientSetTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.collider; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.Arrays; 6 | 7 | import static com.github.rschmitt.collider.Collider.clojureSet; 8 | import static com.github.rschmitt.collider.Collider.transientSet; 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | public class TransientSetTest { 15 | @Test 16 | @SuppressWarnings("unchecked") 17 | public void reuseFails() throws Exception { 18 | TransientSet transientSet = transientSet(); 19 | transientSet.toPersistent(); 20 | assertThrows(IllegalAccessError.class, () -> transientSet.add(new Object())); 21 | } 22 | 23 | @Test 24 | public void remove() throws Exception { 25 | TransientSet transientSet = clojureSet("a", "b", "c").asTransient(); 26 | assertTrue(transientSet.contains("a")); 27 | 28 | transientSet.remove("a"); 29 | assertFalse(transientSet.contains("a")); 30 | 31 | transientSet.remove("b"); 32 | assertFalse(transientSet.contains("a")); 33 | 34 | assertTrue(transientSet.contains("c")); 35 | assertEquals(transientSet.toPersistent(), clojureSet("c")); 36 | } 37 | 38 | @Test 39 | public void idempotentRemove() throws Exception { 40 | TransientSet transientSet = transientSet(); 41 | transientSet.remove(new Object()); 42 | assertEquals(transientSet.toPersistent(), clojureSet()); 43 | } 44 | 45 | @Test 46 | public void add() throws Exception { 47 | TransientSet transientSet = transientSet(); 48 | transientSet.add("a"); 49 | transientSet.add("b"); 50 | transientSet.addAll(clojureSet("b", "c", "d")); 51 | transientSet.addAll(Arrays.asList("d", "e", "f")); 52 | assertEquals(transientSet.toPersistent(), clojureSet("a", "b", "c", "d", "e", "f")); 53 | } 54 | 55 | @Test 56 | public void nulls() throws Exception { 57 | TransientSet transientSet = transientSet(); 58 | assertEquals(0, transientSet.size()); 59 | 60 | transientSet.add(null); 61 | assertTrue(transientSet.contains(null)); 62 | assertEquals(1, transientSet.size()); 63 | 64 | transientSet.remove(null); 65 | assertFalse(transientSet.contains(null)); 66 | assertEquals(0, transientSet.size()); 67 | 68 | transientSet.add(null); 69 | assertEquals(transientSet.toPersistent(), clojureSet(null)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test-all-versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | versions=(1.5.0 1.5.1 1.6.0 1.7.0 1.8.0) 6 | 7 | for i in ${versions[@]} 8 | do 9 | cp pom.xml pom-$i.xml 10 | perl -i -pe 's/\[1.5.0,\)/'"$i"'/g' pom-$i.xml 11 | mvn clean test -f pom-$i.xml 12 | done 13 | 14 | for i in ${versions[@]} 15 | do 16 | rm -f pom-$i.xml pom-$i.xml.bak 17 | done 18 | --------------------------------------------------------------------------------