Skip to content

dsaborg/sequence

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sequence

A lightweight alternative to Java 8 sequential Stream

Maven Central Travis Codecov Gitter

News

2017-02-21 - Sequence v2.3 is released, with optimizations to size and the ability to tell whether a sequence has a defined size or is infinite or unsized through the new sizeIfKnown and sizeType methods. Adds mapLeft / mapRight to BiSequence and mapKeys / mapValues to EntrySequence. Adds argument checking for fail-fast behaviour under incorrect Sequence usage. Brings test coverage of the entire project up to 100% on codecov.io. Adds limitTail to all sequences. Adds groupBy operations to Sequence, and toGroupedMap to EntrySequence and BiSequence. Adds sum, average and statistics to LongSequence, DoubleSequence and IntSequence. Performance optimizations, improved strictness checks, and improved error condition behaviour in primitive collections.

Overview

The Sequence library is a leaner alternative to sequential Java 8 Streams, used in similar ways but with a lighter step, and with better integration with the rest of Java. It has no external dependencies and will not slow down your build.

Sequences use Java 8 lambdas in much the same way as Streams do, but is based on readily available Iterables instead of a black box pipeline, and is built for convenience and compatibility with the rest of Java. It's for programmers wanting to perform every day data processing tasks on moderately sized collections. Sequences go to great lengths to be as lazy and late-evaluating as possible, with minimal overhead.

Sequence aims to be roughly feature complete with sequential Streams, with additional convenience methods for advanced traversal and transformation. In particular it allows easier collecting into common Collections without Collectors, better handling of Maps with Pairs and Map.Entry as first-class citizens, tighter integration with the rest of Java by being implemented in terms of Iterable, and advanced partitioning, mapping and filtering methods, for example peeking at previous or next elements during traversal.

Not being parallel in nature allows more advanced operations on the sequence which rely on traversing elements in Iterator order. If you need parallel iteration or are processing over 1 million or so entries, you might benefit from using a parallel Stream instead.

List<String> evens = Sequence.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                             .filter(x -> x % 2 == 0)
                             .map(Object::toString)
                             .toList();

assertThat(evens, contains("2", "4", "6", "8"));

See also: Sequence#of(T...), Sequence#from(Iterable), Sequence#filter(Predicate), Sequence#map(Function), Sequence#toList()

Unlike Stream, Sequences are directly backed by the underlying storage, allowing direct manipulation of the Collection the sequence is based on to the extent permitted by the combination of operations applied on the sequence, as well as directly reflecting outside changes to the underlying collection. See Updating for more information.

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

Sequence.from(list).filter(x -> x % 2 != 0).clear();

assertThat(list, contains(2, 4));

The Sequence library is protected by over 5000 tests providing 100% branch and line coverage of all classes in the project. Javadoc for the entire project is available at the Sequence javadoc.io Page.

Install

The Sequence library is available for manual install or as a maven central dependency for maven and gradle.

Manual

For manually installable releases, check out the GitHub Releases Page.

Maven

To install in maven, use the maven central dependency:

<dependency>
  <groupId>org.d2ab</groupId>
  <artifactId>sequence</artifactId>
  <version>[2.3,3.0)</version>
</dependency>

Gradle

To install in gradle, use the maven central dependency:

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.d2ab:sequence:[2.3,3.0)'
}

Javadoc

Javadoc for the entire project is available at the Sequence javadoc.io Page.

The main Sequence package is org.d2ab.sequence where all the sequences reside. There are seven kinds of Sequences, each dealing with a different type of entry. The first is the regular Sequence which is the general purpose stream of items. EntrySequence and BiSequence work directly on the constituent components of Map.Entry and Pair objects. The last four are primitive sequences dealing with char, int, long and double primitives; CharSeq, IntSequence, LongSequence, and DoubleSequence. These work much the same as the regular Sequence except they're adapted to work directly on primitives.

Usage

Iterable

Because each Sequence is an Iterable you can re-use them safely after you have already traversed them, as long as they're not backed by an Iterator or Stream which can only be traversed once.

Sequence<Integer> digits = Sequence.ints(); // all integer digits starting at 1

// using sequence of ints first time to get 5 odd numbers
Sequence<Integer> odds = digits.step(2).limit(5);
assertThat(odds, contains(1, 3, 5, 7, 9));

// re-using the same sequence of digits again to get squares of numbers between 4 and 8
Sequence<Integer> squares = digits.startingFrom(4).endingAt(8).map(i -> i * i);
assertThat(squares, contains(16, 25, 36, 49, 64));

See also: Sequence#range(int, int), Sequence#ints(), Sequence#intsFromZero(), Sequence#filter(Predicate), Sequence#map(Function), Sequence#step(long), Sequence#limit(long), Sequence#skip(long), Sequence#startingFrom(T), Sequence#endingAt(T)

