Skip to content

Commit

Permalink
[#162] ifEmpty method
Browse files Browse the repository at this point in the history
  • Loading branch information
amaembo committed Oct 17, 2017
1 parent f7f2287 commit 64b8767
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 1 deletion.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

# IntelliJ IDEA
.idea/dictionaries/
*.iml

# Mac OS/X
.DS_Store
3 changes: 3 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Check also [MIGRATION.md](MIGRATION.md) for possible compatibility problems.
### 0.6.6
* [#145] Added: `intersperse` method for all stream types.
* [#144] Added: `EntryStream.generate`
* [#162] Added: `StreamEx.ifEmpty`, `EntryStream.ifEmpty`

### 0.6.5
* [#137] Added: `StreamEx.toNavigableMap()`, `EntryStream.toNavigableMap()`
Expand Down
1 change: 1 addition & 0 deletions CHEATSHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ Perform parallel stream computation using the custom `ForkJoinPool` | `any.paral
Zip two streams together | `StreamEx.zipWith()`
Get the stream of cumulative prefixes | `any.prefix()`/`EntryStream.prefixKeys()`/`EntryStream.prefixValues()`
Intersperse the stream with given delimiters | `any.intersperse()`
Replace the stream contents if the stream is empty | `StreamEx.ifEmpty()`/`EntryStream.ifEmpty()`

## New terminal operations

Expand Down
37 changes: 37 additions & 0 deletions src/main/java/one/util/streamex/AbstractStreamEx.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ S prependSpliterator(Stream<? extends T> other, Spliterator<? extends T> left) {
return supply(result);
}

@SuppressWarnings("unchecked")
S ifEmpty(Stream<? extends T> other, Spliterator<? extends T> right) {
if (right.getExactSizeIfKnown() == 0)
return (S) this;
Spliterator<T> left = spliterator();
Spliterator<T> result;
if (left.getExactSizeIfKnown() == 0)
result = (Spliterator<T>) right;
else
result = new IfEmptySpliterator<>(left, right);
context = context.combine(other);
return supply(result);
}

abstract S supply(Stream<T> stream);

abstract S supply(Spliterator<T> spliterator);
Expand Down Expand Up @@ -1064,6 +1078,29 @@ public S prepend(Stream<? extends T> other) {
return prependSpliterator(other, other.spliterator());
}

/**
* Returns a stream which contents is the same as this stream, except the case when
* this stream is empty. In this case its contents is replaced with other stream contents.
*
* <p>
* The other stream will not be traversed if this stream is not empty.
*
* <p>
* If this stream is parallel and empty, the other stream is not guaranteed to be parallelized.
*
* <p>
* This is a <a href="package-summary.html#StreamOps">quasi-intermediate
* operation</a>.
*
* @param other other stream to replace the contents of this stream if this stream is empty.
* @return the stream which contents is replaced by other stream contents only if
* this stream is empty.
* @since 0.6.6
*/
public S ifEmpty(Stream<? extends T> other) {
return ifEmpty(other, other.spliterator());
}

/**
* Returns a {@link List} containing the elements of this stream. The
* returned {@code List} is guaranteed to be mutable, but there are no
Expand Down
99 changes: 99 additions & 0 deletions src/main/java/one/util/streamex/IfEmptySpliterator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package one.util.streamex;

import java.util.Spliterator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

/* package */ class IfEmptySpliterator<T> extends StreamExInternals.CloneableSpliterator<T, IfEmptySpliterator<T>> {
// alt == null --> decision is made
private Spliterator<? extends T> spltr, alt;
// positive = number of non-exhausted spliterators; negative = surely non-empty
private final AtomicInteger state = new AtomicInteger(1);

public IfEmptySpliterator(Spliterator<? extends T> spltr, Spliterator<? extends T> alt) {
this.spltr = spltr;
this.alt = alt;
}

void tryInit() {
if (alt != null && spltr.hasCharacteristics(SIZED)) {
if (spltr.estimateSize() == 0) {
spltr = alt;
}
alt = null;
}
}

@Override
public boolean tryAdvance(Consumer<? super T> action) {
if (alt != null) {
if (spltr.tryAdvance(action)) {
state.set(-1);
alt = null;
return true;
}
if (drawState()) {
spltr = alt;
}
alt = null;
}
return spltr.tryAdvance(action);
}

@Override
public void forEachRemaining(Consumer<? super T> action) {
tryInit();
if (alt == null) {
spltr.forEachRemaining(action);
} else {
boolean[] empty = {true};
spltr.forEachRemaining(e -> {
empty[0] = false;
action.accept(e);
});
if (empty[0]) {
if (drawState()) {
(spltr = alt).forEachRemaining(action);
}
} else {
state.set(-1);
}
alt = null;
}
}

boolean drawState() {
return state.updateAndGet(x -> x > 0 ? x - 1 : x) == 0;
}

@Override
public Spliterator<T> trySplit() {
Spliterator<? extends T> prefix = spltr.trySplit();
if (prefix == null) {
return null;
}
tryInit();
if (alt != null) {
state.updateAndGet(x -> x > 0 ? x + 1 : x);
}
IfEmptySpliterator<T> clone = doClone();
clone.spltr = prefix;
return clone;
}

@Override
public long estimateSize() {
tryInit();
long size = spltr.estimateSize();
return alt != null && size == 0 ? alt.estimateSize() : size;
}

@Override
public int characteristics() {
if(alt == null) {
return spltr.characteristics() & (~SORTED);
}
return (spltr.characteristics() & alt.characteristics() & (~SORTED)) |
((spltr.characteristics() | alt.characteristics()) & (ORDERED));
}
}
17 changes: 17 additions & 0 deletions src/main/java/one/util/streamex/StreamEx.java
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,23 @@ public StreamEx<T> prepend(Collection<? extends T> collection) {
return prependSpliterator(null, collection.spliterator());
}

/**
* Returns a stream which contents is the same as this stream, except the case when
* this stream is empty. In this case its contents is replaced with supplied values.
*
* <p>
* This is a <a href="package-summary.html#StreamOps">quasi-intermediate
* operation</a>.
*
* @param values values to replace the contents of this stream if this stream is empty.
* @return the stream which contents is replaced by supplied values only if this stream is empty.
* @since 0.6.6
*/
@SafeVarargs
public final StreamEx<T> ifEmpty(T... values) {
return ifEmpty(null, Spliterators.spliterator(values, Spliterator.ORDERED));
}

/**
* Returns true if this stream contains the specified value.
*
Expand Down
34 changes: 34 additions & 0 deletions src/test/java/one/util/streamex/IfEmptySpliteratorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package one.util.streamex;

import static one.util.streamex.TestHelpers.*;
import org.junit.Test;

import java.util.Collections;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Supplier;

public class IfEmptySpliteratorTest {
@Test
public void testSpliterator() {
List<Integer> data = IntStreamEx.range(1000).boxed().toList();
checkSpliterator("++", data, () -> new IfEmptySpliterator<>(data.spliterator(), data.spliterator()));
checkSpliterator("-+", data, () -> new IfEmptySpliterator<>(Spliterators.emptySpliterator(), data.spliterator()));
checkSpliterator("+-", data, () -> new IfEmptySpliterator<>(data.spliterator(), Spliterators.emptySpliterator()));
checkSpliterator("--", Collections.emptyList(), () -> new IfEmptySpliterator<>(Spliterators.emptySpliterator(), Spliterators.emptySpliterator()));
}

@Test
public void testFiltered() {
List<Integer> data = IntStreamEx.range(1000).boxed().toList();
Supplier<Spliterator<Integer>> allMatch = () -> data.parallelStream().filter(x -> x >= 0).spliterator();
Supplier<Spliterator<Integer>> noneMatch = () -> data.parallelStream().filter(x -> x < 0).spliterator();
Supplier<Spliterator<Integer>> lastMatch = () -> data.parallelStream().filter(x -> x == 999).spliterator();
checkSpliterator("++", data, () -> new IfEmptySpliterator<>(allMatch.get(), allMatch.get()));
checkSpliterator("l+", Collections.singletonList(999), () -> new IfEmptySpliterator<>(lastMatch.get(), allMatch.get()));
checkSpliterator("-+", data, () -> new IfEmptySpliterator<>(noneMatch.get(), allMatch.get()));
checkSpliterator("+-", data, () -> new IfEmptySpliterator<>(allMatch.get(), noneMatch.get()));
checkSpliterator("--", Collections.emptyList(), () -> new IfEmptySpliterator<>(noneMatch.get(), noneMatch.get()));
}
}
23 changes: 23 additions & 0 deletions src/test/java/one/util/streamex/StreamExTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2069,4 +2069,27 @@ public void testIntersperse() {
streamEx(asList("a", "b", "c", "d", "e")::stream, s -> assertEquals(expected, s.get().intersperse("--").toList()));
assertEquals(Collections.emptyList(), StreamEx.empty().intersperse("xyz").toList());
}

@Test
public void testIfEmpty() {
repeat(10, n -> streamEx(asList(1,2,3,4,5,6)::stream, s -> {
assertEquals("123456", s.get().ifEmpty(7,8,9).joining());
assertEquals("123456", s.get().filter(x -> x > 0).ifEmpty(7,8,9).joining());
assertEquals("6", s.get().filter(x -> x > 5).ifEmpty(7,8,9).joining());
assertEquals("789", s.get().filter(x -> x < 0).ifEmpty(7,8,9).joining());
assertEquals("123456", s.get().ifEmpty(s.get()).joining());
assertEquals("123456", s.get().filter(x -> x > 0).ifEmpty(s.get().filter(x -> x % 2 == 1)).joining());
assertEquals("135", s.get().filter(x -> x < 0).ifEmpty(s.get().filter(x -> x % 2 == 1)).joining());
assertEquals("", s.get().filter(x -> x < 0).ifEmpty(s.get().filter(x -> x < 0)).joining());

assertEquals(Optional.of(1), s.get().ifEmpty(7,8,9).findFirst());
assertEquals(Optional.of(1), s.get().filter(x -> x > 0).ifEmpty(7,8,9).findFirst());
assertEquals(Optional.of(6), s.get().filter(x -> x > 5).ifEmpty(7,8,9).findFirst());
assertEquals(Optional.of(7), s.get().filter(x -> x < 0).ifEmpty(7,8,9).findFirst());
assertEquals(Optional.of(1), s.get().ifEmpty(s.get()).findFirst());
assertEquals(Optional.of(1), s.get().filter(x -> x > 0).ifEmpty(s.get().filter(x -> x % 2 == 1)).findFirst());
assertEquals(Optional.of(1), s.get().filter(x -> x < 0).ifEmpty(s.get().filter(x -> x % 2 == 1)).findFirst());
assertEquals(Optional.empty(), s.get().filter(x -> x < 0).ifEmpty(s.get().filter(x -> x < 0)).findFirst());
}));
}
}
8 changes: 8 additions & 0 deletions src/test/java/one/util/streamex/TestHelpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,14 @@ static <T> void checkSpliterator(String msg, Supplier<Spliterator<T>> supplier)
*/
static <T> void checkSpliterator(String msg, List<T> expected, Supplier<Spliterator<T>> supplier) {
List<T> seq = new ArrayList<>();

// Test characteristics
Spliterator<T> forCharacteristics = supplier.get();
if(forCharacteristics.hasCharacteristics(Spliterator.SORTED)) {
forCharacteristics.getComparator(); // must not fail
}
assertTrue(forCharacteristics.estimateSize() >= 0);

// Test forEachRemaining
Spliterator<T> sequential = supplier.get();
sequential.forEachRemaining(seq::add);
Expand Down
16 changes: 16 additions & 0 deletions streamex.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
</component>
</module>

0 comments on commit 64b8767

Please sign in to comment.