From 1c901067b1a5f9d1f4a4277301e5ba20dce0f2c5 Mon Sep 17 00:00:00 2001 From: Mincong Huang Date: Sat, 23 May 2020 23:04:23 +0200 Subject: [PATCH] Iterate once to create two iterators in partition (#2577) * Reproduce the problem * Iterate once to create two iterators in partition * Avoid using io.vavr.collection.Stream * Test behavior of `partition` on different classes * Test that Stream.partition() is lazy * Create Iterator.duplicate() and add tests * Change the implementation of Iterator.partition() * Fix Set * Fix Map * Fix Multimap * Move duplicate to IteratorModule * Remove synchronized keyword * Remove hashCode and equals * Avoid using isEqualTo * Remove redundant tests --- .../io/vavr/collection/AbstractMultimap.java | 8 +- src/main/java/io/vavr/collection/BitSet.java | 3 +- .../java/io/vavr/collection/Collections.java | 15 +++- src/main/java/io/vavr/collection/HashSet.java | 6 +- .../java/io/vavr/collection/Iterator.java | 48 ++++++++++- .../io/vavr/collection/LinkedHashSet.java | 4 +- src/main/java/io/vavr/collection/Maps.java | 8 +- .../java/io/vavr/collection/Traversable.java | 18 ++-- src/main/java/io/vavr/collection/TreeSet.java | 4 +- .../io/vavr/collection/AbstractMapTest.java | 15 ++++ .../vavr/collection/AbstractMultimapTest.java | 14 +++ .../io/vavr/collection/AbstractSetTest.java | 18 +++- .../java/io/vavr/collection/ArrayTest.java | 15 ++++ .../java/io/vavr/collection/CharSeqTest.java | 14 +++ .../java/io/vavr/collection/IteratorTest.java | 35 ++++++++ .../java/io/vavr/collection/ListTest.java | 15 ++++ .../java/io/vavr/collection/QueueTest.java | 15 ++++ .../java/io/vavr/collection/StreamTest.java | 34 ++++++++ .../java/io/vavr/collection/VectorTest.java | 16 ++++ .../java/io/vavr/issues/Issue2559Test.java | 86 +++++++++++++++++++ 20 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 src/test/java/io/vavr/issues/Issue2559Test.java diff --git a/src/main/java/io/vavr/collection/AbstractMultimap.java b/src/main/java/io/vavr/collection/AbstractMultimap.java index f3dd468130..ba933d9199 100644 --- a/src/main/java/io/vavr/collection/AbstractMultimap.java +++ b/src/main/java/io/vavr/collection/AbstractMultimap.java @@ -472,8 +472,12 @@ public M orElse(Supplier>> supplier) { @Override public Tuple2 partition(Predicate> predicate) { Objects.requireNonNull(predicate, "predicate is null"); - final Tuple2>, Iterator>> p = iterator().partition(predicate); - return Tuple.of((M) createFromEntries(p._1), (M) createFromEntries(p._2)); + final java.util.List> left = new java.util.ArrayList<>(); + final java.util.List> right = new java.util.ArrayList<>(); + for (Tuple2 entry : this) { + (predicate.test(entry) ? left : right).add(entry); + } + return Tuple.of((M) createFromEntries(left), (M) createFromEntries(right)); } @SuppressWarnings("unchecked") diff --git a/src/main/java/io/vavr/collection/BitSet.java b/src/main/java/io/vavr/collection/BitSet.java index acb716fbd7..4aa3c5fc6d 100644 --- a/src/main/java/io/vavr/collection/BitSet.java +++ b/src/main/java/io/vavr/collection/BitSet.java @@ -831,8 +831,7 @@ public BitSet scan(T zero, BiFunction oper @Override public Tuple2, BitSet> partition(Predicate predicate) { - Objects.requireNonNull(predicate, "predicate is null"); - return iterator().partition(predicate).map(this::createFromAll, this::createFromAll); + return Collections.partition(this, this::createFromAll, predicate); } @Override diff --git a/src/main/java/io/vavr/collection/Collections.java b/src/main/java/io/vavr/collection/Collections.java index 991dfbf876..9ddd1776e2 100644 --- a/src/main/java/io/vavr/collection/Collections.java +++ b/src/main/java/io/vavr/collection/Collections.java @@ -18,6 +18,8 @@ */ package io.vavr.collection; +import io.vavr.Tuple; +import io.vavr.Tuple2; import io.vavr.collection.JavaConverters.ChangePolicy; import io.vavr.collection.JavaConverters.ListView; import io.vavr.control.Option; @@ -294,6 +296,17 @@ static > U mapKeys(Map source, U zero, Func }); } + static , T> Tuple2 partition(C collection, Function, C> creator, + Predicate predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + final java.util.List left = new java.util.ArrayList<>(); + final java.util.List right = new java.util.ArrayList<>(); + for (T element : collection) { + (predicate.test(element) ? left : right).add(element); + } + return Tuple.of(creator.apply(left), creator.apply(right)); + } + @SuppressWarnings("unchecked") static , T> C removeAll(C source, Iterable elements) { Objects.requireNonNull(elements, "elements is null"); @@ -556,7 +569,7 @@ private static IterableWithSize withSizeTraversable(Iterable return new IterableWithSize<>(iterable, ((Traversable) iterable).size()); } } - + static class IterableWithSize { private final Iterable iterable; private final int size; diff --git a/src/main/java/io/vavr/collection/HashSet.java b/src/main/java/io/vavr/collection/HashSet.java index 04e68b4508..73b37874f9 100644 --- a/src/main/java/io/vavr/collection/HashSet.java +++ b/src/main/java/io/vavr/collection/HashSet.java @@ -668,7 +668,7 @@ public boolean isAsync() { public boolean isEmpty() { return tree.isEmpty(); } - + /** * A {@code HashSet} is computed eagerly. * @@ -730,9 +730,7 @@ public HashSet orElse(Supplier> supplier) { @Override public Tuple2, HashSet> partition(Predicate predicate) { - Objects.requireNonNull(predicate, "predicate is null"); - final Tuple2, Iterator> p = iterator().partition(predicate); - return Tuple.of(HashSet.ofAll(p._1), HashSet.ofAll(p._2)); + return Collections.partition(this, HashSet::ofAll, predicate); } @Override diff --git a/src/main/java/io/vavr/collection/Iterator.java b/src/main/java/io/vavr/collection/Iterator.java index 4e76922280..289b06fac9 100644 --- a/src/main/java/io/vavr/collection/Iterator.java +++ b/src/main/java/io/vavr/collection/Iterator.java @@ -23,6 +23,7 @@ import java.math.BigDecimal; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.*; import static io.vavr.collection.BigDecimalHelper.areEqual; @@ -1722,10 +1723,8 @@ default Tuple2, Iterator> partition(Predicate predicat if (!hasNext()) { return Tuple.of(empty(), empty()); } else { - final Stream that = Stream.ofAll(this); - final Iterator first = that.iterator().filter(predicate); - final Iterator second = that.iterator().filter(predicate.negate()); - return Tuple.of(first, second); + final Tuple2, Iterator> dup = IteratorModule.duplicate(this); + return Tuple.of(dup._1.filter(predicate), dup._2.filterNot(predicate)); } } @@ -1952,6 +1951,7 @@ default Tuple2, Iterator> span(Predicate predicate) { } } + @Override default String stringPrefix() { return "Iterator"; @@ -2182,6 +2182,46 @@ public String toString() { } } +interface IteratorModule { + /** + * Creates two new iterators that both iterates over the same elements as + * this iterator and in the same order. The duplicate iterators are + * considered equal if they are positioned at the same element. + *

+ * Given that most methods on iterators will make the original iterator + * unfit for further use, this methods provides a reliable way of calling + * multiple such methods on an iterator. + * + * @return a pair of iterators + */ + static Tuple2, Iterator> duplicate(Iterator iterator) { + final java.util.Queue gap = new java.util.LinkedList<>(); + final AtomicReference> ahead = new AtomicReference<>(); + class Partner implements Iterator { + + @Override + public boolean hasNext() { + return (this != ahead.get() && !gap.isEmpty()) || iterator.hasNext(); + } + + @Override + public T next() { + if (gap.isEmpty()) { + ahead.set(this); + } + if (this == ahead.get()) { + final T element = iterator.next(); + gap.add(element); + return element; + } else { + return gap.poll(); + } + } + } + return Tuple.of(new Partner(), new Partner()); + } +} + final class ConcatIterator implements Iterator { private static final class Iterators { diff --git a/src/main/java/io/vavr/collection/LinkedHashSet.java b/src/main/java/io/vavr/collection/LinkedHashSet.java index 9e5a7911e5..c28e7d8d6e 100644 --- a/src/main/java/io/vavr/collection/LinkedHashSet.java +++ b/src/main/java/io/vavr/collection/LinkedHashSet.java @@ -751,9 +751,7 @@ public LinkedHashSet orElse(Supplier> supplie @Override public Tuple2, LinkedHashSet> partition(Predicate predicate) { - Objects.requireNonNull(predicate, "predicate is null"); - final Tuple2, Iterator> p = iterator().partition(predicate); - return Tuple.of(LinkedHashSet.ofAll(p._1), LinkedHashSet.ofAll(p._2)); + return Collections.partition(this, LinkedHashSet::ofAll, predicate); } @Override diff --git a/src/main/java/io/vavr/collection/Maps.java b/src/main/java/io/vavr/collection/Maps.java index ae2af545bc..179e687d26 100644 --- a/src/main/java/io/vavr/collection/Maps.java +++ b/src/main/java/io/vavr/collection/Maps.java @@ -201,8 +201,12 @@ static > M ofStream(M map, java.util.stream.Stream< static > Tuple2 partition(M map, OfEntries ofEntries, Predicate> predicate) { Objects.requireNonNull(predicate, "predicate is null"); - final Tuple2>, Iterator>> p = map.iterator().partition(predicate); - return Tuple.of(ofEntries.apply(p._1), ofEntries.apply(p._2)); + final java.util.List> left = new java.util.ArrayList<>(); + final java.util.List> right = new java.util.ArrayList<>(); + for (Tuple2 entry : map) { + (predicate.test(entry) ? left : right).add(entry); + } + return Tuple.of(ofEntries.apply(left), ofEntries.apply(right)); } static > M peek(M map, Consumer> action) { diff --git a/src/main/java/io/vavr/collection/Traversable.java b/src/main/java/io/vavr/collection/Traversable.java index 59d70bf235..9c140a69be 100644 --- a/src/main/java/io/vavr/collection/Traversable.java +++ b/src/main/java/io/vavr/collection/Traversable.java @@ -164,7 +164,7 @@ public interface Traversable extends Iterable, Foldable, io.vavr.Value< static Traversable narrow(Traversable traversable) { return (Traversable) traversable; } - + /** * Matches each element with a unique key that you extract from it. * If the same key is present twice, the function will return {@code None}. @@ -223,7 +223,7 @@ default Option average() { throw new UnsupportedOperationException("not numeric", x); } } - + /** * Collects all elements that are in the domain of the given {@code partialFunction} by mapping the elements to type {@code R}. *

@@ -341,7 +341,7 @@ default int count(Predicate predicate) { Traversable dropUntil(Predicate predicate); /** - * Drops elements while the predicate holds for the current element. + * Drops elements while the predicate holds for the current element. *

* Note: This is essentially the same as {@code dropUntil(predicate.negate())}. * It is intended to be used with method references, which cannot be negated directly. @@ -370,7 +370,7 @@ default int count(Predicate predicate) { *

  • contain the same elements
  • *
  • have the same element order, if the collections are of type Seq
  • * - * + * * Two Map/Multimap elements, resp. entries, (key1, value1) and (key2, value2) are equal, * if the keys are equal and the values are equal. *

    @@ -685,7 +685,7 @@ default T get() { default Option headOption() { return isEmpty() ? Option.none() : Option.some(head()); } - + /** * Returns the hash code of this collection. *
    @@ -1055,7 +1055,7 @@ default > Option minBy(Function @@ -1372,7 +1372,7 @@ default Option reduceRightOption(BiFunction Traversable scanRight(U zero, BiFunction operation); - + /** * Returns the single element of this Traversable or throws, if this is empty or contains more than one element. * @@ -1536,7 +1536,7 @@ default Number sum() { } } } - + /** * Drops the first element of a non-empty Traversable. * @@ -1666,7 +1666,7 @@ default Number sum() { * @throws NullPointerException if {@code that} is null */ Traversable> zipAll(Iterable that, T thisElem, U thatElem); - + /** * Returns a traversable formed from this traversable and another Iterable collection by mapping elements. * If one of the two iterables is longer than the other, its remaining elements are ignored. diff --git a/src/main/java/io/vavr/collection/TreeSet.java b/src/main/java/io/vavr/collection/TreeSet.java index 50e4f04f02..688c8a3005 100644 --- a/src/main/java/io/vavr/collection/TreeSet.java +++ b/src/main/java/io/vavr/collection/TreeSet.java @@ -800,9 +800,7 @@ public TreeSet orElse(Supplier> supplier) { @Override public Tuple2, TreeSet> partition(Predicate predicate) { - Objects.requireNonNull(predicate, "predicate is null"); - return iterator().partition(predicate).map(i1 -> TreeSet.ofAll(tree.comparator(), i1), - i2 -> TreeSet.ofAll(tree.comparator(), i2)); + return Collections.partition(this, values -> TreeSet.ofAll(tree.comparator(), values), predicate); } @Override diff --git a/src/test/java/io/vavr/collection/AbstractMapTest.java b/src/test/java/io/vavr/collection/AbstractMapTest.java index 56b47ececd..4316e58a94 100644 --- a/src/test/java/io/vavr/collection/AbstractMapTest.java +++ b/src/test/java/io/vavr/collection/AbstractMapTest.java @@ -1461,6 +1461,21 @@ public void shouldReturnDefaultValue() { assertThat(map.getOrElse("3", "3")).isEqualTo("3"); } + // -- partition + + @Test + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Map map = mapOf("1", 1, "2", 2, "3", 3); + final Tuple2, ? extends Map> results = map.partition(entry -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(mapOf("1", 1, "2", 2, "3", 3)); + assertThat(results._2).isEmpty(); + assertThat(count.get()).isEqualTo(3); + } + // -- spliterator @Test diff --git a/src/test/java/io/vavr/collection/AbstractMultimapTest.java b/src/test/java/io/vavr/collection/AbstractMultimapTest.java index d5a32f9aa1..20e3e5b86b 100644 --- a/src/test/java/io/vavr/collection/AbstractMultimapTest.java +++ b/src/test/java/io/vavr/collection/AbstractMultimapTest.java @@ -809,6 +809,20 @@ public void shouldPartitionIntsInOddAndEvenHavingOddAndEvenNumbers() { mapOfTuples(Tuple.of(1, 2), Tuple.of(3, 4)))); } + @Test + @SuppressWarnings("unchecked") + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Multimap map = mapOfTuples(Tuple.of("1", 1), Tuple.of("2", 2), Tuple.of("3", 3)); + final Tuple2, ? extends Multimap> results = map.partition(entry -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(mapOfTuples(Tuple.of("1", 1), Tuple.of("2", 2), Tuple.of("3", 3))); + assertThat(results._2).isEmpty(); + assertThat(count.get()).isEqualTo(3); + } + // -- put @Test diff --git a/src/test/java/io/vavr/collection/AbstractSetTest.java b/src/test/java/io/vavr/collection/AbstractSetTest.java index 687f11b200..bd4c6a435b 100644 --- a/src/test/java/io/vavr/collection/AbstractSetTest.java +++ b/src/test/java/io/vavr/collection/AbstractSetTest.java @@ -18,10 +18,12 @@ */ package io.vavr.collection; +import io.vavr.Tuple2; import org.junit.Test; import java.math.BigDecimal; import java.util.Spliterator; +import java.util.concurrent.atomic.AtomicInteger; public abstract class AbstractSetTest extends AbstractTraversableRangeTest { @@ -189,6 +191,20 @@ public void shouldRemoveElement() { assertThat(empty().remove(5)).isEqualTo(empty()); } + // -- partition + + @Test + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Tuple2, ? extends Set> results = of(1, 2, 3).partition(i -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(of(1, 2, 3)); + assertThat(results._2).isEqualTo(of()); + assertThat(count.get()).isEqualTo(3); + } + // -- removeAll @Test @@ -227,7 +243,7 @@ public void shouldReturnSameSetWhenEmptyUnionNonEmpty() { assertThat(empty().union(set)).isSameAs(set); } } - + @Test public void shouldReturnSameSetWhenNonEmptyUnionEmpty() { final Set set = of(1, 2); diff --git a/src/test/java/io/vavr/collection/ArrayTest.java b/src/test/java/io/vavr/collection/ArrayTest.java index 27aac0918a..37b6b5fb26 100644 --- a/src/test/java/io/vavr/collection/ArrayTest.java +++ b/src/test/java/io/vavr/collection/ArrayTest.java @@ -27,6 +27,7 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; @@ -243,6 +244,20 @@ public void shouldThrowExceptionWhenGetIndexEqualToLength() { .isInstanceOf(IndexOutOfBoundsException.class).hasMessage("get(1)"); } + // -- partition + + @Test + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Tuple2, Array> results = of(1, 2, 3).partition(i -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(of(1, 2, 3)); + assertThat(results._2).isEqualTo(of()); + assertThat(count.get()).isEqualTo(3); + } + // -- transform() @Test diff --git a/src/test/java/io/vavr/collection/CharSeqTest.java b/src/test/java/io/vavr/collection/CharSeqTest.java index 6a8bc79da0..1fcc0a3e2d 100644 --- a/src/test/java/io/vavr/collection/CharSeqTest.java +++ b/src/test/java/io/vavr/collection/CharSeqTest.java @@ -273,6 +273,20 @@ public void shouldCaclNonemptyOrElseSupplier() { assertThat(src.orElse(() -> CharSeq.of("12"))).isSameAs(src); } + // -- partition + + @Test + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Tuple2 results = CharSeq.of('1', '2', '3').partition(c -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(CharSeq.of('1', '2', '3')); + assertThat(results._2).isEqualTo(CharSeq.of()); + assertThat(count.get()).isEqualTo(3); + } + // -- patch @Test diff --git a/src/test/java/io/vavr/collection/IteratorTest.java b/src/test/java/io/vavr/collection/IteratorTest.java index 8416fa3e33..66241cfb68 100644 --- a/src/test/java/io/vavr/collection/IteratorTest.java +++ b/src/test/java/io/vavr/collection/IteratorTest.java @@ -733,6 +733,41 @@ public void shouldReturnSomeOnNextOptionWhenIteratorOfOneElement() { assertThat(Iterator.of(1).nextOption()).isEqualTo(Option.some(1)); } + // -- partition() + + @Test + public void shouldPartition() { + final Tuple2, Iterator> partitions = of("1", "2", "3").partition("2"::equals); + assertThat(String.join(", ", partitions._1)).isEqualTo("2"); + assertThat(String.join(", ", partitions._2)).isEqualTo("1, 3"); + } + + @Test(timeout = 5_000L) // avoid endless test caused by infinite iterator + public void shouldPartitionLazily() { + final java.util.List itemsCalled = new java.util.ArrayList<>(); + + // Given an infinite iterator + final Iterator iterator = Iterator.iterate(1, i -> { + itemsCalled.add(i); + return i + 1; + }); + + // When partitioning it + // Then the partitioning is done lazily (otherwise the test will timeout) + final Tuple2, Iterator> partitions = iterator.partition(i -> i % 2 == 0); + assertThat(itemsCalled).isEmpty(); + + // When moving forwards iterators + // Then the moves are done as expected + assertThat(partitions._1.hasNext()).isTrue(); + assertThat(partitions._1.next()).isEqualTo(2); + for (int i : of(1, 3, 5)) { + assertThat(partitions._2.hasNext()).isTrue(); + assertThat(partitions._2.next()).isEqualTo(i); + } + assertThat(itemsCalled).containsExactly(1, 2, 3, 4); + } + // -- .toString() @Test diff --git a/src/test/java/io/vavr/collection/ListTest.java b/src/test/java/io/vavr/collection/ListTest.java index 0f3284efa9..0eb2486284 100644 --- a/src/test/java/io/vavr/collection/ListTest.java +++ b/src/test/java/io/vavr/collection/ListTest.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.NoSuchElementException; import java.util.Spliterator; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; @@ -240,6 +241,20 @@ public void shouldReturnSelfWhenIterableIsInstanceOfListView() { assertThat(target).isSameAs(source.getDelegate()); } + // -- partition + + @Test + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Tuple2, List> results = of(1, 2, 3).partition(i -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(of(1, 2, 3)); + assertThat(results._2).isEqualTo(of()); + assertThat(count.get()).isEqualTo(3); + } + // -- peek @Test(expected = NoSuchElementException.class) diff --git a/src/test/java/io/vavr/collection/QueueTest.java b/src/test/java/io/vavr/collection/QueueTest.java index 4f17505f26..168ddf0ef0 100644 --- a/src/test/java/io/vavr/collection/QueueTest.java +++ b/src/test/java/io/vavr/collection/QueueTest.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.NoSuchElementException; import java.util.Spliterator; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; @@ -236,6 +237,20 @@ public void shouldReturnSelfWhenIterableIsInstanceOfListView() { assertThat(target).isSameAs(source.getDelegate()); } + // -- partition + + @Test + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Tuple2, Queue> results = of(1, 2, 3).partition(i -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(of(1, 2, 3)); + assertThat(results._2).isEqualTo(of()); + assertThat(count.get()).isEqualTo(3); + } + // -- peek @Test(expected = NoSuchElementException.class) diff --git a/src/test/java/io/vavr/collection/StreamTest.java b/src/test/java/io/vavr/collection/StreamTest.java index 4e7c318e3f..f50f7a0875 100644 --- a/src/test/java/io/vavr/collection/StreamTest.java +++ b/src/test/java/io/vavr/collection/StreamTest.java @@ -28,6 +28,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Spliterator; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -469,6 +470,39 @@ public void shouldFlatMapInfiniteTraversable() { .isEqualTo(Stream.of(1, 2, 2, 4, 3, 6, 4)); } + // -- partition + + @Test + public void shouldPartitionInTwoIterations() { + final AtomicInteger count = new AtomicInteger(0); + final Tuple2, Stream> results = Stream.of(1, 2, 3).partition(i -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(of(1, 2, 3)); + assertThat(results._2).isEqualTo(of()); + assertThat(count.get()).isEqualTo(6); + } + + @Test + public void shouldPartitionLazily() { + final java.util.Set itemsCalled = new java.util.HashSet<>(); + + final Stream infiniteStream = Stream.iterate(0, i -> i + 1); + assertThat(itemsCalled).isEmpty(); + + final Tuple2, Stream> results = infiniteStream.partition(i -> { + itemsCalled.add(i); + return i % 2 == 0; + }); + assertThat(itemsCalled).containsExactly(0, 1); + assertThat(results._1.head()).isEqualTo(0); + assertThat(results._2.head()).isEqualTo(1); + assertThat(results._1.take(3)).isEqualTo(of(0, 2, 4)); + assertThat(results._2.take(3)).isEqualTo(of(1, 3, 5)); + assertThat(itemsCalled).containsExactly(0, 1, 2, 3, 4, 5); + } + // -- peek @Override diff --git a/src/test/java/io/vavr/collection/VectorTest.java b/src/test/java/io/vavr/collection/VectorTest.java index 14e2e6709c..2bc684a2c1 100644 --- a/src/test/java/io/vavr/collection/VectorTest.java +++ b/src/test/java/io/vavr/collection/VectorTest.java @@ -28,6 +28,7 @@ import java.io.InvalidObjectException; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; @@ -235,6 +236,21 @@ public void shouldReturnSelfWhenIterableIsInstanceOfListView() { assertThat(target).isSameAs(source.getDelegate()); } + // -- partition + + @Test + public void shouldPartitionInOneIteration() { + final AtomicInteger count = new AtomicInteger(0); + final Vector values = ofAll(1, 2, 3); + final Tuple2, Vector> results = values.partition(v -> { + count.incrementAndGet(); + return true; + }); + assertThat(results._1).isEqualTo(ofAll(1, 2, 3)); + assertThat(results._2).isEmpty(); + assertThat(count.get()).isEqualTo(3); + } + // -- primitives @Test diff --git a/src/test/java/io/vavr/issues/Issue2559Test.java b/src/test/java/io/vavr/issues/Issue2559Test.java new file mode 100644 index 0000000000..8a11b2dbc1 --- /dev/null +++ b/src/test/java/io/vavr/issues/Issue2559Test.java @@ -0,0 +1,86 @@ +package io.vavr.issues; + +import io.vavr.Tuple2; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import org.junit.Before; +import org.junit.Test; + +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * partition method seems to not work correctly, comparing to scala + * https://github.com/vavr-io/vavr/issues/2559 + */ +public class Issue2559Test { + + private java.util.Map fruitsBeingEaten = new java.util.HashMap<>(); + + @Before + public void setUp() { + fruitsBeingEaten = new java.util.HashMap<>(); + } + + @Test + public void partitionShouldBeUnique() { + final Set fruitsToEat = HashSet.of("apple", "banana"); + final Tuple2, ? extends Set> partition = fruitsToEat.partition(this::biteAndCheck); + assertThat(partition._1).isEmpty(); + assertThat(partition._2).isEqualTo(HashSet.of("apple", "banana")); + assertThat(fruitsBeingEaten) + .hasSize(2) + .containsEntry("apple", new Eat(1, "apple")) + .containsEntry("banana", new Eat(1, "banana")); + } + + private boolean biteAndCheck(String name) { + final Eat eat = fruitsBeingEaten.getOrDefault(name, Eat.prepare(name)).bite(); + fruitsBeingEaten.put(name, eat); + return eat.isEaten(); + } + + private static class Eat { + final int bites; + final String name; + + public static Eat prepare(String name) { + return new Eat(0, name); + } + + private Eat(int bites, String name) { + this.bites = bites; + this.name = name; + } + + public Eat bite() { + return new Eat(bites + 1, name); + } + + public boolean isEaten() { + return bites >= 2; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Eat)) return false; + Eat eat = (Eat) o; + return bites == eat.bites && Objects.equals(name, eat.name); + } + + @Override + public int hashCode() { + return Objects.hash(bites, name); + } + + @Override + public String toString() { + return "Eat{" + + "bites=" + bites + + ", name='" + name + '\'' + + '}'; + } + } +}