Foreach

Because each Sequence is an Iterable they work beautifully in foreach loops:

Sequence<Integer> sequence = Sequence.ints().limit(5);

int expected = 1;
for (int each : sequence)
    assertThat(each, is(expected++));

assertThat(expected, is(6));

FunctionalInterface

Because Sequence is a @FunctionalInterface requiring only the iterator() method of Iterable to be implemented, it's very easy to create your own full-fledged Sequence instances that can be operated on like any other Sequence through the default methods on the interface that carry the bulk of the burden. In fact, this is how Sequence's own factory methods work. You could consider all of Sequence to be a smarter version of Iterable.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

// Sequence as @FunctionalInterface of list's Iterator
Sequence<Integer> sequence = list::iterator;

// Operate on sequence as any other sequence using default methods
Sequence<String> transformed = sequence.map(Object::toString);

assertThat(transformed.limit(3), contains("1", "2", "3"));

Caching

Sequences can be created from Iterators or Streams but can then only be passed over once.

Iterator<Integer> iterator = Arrays.asList(1, 2, 3, 4, 5).iterator();

Sequence<Integer> sequence = Sequence.once(iterator);

assertThat(sequence, contains(1, 2, 3, 4, 5));
assertThat(sequence, is(emptyIterable()));

See also: Sequence#once(Iterator), Sequence#once(Stream)

If you have an Iterator or Stream and wish to convert it to a full-fledged multi-iterable Sequence, use the caching methods on Sequence.

Iterator<Integer> iterator = Arrays.asList(1, 2, 3, 4, 5).iterator();

Sequence<Integer> cached = Sequence.cache(iterator);

assertThat(cached, contains(1, 2, 3, 4, 5));
assertThat(cached, contains(1, 2, 3, 4, 5));

See also: Sequence#cache(Iterable), Sequence#cache(Iterator), Sequence#cache(Stream)

Updating

Sequences have full support for updating the underlying collection where possible, through Iterator#remove(), by modifying the underlying collection directly (in between iterations), and by using Collection methods directly on the Sequence itself.

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

Sequence.from(list).filter(x -> x % 2 != 0).clear();

assertThat(list, contains(2, 4));
List<Integer> list = new ArrayList<>(Lists.of(1, 2, 3, 4, 5));
Sequence<String> evens = Sequence.from(list)
                                       .filter(x -> x % 2 == 0)
                                       .biMap(Object::toString, Integer::parseInt);

assertThat(evens, contains("2", "4"));

evens.add("6"); // biMap allows adding back to underlying collection
expecting(IllegalArgumentException.class, () -> evens.add("7")); // cannot add filtered out item

assertThat(evens, contains("2", "4", "6"));
assertThat(list, contains(1, 2, 3, 4, 5, 6));

Size

Since Sequence implements Collection it provides a fully functioning size() method, however note that this method may degrade to O(n) performance if the size is not known a-priori and thus needs to be calculated by traversing the Sequence. In some cases, the size can be calculated in advance and Sequence makes full use of this:

Sequence<Integer> repeated = Sequence.of(1, 2, 3).repeat().limit(5);
assertThat(repeated, contains(1, 2, 3, 1, 2));
assertThat(repeated.size(), is(5)); // immediate return value
assertThat(repeated.sizeIfKnown(), is(5)); // would return -1 if unknown in advance

While Sequence goes to great lengths to be able to detect the size in advance, in many cases the size can not be calculated in advance and so Sequence must traverse the list of elements to calculate the size, degrading to O(n) performance for the size() operation:

List<Integer> growingList = new ArrayList<Integer>() {
    @Override
    public Iterator<Integer> iterator() {
        add(size() + 1);
        return super.iterator();
    }
};

Sequence<Integer> repeated = Sequence.from(growingList).repeat().limit(10);
assertThat(repeated, contains(1, 1, 2, 1, 2, 3, 1, 2, 3, 4));
assertThat(repeated.size(), is(10)); // O(n) traversal of elements required
assertThat(repeated.sizeIfKnown(), is(-1)); // cannot determine size in advance as collections can mutate

Streams

Sequences interoperate beautifully with Stream, through the once(Stream) and .stream() methods.

Sequence<String> paired = Sequence.once(Stream.of("a", "b", "c", "d")).pairs().flatten();

assertThat(paired.stream().collect(Collectors.toList()), contains("a", "b", "b", "c", "c", "d"));

See also: Sequence#once(Stream), Sequence#cache(Stream), Sequence#stream()

Recursion

There is full support for infinite recursive Sequences, including termination at a known value.

Sequence<Integer> fibonacci = BiSequence.recurse(0, 1, (i, j) -> Pair.of(j, i + j)).toSequence((i, j) -> i);

assertThat(fibonacci.endingAt(34), contains(0, 1, 1, 2, 3, 5, 8, 13, 21, 34));
Exception exception = new IllegalStateException(new IllegalArgumentException(new NullPointerException()));

Sequence<Throwable> exceptionAndCauses = Sequence.recurse(exception, Throwable::getCause).untilNull();

assertThat(exceptionAndCauses, contains(instanceOf(IllegalStateException.class),
                                        instanceOf(IllegalArgumentException.class),
                                        instanceOf(NullPointerException.class)));
Iterator<String> delimiter = Sequence.of("").append(Sequence.of(", ").repeat()).iterator();

StringBuilder joined = new StringBuilder();
for (String number : Arrays.asList("One", "Two", "Three"))
    joined.append(delimiter.next()).append(number);

assertThat(joined.toString(), is("One, Two, Three"));
CharSeq hexGenerator = CharSeq.random("0-9", "A-F").limit(8);

String hexNumber1 = hexGenerator.asString();
String hexNumber2 = hexGenerator.asString();

assertTrue(hexNumber1.matches("[0-9A-F]{8}"));
assertTrue(hexNumber2.matches("[0-9A-F]{8}"));
assertThat(hexNumber1, is(not(hexNumber2)));

See also: BiSequence, BiSequence#recurse(L, R, BiFunction), BiSequence#toSequence(BiFunction), Pair, Pair#of(T, U), Sequence#recurse(T, UnaryOperator), Sequence#generate(Supplier), Sequence#repeat(), Sequence#repeat(long), Sequence#until(T), Sequence#endingAt(T), Sequence#untilNull(T), Sequence#until(Predicate), Sequence#endingAt(Predicate)

Reduction

The standard reduction operations are available as per Stream:

Sequence<Long> thirteen = Sequence.longs().limit(13);

long factorial = thirteen.reduce(1L, (r, i) -> r * i);

assertThat(factorial, is(6227020800L));

See also: Sequence#reduce(BinaryOperator), Sequence#reduce(T, BinaryOperator)

Maps

Maps are handled as Sequences of Entry, with special transformation methods that convert to/from Maps.

Sequence<Integer> keys = Sequence.of(1, 2, 3);
Sequence<String> values = Sequence.of("1", "2", "3");

Map<Integer, String> map = keys.interleave(values).toMap();

assertThat(map, is(equalTo(Maps.builder(1, "1").put(2, "2").put(3, "3").build())));

See also: Sequence#interleave(Iterable), Sequence#pairs(), Sequence#adjacentPairs(), Pair, Sequence#toMap(), Sequence#toMap(Function, Function), Sequence#toSortedMap(), Sequence#toSortedMap(Function, Function)

You can also map Entry Sequences to Pairs which allows more expressive transformation and filtering.

Map<String, Integer> map = Maps.builder("1", 1).put("2", 2).put("3", 3).put("4", 4).build();

Sequence<Pair<String, Integer>> sequence = Sequence.from(map)
                                                   .map(Pair::from)
                                                   .filter(pair -> pair.test((s, i) -> i != 2))
                                                   .map(pair -> pair.map((s, i) -> Pair.of(s + " x 2", i * 2)));

assertThat(sequence.toMap(), is(equalTo(Maps.builder("1 x 2", 2).put("3 x 2", 6).put("4 x 2", 8).build())));

See also: Pair, Pair#of(T, U), Pair#from(Entry), Pair#test(BiPredicate), Pair#map(BiFunction)

You can also work directly on Entry keys and values using EntrySequence.

Map<String, Integer> original = Maps.builder("1", 1).put("2", 2).put("3", 3).put("4", 4).build();

EntrySequence<Integer, String> oddsInverted = EntrySequence.from(original)
                                                           .filter((k, v) -> v % 2 != 0)
                                                           .map((k, v) -> Maps.entry(v, k));

assertThat(oddsInverted.toMap(), is(equalTo(Maps.builder(1, "1").put(3, "3").build())));

See also: EntrySequence, EntrySequence#from(Map), EntrySequence#filter(BiPredicate), EntrySequence#map(BiFunction), EntrySequence#toSequence(BiFunction), Maps#entry(K, V), EntrySequence#toMap()

Pairs

When iterating over sequences of Pairs of item, BiSequence provides native operators and transformations:

BiSequence<String, Integer> presidents = BiSequence.ofPairs("Abraham Lincoln", 1861, "Richard Nixon", 1969,
                                                            "George Bush", 2001, "Barack Obama", 2005);

Sequence<String> joinedOffice = presidents.toSequence((n, y) -> n + " (" + y + ")");

assertThat(joinedOffice, contains("Abraham Lincoln (1861)", "Richard Nixon (1969)", "George Bush (2001)",
                                  "Barack Obama (2005)"));

See also: BiSequence, BiSequence#from(Map), BiSequence#filter(BiPredicate), BiSequence#map(BiFunction), BiSequence#toSequence(BiFunction), BiSequence#toMap()

Primitive

There are also primitive versions of Sequence for char, int, long and double processing: CharSeq, IntSequence, LongSequence and DoubleSequence.

CharSeq snakeCase = CharSeq.from("Hello Lexicon").map(c -> (c == ' ') ? '_' : c).map(Character::toLowerCase);

assertThat(snakeCase.asString(), is("hello_lexicon"));
IntSequence squares = IntSequence.positive().map(i -> i * i);

assertThat(squares.limit(5), contains(1, 4, 9, 16, 25));
LongSequence negativeOdds = LongSequence.negative().step(2);

assertThat(negativeOdds.limit(5), contains(-1L, -3L, -5L, -7L, -9L));
DoubleSequence squareRoots = IntSequence.positive().toDoubles().map(Math::sqrt);

assertThat(squareRoots.limit(3), contains(sqrt(1), sqrt(2), sqrt(3)));

See also: CharSeq, IntSequence, LongSequence, DoubleSequence Sequence#toChars(ToCharFunction) Sequence#toInts(ToIntFunction) Sequence#toLongs(ToLongFunction) Sequence#toDoubles(ToDoubleFunction)

Peeking

Sequences also have mapping and filtering methods that peek on the previous and next elements:

CharSeq titleCase = CharSeq.from("hello_lexicon")
                           .mapBack('_', (prev, x) -> prev == '_' ? toUpperCase(x) : x)
                           .map(c -> (c == '_') ? ' ' : c);

assertThat(titleCase.asString(), is("Hello Lexicon"));

See also: Sequence#peekBack(BiConsumer), Sequence#peekForward(BiConsumer), Sequence#filterBack(BiPredicate), Sequence#filterForward(BiPredicate), Sequence#mapBack(BiFunction), Sequence#mapForward(BiFunction)

Partitioning

Both regular and primitive Sequences have advanced windowing and partitioning methods, allowing you to divide up Sequences in various ways, including a partitioning method that uses a binary predicate to determine which elements to create a batch between.

Sequence<Sequence<Integer>> batched = Sequence.of(1, 2, 3, 4, 5, 6, 7, 8, 9).batch(3);

assertThat(batched, contains(contains(1, 2, 3), contains(4, 5, 6), contains(7, 8, 9)));
String vowels = "aeoiuy";

Sequence<String> consonantsVowels = CharSeq.from("terrain")
                                           .batch((a, b) -> (vowels.indexOf(a) < 0) != (vowels.indexOf(b) < 0))
                                           .map(CharSeq::asString);

assertThat(consonantsVowels, contains("t", "e", "rr", "ai", "n"));

See also: Sequence#window(int), Sequence#window(int, int), Sequence#batch(int), Sequence#batch(BiPredicate), Sequence#split(T), Sequence#split(Predicate)

Reading

Primitive sequences can be read from Readers or InputStreams into a CharSeq or IntSequence respective. These can also be converted back to Readers and InputStreams respectively, allowing for filtering or transformation of these streams.

Reader reader = new StringReader("hello world\ngoodbye world\n");

Sequence<String> titleCase = CharSeq.read(reader)
                                    .mapBack('\n', (prev, x) -> isWhitespace(prev) ? toUpperCase(x) : x)
                                    .split('\n')
                                    .map(phrase -> phrase.append('!'))
                                    .map(CharSeq::asString);

assertThat(titleCase, contains("Hello World!", "Goodbye World!"));

reader.close(); // sequence does not close reader
String original = "hello world\ngoodbye world\n";

BufferedReader transformed = new BufferedReader(CharSeq.from(original).map(Character::toUpperCase).asReader());

assertThat(transformed.readLine(), is("HELLO WORLD"));
assertThat(transformed.readLine(), is("GOODBYE WORLD"));

transformed.close();
InputStream inputStream = new ByteArrayInputStream(new byte[]{0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF});

String hexString = IntSequence.read(inputStream)
                              .toSequence(Integer::toHexString)
                              .map(String::toUpperCase)
                              .join();

assertThat(hexString, is("DEADBEEF"));

inputStream.close();

See also: CharSeq#read(Reader), IntSequence#read(InputStream)

Conclusion

Go ahead and give it a try and experience a leaner way to Stream your Sequences! :bowtie:

Copyright © 2016-2017 Daniel Skogquist Åborg (d2ab.org). Licensed under the Apache License, Version 2.0.

Your feedback is welcome! For comments, feature requests or bug reports, use the GitHub Issues Page or email me at daniel@d2ab.org.

Developed with IntelliJ IDEA. ❤️

About

A lightweight alternative to Java 8 sequential Stream

Resources

License

Stars

Watchers

Forks

Packages

No packages published