diff --git a/.gitignore b/.gitignore index 4827db8..06e6b81 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ .project .settings/ /target/ +.*.html diff --git a/README.md b/README.md index 7507cba..6f6f878 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,30 @@ # Chainable +![GitHub](https://img.shields.io/github/license/chainables/chainable) +![Maven Central](https://img.shields.io/maven-central/v/com.github.chainables/chainable) +![GitHub top language](https://img.shields.io/github/languages/top/chainables/chainable) +![GitHub last commit](https://img.shields.io/github/last-commit/chainables/chainable) +![Lines of code](https://img.shields.io/tokei/lines/github/chainables/chainable) + > :warning: Under construction / Work in progress / Coming soon... -## Overview +- [Summary](#summary) +- [System Requirements](#system-requirements) +- [Usage (Maven)](#usage-maven) +- [Overview](#overview) + - [Highlights](#highlights) + - [Sequence Processing](#sequence-processing) + - [Tree Processing](#tree-processing) + - [Chainable vs Java Stream](#chainable-vs-java-stream) +- [Examples](#examples) + - [Chains](#chain-examples) + - [Trees](#tree-examples) + +## Summary -`Chainable` is an `Iterable`-based alternative to Java's `Stream` and Google's *guava* focused on delivering richer fluent API for sequence and tree processing. It is heavily inspired by the **iterator pattern**, **functional programming**, **lazy evaluation** and C#'s `Enumerable`, but also extended into areas of functionality not addressed by older approaches. It is intended to enable writing code that is more succinct, readable, simpler to implement, and sometimes faster than its non-lazy/non-functional equivalents. +*Chainable* is intended to be a rich, `Iterable`-based alternative to Java's `Stream` and Google's *guava*, but focused on sequence and tree processing in particular, using lambdas and command chaining. It is heavily inspired by the iterator pattern, functional programming, lazy evaluation and C#'s `Enumerable`, but also extended into areas of functionality not addressed by older approaches. It is designed to enable writing powerful yet readable code quickly, succinctly, and performing sometimes faster than its non-lazy/non-functional equivalents. + +The implementation is lightweight and self-contained, i.e. it has no external dependencies, so as not to contribute to any sub-dependency versioning challenges. ```java Chainable chain = Chainable @@ -23,76 +43,198 @@ assertEquals("NIAHC", textBackwards); ``` -The implementation is lightweight and self-contained, i.e. it has no external dependencies, so as not to contribute to any sub-dependency versioning challenges. +## System requirements +![GitHub top language](https://img.shields.io/github/languages/top/chainables/chainable) -(A note on the terminology: `Chainable` is the interface, whereas the word *"chain"* is used throughout the documentation to refer to specific instances of `Chainable`). +- Java 8+ -### Chainable vs Java Stream +## Usage (Maven) +![Maven Central](https://img.shields.io/maven-central/v/com.github.chainables/chainable) + +Add this to your POM's ``: + +```xml + + com.github.chainables + chainable + [0.5,) + +``` + +## Getting Started + +A simple starting **chain** can be created using one of the factory methods on `Chainable`, such as [`from()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#from-T...-) or [`empty()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#empty-java.lang.Class-). + +
Example... + + ```java + // From pre-defined values + Chainable chain = Chainable.from("a", "b", "c"); + + // Empty but expecting String items + Chainable chain = Chainable.empty(String.class); -Although `Chainable` overlaps with Java's `Stream` in some areas of functionality, the design of `Chainable` is optimized for a somewhat different set of goals and aligns more with C#'s `Enumerable`'s LINQ-style methods than Java streams. Also, *chainable* provides functional programming-based API for trees, seamlessly integrated with its sequence processing API, as well as other (future) basic data structures. + // From an existing Iterable + Chainable chain = Chainable.from(existingIterable); -Another key difference from `Stream` is that `Chainable` fully preserves the functional and re-entrancy semantics of `Iterable`, that is it can be traversed multiple times, with multiple iterator instantiations, whereas Java's built-in `Stream` can be traversed only once. + // From an existing Stream + Chainable chain = Chainable.from(existingStream); + ``` -The `Chainable` API surface also exposes various additional convenience methods for sequential chain processing with functional programming, and not so much oriented toward the parallelism that was a key guiding design principle behind Java's `Stream`. Also, some of the overlapping APIs are only available in Java streams starting with Java 9, whereas `Chainable` is fully functional starting with Java 8. +
-Having said that, a level of interoperability between `Stream` and `Chainable` exists: a chain can be created from a stream (see `Chainable#from(Stream)`, and vice-versa (see `Chainable#stream()`). +A simple **tree** can be created using the [`withRoot()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/ChainableTree.html#withRoot-T-) factory method. + +
Example... + + ```java + // Example tree of String values + ChainableTree tree = ChainableTree.withRoot("root"); + ``` + +and then child sub-trees can be assigned to it either: + - as explicitly pre-defined trees or values: + + ```java + // Assign explicit child subtrees + tree.withChildren( + ChainableTree + .withRoot("1") + .withChildren("1.1", 1.2"), + ChainableTree + .withRoot("2") + .withChildren("2.1", "2.2")); + ``` + + - or dynamically, using functional programming by providing a child-extracting lambda -- see the [tree processing example](#tree-processing), or more in the [Tree Examples](#tree-examples) section. +
+ +## Overview ### Highlights -Besides the part of the API overlapping with streams, current highlights of `Chainable` include: +In general, some of the current key highlights of `Chainable` include: #### Sequence processing + > :warning: Section under construction as the API is under active development/at pre-release stage. -- **interleaving** (see `Chainable#interleave`) - two or more chains that have their own evaluation logic can be interleaved, -so that subsequent chain can apply to their outputs in a quasi-parallel (or sequential round-robin) fashion, so as not to have a bias toward one chain first, while still not actually being concurrent. -![Interleave](./src/main/java/doc-files/img/interleave.png) + - **single pass caching** - By default, each re-iteration over a given chain re-evaluates the specified lambdas. But using the [`cached()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#cached--) method, you can create a chain that is lazily evaluated only on the first complete pass, when it is iterated all the way to the end, assuming it is not infinite. From then on, subsequent iterations over the same chain would only navigate through the internally cached outputs of that initial pass, no longer evaluating the provided lambdas. This means the cached chain, upon subsequent traversals, starts behaving de-facto like a `List`. -- **breadth-first/depth-first traversal** - enabling tree-like traversals of a chain of items, where children of an item are dynamically added by the caller-specified child extractor and traversed either breadth-first (queue-like, `Chainable#breadthFirst()`) or depth-first (stack-like, `Chainable#depthFirst()`), both in a lazy fashion. + - **interleaving** - Two or more chains that have their own evaluation logic can be interleaved using the [`interleave()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#interleave-java.lang.Iterable...-) method, +so that a subsequent chain can apply its logic to their outputs in a quasi-parallel (or sequential round-robin) fashion, while still not actually being concurrent. -- **single pass caching** - by default, each re-iteration over a given chain re-evaluates the lambdas, just like in a typical `Iterable`. But it is possible to create a chain that is lazy-evaluated only on the first pass, i.e. when it is iterated all the way to the end (see `Chainable#cached()`). From then on, subsequent iterations over the same chain would only navigate through the internally cached outputs of that initial pass, no longer evaluating the provided lambdas. That means the cached chain, upon subsequent traversals, starts behaving like a collection. + ![Interleave](./src/main/java/doc-files/img/interleave.png) -- **disjunctive filtering** - you can specify one or more filter predicates at the same time (see `Chainable#whereEither`), with disjuctive (logical-OR) semantics. This means you can define specific filtering predicates for specific purposes and then just supply them all as parameters, rather than having to create yet another predicate that's an *OR* of the others. + - **breadth-first / depth-first traversal** - You can achieve a tree-like traversal of a chain, where children of each item extracted by the child-extracting lambda are inserted immediately ahead [`depthFirst()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#depthFirst-java.util.function.Function-) or appended to the end of the chain [`breadthFirst()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#breadthFirst-java.util.function.Function-), thereby resulting in a pre-order/depth-first or breadth-first traversal respectively. -- **skipping** of the leading sub-chain of items under various scenarios, e.g.: - - skip *as long as* they satisfy a condition (`Chainable#notAsLongAs()`) - - skip *before* they satisfy a condition (`Chainable#notBefore()`) + - **disjunctive filtering** - Using the [`whereEither()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#whereEither-java.util.function.Predicate...-) method, you can specify one or more filter predicates at the same time, with disjunctive (logical-OR) semantics. This means you can define specific filtering predicates for specific purposes and then just supply them all as parameters, rather than having to create yet another predicate that's an *OR* of the others. + + - **skipping** of the leading sub-chain of items under various scenarios, e.g.: + - skip as long as they satisfy a condition using [`notAsLongAs()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#notAsLongAs-java.util.function.Predicate-) + - skip before they satisfy a condition using [`notBefore()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#notBefore-java.util.function.Predicate-) -- **trimming** of the trailing sub-chain of items under various scenario, e.g.: - - stop *as soon as* the specified conditions are satisfied (`Chainable#asLongAs()`) - - or are no longer satisfied (`Chainable#before()`.) + - **trimming** of the trailing sub-chain of items under various scenario, e.g.: + - stop right before the specified condition is satisfied using [`before()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#before-java.util.function.Predicate-) + - or as soon as it is no longer satisfied using [`asLongAs()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#asLongAs-java.util.function.Predicate-) -- **equality and sub-array containment** checks, but evaluated lazily, i.e. chains failing the equality (`Chainable#equals`) or sub-array containment (`Chainable#containsSubarray`) tests return quickly, without traversing/evaluating the rest of the chain. + - **equality and sub-array containment checks**, evaluated lazily. Chain comparison using the equality [`equals()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#equals-java.lang.Iterable-) test or the sub-array containment [`containsSubarray()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#containsSubarray-java.lang.Iterable-) tests return quickly, without traversing/evaluating the rest of the chain. -- chainable **string joining/splitting** operations - you can quickly get a chain of tokens or characters out of a string (see `Chainables#split()`, process it using `Chainable` APIs and go back to a string (see `Chainable#join()`). + - chainable **string joining/splitting** operations - You can get a chain of tokens or characters out of a string with `Chainable`'s [`split()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainables.html#split-java.lang.String-java.lang.String-boolean-) method, process it using various `Chainable` APIs and go back to a string using [`join()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#join-java.lang.String-). + #### Tree processing -> :triangular_flag_on_post: To do... +> :warning: Section under construction as the API is under active development/at pre-release stage. + + Tightly integrated with `Chainable`, the functional programming-based tree (trie) support [`ChainableTree`](https://www.javadoc.io/doc/com.github.chainables/chainable/latest/com/github/chainables/chainable/ChainableTree.html) is a particularly distinguishing feature of the *Chainables* library. A chainable tree can be defined using a lazily-evaluated lambda in the [`withChildValueExtractor()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/ChainableTree.html#withChildValueExtractor-java.util.function.Function-) method, which does not evaluate/traverse the children of a given parent until necessary. A number of capabilities stem from this: -### Performance Considerations + - **infinite trees**, or trees of infinite depth, can be easily defined in terms of children-generating lambdas. For example, the code below defines a lazily evaluated infinite tree made of all the possible permutations of the letters *a*, *b* and *c*, where each layer of the tree consists of nodes of increasingly longer strings: -In general, although Java (as of v8) supports a notion of functional programming with lambdas, Java is known not to be optimized for functional programming's performance. Hence, in scenarios where code speed is a topmost priority, this style of programming may not be preferable, regardless of whether `Chainable` or Java's `Stream` is considered. +```java + char[] alphabet = { 'a', 'b', 'c' }; // Define alphabet to take letters from + ChainableTree permutations = ChainableTree + .withRoot("") // Blank string at the root + .withChildValueExtractor(p -> Chainable + .empty(String.class) // Start with an empty chain of strings + .chainIndexed((s, i) -> p + alphabet[i.intValue()]) // Append each alphabet item to the parent + .first(alphabet.length)); // Limit the children chain to the size of the alphabet +``` -However, if most of the processing time is spent by the application on fetching data from external sources (an inherently slower process), or where the evaluation of each item in a sequence is time-consuming in general, then lazy evaluation enabled by `Chainable` can result in significant performance gains. It is in those scenarios where the benefits of functional programming begin to greatly outweigh the performance costs of Java's lambda support. (Not to mention the improved code readability and succinctness that can be achieved.) + If you were to begin to traverse this infinite tree, its initial few layers would look like this: + + ``` +- (blank root) + - a + - aa + - aaa + ... + - ab + - aba + ... + - ac + - aca + ... + - b + - ba + ... + - c + - ca + ... +``` -With this in mind, based on some initial testing in Java 8 so far, `Chainable` currently appears comparable in performance to `Streams`, outperforming it slightly in some circumstances and under-performing it slightly in some others. Although under the hood `Chainable` works quite differently from `Stream`, the biggest factor in the performance of either approach is ultimately the JVM's handling of lambdas, arguably more than anything else. This likely explains why the performance differences between the two so far are not significant (see existing benchmarks in [PerfTest.java](https://github.com/martinsawicki/chainable/blob/dev/src/test/java/com/github/martinsawicki/chainable/PerfTest.java)). + - **tree trimming** - Especially useful for infinite trees, a chainable tree can be limited in depth based on either: + - a lazily evaluated predicate condition that the lowermost descendants are to meet, using [`notBelowWhere()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/ChainableTree.html#notBelowWhere-java.util.function.Predicate-) + - or taking into consideration the depth of the tree so far, using the `BiPredicate` flavor of [`notBelowWhere()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/ChainableTree.html#notBelowWhere-java.util.function.BiPredicate-). -## System requirements + This enables methods that may result in a full traversal of the tree (such as [`firstWhere()`])https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/ChainableTree.html#firstWhere-java.util.function.Predicate-)) to *eventually* return, which they might not otherwise do if the tree is infinite. -- Java 8+ + For example, the following code, which builds on the previous example, results in a view of the previously defined tree that is limited to de-facto 3 layers of depth for any subsequent logic applied to it: + + ```java + String text = permutations + .notBelowWhere(t -> t.value().length() >= 3) // Limit permutation length to 3 letters + .breadthFirst() // Create chain from breadth-first traversal + .afterFirst() // Skip the empty root + .join(", "); + + System.out.println(text); +``` + + The beginning of the output text, which lists all the permutations of *a*, *b*, and *c*, up to 3 letters in length, will be: `a, b, c, aa, ab, ac, ba, bb, bc, ca, cb, cc, aaa, aab, ...` -## Usage (Maven) > :triangular_flag_on_post: To do... +### Chainable vs Java Stream + +Although *Chainable* overlaps with Java's `Stream` in some areas of functionality, the design of *Chainable* is optimized for a somewhat different set of goals, manifested in a number of design and functional differences: + +- Unlike streams, `Chainable` derives from `Iterable` to simplify usage inside the *for-each* flavor of the `for` loop. + +- By virtue of deriving from `Iterable`, chains are **"re-entrant"** in the sense that multiple iterators can be instantiated against the same chain. + +- Unlike Java's streams, the *Chainables* library also provides **functional programming-based API for trees** (or more specifically, *tries*), seamlessly integrated with the `Chainable` sequence processing API (see [`ChainableTree`](https://www.javadoc.io/doc/com.github.chainables/chainable/latest/com/github/chainables/chainable/ChainableTree.html)), as well as other (future) basic data structures. + +- `Chainable` supports caching of the already evaluated items in the chain, if that is what the programmer chooses to enable (see [`cached()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#cached--)). This can be especially useful if the underlying sequence is not expected to change before subsequent traversals, or if it is wrapping a Java `Stream` (which itself by definition is not re-entrant.) + +- `Chainable` exposes various additional convenience methods for sequential processing with functional programming that are not present in streams. + +- While some of the overlapping APIs in Java's `Stream` are only available starting with Java 9, `Chainable` is **fully functional starting with Java 8**. + +- `Chainable` is not (currently) oriented toward the parallelism that was a key guiding design principle behind Java's `Stream`. + +Functional and design differences with streams aside, a level of interoperability between `Stream` and `Chainable` exists: a chain can be created from a stream using [`from(Stream)`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#from-java.util.stream.Stream-), and vice-versa using [`stream()`](https://www.javadoc.io/static/com.github.chainables/chainable/0.5.2/com/github/chainables/chainable/Chainable.html#stream--). A chain wrapping a stream makes the stream *appear* reentrant, even though in reality it is traversed only once under the hood. That is because the chain wrapper for the stream automatically caches the already evaluated stream items and only accesses the underlying stream for not yet visited items. + ## Examples -### Fibonacci Sequence +### Chain Examples -In this example, each next item is the sum of the previous two preceding it in the chain: +#### Fibonacci Sequence -```java + In this example, each next item is the sum of the previous two preceding it in the chain: + + ```java // When String fibonacciFirst8 = Chainable .from(0l, 1l) // Starting values for Fibonacci @@ -103,15 +245,15 @@ In this example, each next item is the sum of the previous two preceding it in t assertEquals( "0, 1, 1, 2, 3, 5, 8, 13", fibonacciFirst8); -``` + ``` -The flavor of the `chain()` method used above feeds the user-specified lambda with the two preceding items. + The flavor of the `chain()` method used above feeds the user-specified lambda with the two preceding items. -### Interleaving +#### Interleaving two chains -In this examples, a chain of odd numbers is interleaved with a chain of even numbers to produce a chain of natural numbers: + In this examples, a chain of odd numbers is interleaved with a chain of even numbers to produce a chain of natural numbers: -```java + ```java final Chainable odds = Chainable .from(1l) // Start with 1 .chain(o -> o + 2); // Generate infinite chain of odd numbers @@ -128,6 +270,43 @@ In this examples, a chain of odd numbers is interleaved with a chain of even num assertEquals( "1, 2, 3, 4, 5, 6, 7, 8, 9, 10" naturals); -``` + ``` + +> :triangular_flag_on_post: To do... + +### Tree Examples + +#### Breadth-first traversal + +> :triangular_flag_on_post: To do... + +#### Finding an item matching some criteria + +> :triangular_flag_on_post: To do... + +#### Infinite tree of permutations of 3 letters + +In this example, an infinite tree is defined with a child extracting lambda that generates strings as permutations of letters from the specified alphabet (*a, b, c*) of increasingly greater length. Then, a "view" of the tree is defined, limiting its depth to 4 layers (including the empty root). Finally, it is transformed into a string listing of all the permutations: + + ```java + char[] alphabet = { 'a', 'b', 'c' }; // Define alphabet to take letters from + ChainableTree permutations = ChainableTree + .withRoot("") // Blank string at the root + .withChildValueExtractor(p -> Chainable + .empty(String.class) // Start with empty chain of strings + .chainIndexed((s, i) -> p + alphabet[i.intValue()]) // Append each alphabet item to the parent + .first(alphabet.length)); // Limit the children chain to the size of the alphabet + + // Prepare a listing of the permutations + String text = permutationsUptoLength3 = permutations + .notBelowWhere(t -> t.value().length() >= 3) // Limit permutation length to 3 letters + .breadthFirst() // Create chain from breadth-first traversal + .afterFirst() // Skip the empty root + .join(", "); + + System.out.println(text); + ``` + +The beginning of the output text here, which lists all the permutations of *a*, *b*, and *c* up to 3 letters in length, will be: `a, b, c, aa, ab, ac, ba, bb, bc, ca, cb, cc, aaa, aab, ...` > :triangular_flag_on_post: To do... \ No newline at end of file diff --git a/pom.xml b/pom.xml index 50814fb..0d8f474 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,10 @@ - 4.0.0 com.github.chainables chainable - 0.5.2 + 0.6.0 jar ${project.groupId}:${project.artifactId} diff --git a/src/main/java/com/github/chainables/chainable/Chainable.java b/src/main/java/com/github/chainables/chainable/Chainable.java index b7b1a8b..7e94f9a 100644 --- a/src/main/java/com/github/chainables/chainable/Chainable.java +++ b/src/main/java/com/github/chainables/chainable/Chainable.java @@ -30,22 +30,10 @@ * iterator pattern, functional programming and lazy evaluation, intended for achieving code that is more succinct, readable, simpler to implement * and sometimes faster than its non-lazy/non-functional equivalent. *

- * {@link Chainable} is somewhat analogous to and inspired by C#'s {@code Enumerable} (LINQ), and conceived of before but ultimately also - * somewhat overlapping with Java 8's {@link java.util.stream.Stream}. + * It is intended to be a rich, `Iterable`-based alternative to Java's `Stream` and Google's *guava*. *

- * One of the key differences from {@link java.util.stream.Stream} is that {@link Chainable} fully preserves the functional and - * re-entrancy semantics of {@link java.lang.Iterable}, i.e. it can be traversed multiple times, with multiple iterator instantiations, - * whereas {@link java.util.stream.Stream} cannot be. - *

- * Also, the {@link Chainable} API surface contains various unique convenience methods, as {@link Chainable} is intended primarily for sequential - * access and not so much the parallelism that has been a key guiding design principle behind Java's {@link Stream}. - *

- * Having said that, a basic level of interoperability between {@link java.util.stream.Stream} and {@link Chainable} is possible: a chain can - * be created from a stream (see {@link Chainable#from(Stream)}) and a stream can be created from a chain (see {@link Chainable#stream()}). - *

- * (A note on the vocabulary: {@link Chainable} is the interface, whereas the word "chain" is used throughout the documentation to refer to a - * specific instance of a {@link Chainable}). - * + * See the project's home site at http://www.github.com/chainables/chainable for more information. + * * @author Martin Sawicki * * @param the type of items in the chain @@ -85,7 +73,7 @@ static Chainable empty(Class clazz) { * C#:{@code Enumerable.AsEnumerable()} * */ - static Chainable from(Iterable items) { + static Chainable from(Iterable items) { return Chain.from(items); } @@ -114,15 +102,16 @@ static Chainable from(T...items) { } /** - * Creates a new chain from the specified {@code stream}, which supports multiple traversals, just like a + * Creates a new chain from the specified {@code stream} that supports multiple traversals, just like a * standard {@link java.lang.Iterable}, even though the underlying {@link java.util.stream.Stream} does not. *

- * Note that upon subsequent traversals of the chain, the original stream is not recomputed, but rather its values as - * obtained during its first traversal are cached internally and used for any subsequent traversals. + * Note that upon subsequent traversals of the chain, none of the items in the original stream are evaluated twice, + * but rather their values are cached internally and used for any subsequent traversals, even if previous traversals of the chain + * were incomplete. * @param stream the stream to create a chain from * @return a chain based on the specified {@code stream} */ - static Chainable from(Stream stream) { + static Chainable from(Stream stream) { if (stream == null) { return Chainable.empty(); } @@ -130,29 +119,40 @@ static Chainable from(Stream stream) { return Chainable.from(new Iterable() { List cache = new ArrayList<>(); - Iterator iter = stream.iterator(); + Iterator streamIter = stream.iterator(); @Override public Iterator iterator() { - if (this.iter == null) { + if (this.streamIter == null) { return this.cache.iterator(); } else { return new Iterator() { + int nextCacheIndex = 0; + @Override public boolean hasNext() { - if (iter.hasNext()) { + if (cache.size() > this.nextCacheIndex) { + return true; + } else if (streamIter.hasNext()) { return true; } else { - iter = null; + streamIter = null; return false; } } @Override public T next() { - T next = iter.next(); - cache.add(next); - return next; + if (cache.size() > this.nextCacheIndex) { + return cache.get(this.nextCacheIndex++); + } else if (streamIter == null) { + return null; + } else { + T next = streamIter.next(); + cache.add(next); + this.nextCacheIndex++; + return next; + } } }; } @@ -160,6 +160,50 @@ public T next() { }); } + /** + * Splits the specified {@code text} into a individual characters. + * @param text the text to split + * @return a chain of characters + * @see #split(String, String) + * @see #split(String, String, boolean) + * @see #join() + */ + static Chainable split(String text) { + return Chainables.split(text); + } + + /** + * Splits the specified {@code text} using the specified {@code delimiterChars}. + *

+ * Each of the characters in the specified {@code delimiterCharacters} is used as a separator individually. + * @param text the text to split + * @param delimiterCharacters the characters to use to split the specified {@code text} + * @return the split strings, including the delimiters + * @see #split(String) + * @see #split(String, String, boolean) + * @see #join() + */ + static Chainable split(String text, String delimiterCharacters) { + return Chainables.split(text, delimiterCharacters); + } + + /** + * Splits the specified {@code text} using the specified {@code delimiterChars}, optionally including the delimiter characters in the + * resulting chain. + *

+ * Each of the characters in the specified {@code delimiterCharacters} is used as a separator individually. + * @param text the text to split + * @param delimiterCharacters the characters to use to split the specified {@code text} + * @param includeDelimiters if {@code true}, the delimiter chars are included in the returned results, otherwise they're not + * @return the split strings + * @see #split(String) + * @see #split(String, String) + * @see #join() + */ + static Chainable split(String text, String delimiterCharacters, boolean includeDelimiters) { + return Chainables.split(text, delimiterCharacters, includeDelimiters); + } + /** * Returns a chain of items after the first one in this chain. * @return items following the first one @@ -197,7 +241,7 @@ default Chainable afterFirst(long number) { * C#:{@code Enumerable.All()} * */ - default boolean allWhere(Predicate condition) { + default boolean allWhere(Predicate condition) { return Chainables.allWhere(this, condition); } @@ -208,7 +252,7 @@ default boolean allWhere(Predicate condition) { * @see #allWhere(Predicate) */ @SuppressWarnings("unchecked") - default boolean allWhereEither(Predicate...conditions) { + default boolean allWhereEither(Predicate...conditions) { return Chainables.allWhereEither(this, conditions); } @@ -234,7 +278,7 @@ default boolean any() { * C#:{@code Enumerable.Any(Func)} * */ - default boolean anyWhere(Predicate condition) { + default boolean anyWhere(Predicate condition) { return Chainables.anyWhere(this, condition); } @@ -245,7 +289,7 @@ default boolean anyWhere(Predicate condition) { * @see #anyWhere(Predicate) */ @SuppressWarnings("unchecked") - default boolean anyWhereEither(Predicate... conditions) { + default boolean anyWhereEither(Predicate... conditions) { return Chainables.anyWhereEither(this, conditions); } @@ -267,7 +311,7 @@ default Chainable apply() { * Java:{@link java.util.stream.Stream#forEach(Consumer)} * */ - default Chainable apply(Consumer action) { + default Chainable apply(Consumer action) { return Chainables.apply(this, action); } @@ -284,7 +328,7 @@ default Chainable apply(Consumer action) { * @see #apply() * @see #apply(Consumer) */ - default Chainable applyAsYouGo(Consumer action) { + default Chainable applyAsYouGo(Consumer action) { return Chainables.applyAsYouGo(this, action); // TODO: shouldn't this call applyAsYouGo? } @@ -316,7 +360,7 @@ default Chainable ascending() { * * @see #descending(ToStringFunction) */ - default Chainable ascending(ToStringFunction keyExtractor) { + default Chainable ascending(ToStringFunction keyExtractor) { return Chainables.ascending(this, keyExtractor); } @@ -334,7 +378,7 @@ default Chainable ascending(ToStringFunction keyExtractor) { * * @see #descending(ToLongFunction) */ - default Chainable ascending(ToLongFunction keyExtractor) { + default Chainable ascending(ToLongFunction keyExtractor) { return Chainables.ascending(this, keyExtractor); } @@ -352,7 +396,7 @@ default Chainable ascending(ToLongFunction keyExtractor) { * * @see #descending(ToDoubleFunction) */ - default Chainable ascending(ToDoubleFunction keyExtractor) { + default Chainable ascending(ToDoubleFunction keyExtractor) { return Chainables.ascending(this, keyExtractor); } @@ -369,7 +413,7 @@ default Chainable ascending(ToDoubleFunction keyExtractor) { * * @see #asLongAsEquals(Object) */ - default Chainable asLongAs(Predicate condition) { + default Chainable asLongAs(Predicate condition) { return (condition == null) ? this : this.before(condition.negate()); } @@ -412,7 +456,7 @@ default Chainable ofType(O example) { * @see #asLongAs(Predicate) * @see #notAfter(Predicate) */ - default Chainable before(Predicate condition) { + default Chainable before(Predicate condition) { return Chainables.before(this, condition); } @@ -440,7 +484,7 @@ default Chainable beforeValue(T item) { * @see #breadthFirstAsLongAs(Function, Predicate) * @see #depthFirst(Function) */ - default Chainable breadthFirst(Function> childExtractor) { + default Chainable breadthFirst(Function> childExtractor) { return Chainables.breadthFirst(this, childExtractor); } @@ -462,7 +506,7 @@ default Chainable breadthFirst(Function> childExtractor) { * @see #breadthFirst(Function) * @see #depthFirst(Function) */ - default Chainable breadthFirstNotBelow(Function> childExtractor, Predicate condition) { + default Chainable breadthFirstNotBelow(Function> childExtractor, Predicate condition) { return Chainables.breadthFirstNotBelow(this, childExtractor, condition); } @@ -479,7 +523,7 @@ default Chainable breadthFirstNotBelow(Function> childExtracto * @see #breadthFirst(Function) * @see #depthFirst(Function) */ - default Chainable breadthFirstAsLongAs(Function> childExtractor, Predicate condition) { + default Chainable breadthFirstAsLongAs(Function> childExtractor, Predicate condition) { return Chainables.breadthFirstAsLongAs(this, childExtractor, condition); } @@ -636,7 +680,7 @@ default Chainable concat(T item) { * @return the resulting chain * @see #concat(Iterable) */ - default Chainable concat(Function> lister) { + default Chainable concat(Function> lister) { return Chainables.concat(this, lister); } @@ -697,7 +741,7 @@ default boolean containsSubarray(Iterable subarray) { * Counts the items in this chain. *

* This triggers a full traversal/evaluation of the items. If the expected number, maximum or minimum is known and the goal is only to - * confirm the expectation, it should be generally more efficient to use {@link #isCountAtLeast(int)}, {@link #isCountAtMost(int)} or {{@link #isCountExactly(long)} + * confirm the expectation, it should be generally more efficient to use {@link #isCountAtLeast(long)}, {@link #isCountAtMost(long)} or {{@link #isCountExactly(long)} * for that purpose, especially if the chain is defined dynamically/functionally and is potentially infinite. * @return total number of items * @chainables.similar @@ -744,7 +788,7 @@ default Chainable depthFirst(Function> childExtractor) { * @param condition * @return resulting chain */ - default Chainable depthFirstNotBelow(Function> childExtractor, Predicate condition) { + default Chainable depthFirstNotBelow(Function> childExtractor, Predicate condition) { return Chainables.depthFirstNotBelow(this, childExtractor, condition); } @@ -776,7 +820,7 @@ default Chainable descending() { * * @see #ascending(ToLongFunction) */ - default Chainable descending(ToLongFunction keyExtractor) { + default Chainable descending(ToLongFunction keyExtractor) { return Chainables.descending(this, keyExtractor); } @@ -794,7 +838,7 @@ default Chainable descending(ToLongFunction keyExtractor) { * * @see #ascending(ToDoubleFunction) */ - default Chainable descending(ToDoubleFunction keyExtractor) { + default Chainable descending(ToDoubleFunction keyExtractor) { return Chainables.descending(this, keyExtractor); } @@ -812,7 +856,7 @@ default Chainable descending(ToDoubleFunction keyExtractor) { * * @see #ascending(ToStringFunction) */ - default Chainable descending(ToStringFunction keyExtractor) { + default Chainable descending(ToStringFunction keyExtractor) { return Chainables.descending(this, keyExtractor); } @@ -840,7 +884,7 @@ default Chainable distinct() { * C#:{@code Enumerable.Distinct()} with a custom comparer * */ - default Chainable distinct(Function keyExtractor) { + default Chainable distinct(Function keyExtractor) { return Chainables.distinct(this, keyExtractor); } @@ -946,7 +990,7 @@ default Chainable first(long count) { * * @see #firstWhereEither(Predicate...) */ - default T firstWhere(Predicate condition) { + default T firstWhere(Predicate condition) { return Chainables.firstWhereEither(this, condition); } @@ -957,10 +1001,19 @@ default T firstWhere(Predicate condition) { * @see #firstWhere(Predicate) */ @SuppressWarnings("unchecked") - default T firstWhereEither(Predicate... conditions) { + default T firstWhereEither(Predicate... conditions) { return Chainables.firstWhereEither(this, conditions); } + /** + * Fetches the item in the chain at the specified {@code index}, traversing/evaluating the chain as needed until that index is reached. + * @param index the index of the item to retrieve from this chain + * @return the item at the specified index + */ + default T get(long index) { + return Chainables.get(this, index); + } + /** * Interleaves the items of the specified {@code iterables}. *

Example: @@ -1101,7 +1154,7 @@ default Chainable last(int count) { * * @see #min(Function) */ - default T max(Function valueExtractor) { + default T max(Function valueExtractor) { return Chainables.max(this, valueExtractor); } @@ -1118,7 +1171,7 @@ default T max(Function valueExtractor) { * * @see #max(Function) */ - default T min(Function valueExtractor) { + default T min(Function valueExtractor) { return Chainables.min(this, valueExtractor); } @@ -1133,7 +1186,7 @@ default T min(Function valueExtractor) { * * @see #noneWhereEither(Predicate...) */ - default boolean noneWhere(Predicate condition) { + default boolean noneWhere(Predicate condition) { return Chainables.noneWhere(this, condition); } @@ -1144,7 +1197,7 @@ default boolean noneWhere(Predicate condition) { * @see #noneWhere(Predicate) */ @SuppressWarnings("unchecked") - default boolean noneWhereEither(Predicate... conditions) { + default boolean noneWhereEither(Predicate... conditions) { return Chainables.noneWhereEither(this, conditions); } @@ -1159,7 +1212,7 @@ default boolean noneWhereEither(Predicate... conditions) { * @see #asLongAs(Predicate) * @see #notAsLongAs(Predicate) */ - default Chainable notAfter(Predicate condition) { + default Chainable notAfter(Predicate condition) { return Chainables.notAfter(this, condition); } @@ -1179,7 +1232,7 @@ default Chainable notAfter(Predicate condition) { * @see #asLongAs(Predicate) * @see #before(Predicate) */ - default Chainable notAsLongAs(Predicate condition) { + default Chainable notAsLongAs(Predicate condition) { return Chainables.notAsLongAs(this, condition); } @@ -1209,7 +1262,7 @@ default Chainable notAsLongAsValue(T item) { * @see #asLongAs(Predicate) * @see #notAsLongAs(Predicate) */ - default Chainable notBefore(Predicate condition) { + default Chainable notBefore(Predicate condition) { return Chainables.notBefore(this, condition); } @@ -1238,7 +1291,7 @@ default Chainable notBeforeEquals(T item) { * * @see #where(Predicate) */ - default Chainable notWhere(Predicate condition) { + default Chainable notWhere(Predicate condition) { return Chainables.notWhere(this, condition); } @@ -1255,7 +1308,7 @@ default Chainable notWhere(Predicate condition) { * * @see #transformAndFlatten(Function) */ - default Chainable replace(Function> replacer) { + default Chainable replace(Function> replacer) { return Chainables.replace(this, replacer); } @@ -1315,7 +1368,7 @@ default Stream stream() { * C#:{@code Enumerable.Aggregate()}, but specifically for summation * */ - default long sum(Function valueExtractor) { + default long sum(Function valueExtractor) { return Chainables.sum(this, valueExtractor); } @@ -1340,7 +1393,7 @@ default List toList() { * C#:{@code Enumerable.ToDictionary()} * */ - default Map toMap(Function keyExtractor) { + default Map toMap(Function keyExtractor) { return Chainables.toMap(this, keyExtractor); } @@ -1364,7 +1417,7 @@ default ChainableQueue toQueue() { * * @see #transformAndFlatten(Function) */ - default Chainable transform(Function transformer) { + default Chainable transform(Function transformer) { return Chainables.transform(this, transformer); } @@ -1379,7 +1432,7 @@ default Chainable transform(Function transformer) { * * @see #transform(Function) */ - default Chainable transformAndFlatten(Function> transformer) { + default Chainable transformAndFlatten(Function> transformer) { return Chainables.transformAndFlatten(this, transformer); } @@ -1393,7 +1446,7 @@ default Chainable transformAndFlatten(Function> transforme * C#:{@code Enumerable.Where()} * */ - default Chainable where(Predicate condition) { + default Chainable where(Predicate condition) { return Chainables.whereEither(this, condition); } @@ -1404,7 +1457,7 @@ default Chainable where(Predicate condition) { * @see #where(Predicate) */ @SuppressWarnings("unchecked") - default Chainable whereEither(Predicate... conditions) { + default Chainable whereEither(Predicate... conditions) { return Chainables.whereEither(this, conditions); } diff --git a/src/main/java/com/github/chainables/chainable/ChainableTree.java b/src/main/java/com/github/chainables/chainable/ChainableTree.java index 7639a05..42b2570 100644 --- a/src/main/java/com/github/chainables/chainable/ChainableTree.java +++ b/src/main/java/com/github/chainables/chainable/ChainableTree.java @@ -364,7 +364,7 @@ default ChainableTree where(Predicate> condition) { /** * Creates a new tree (a single node) with the specified wrapped {@code value}. * @param value the value to wrap in the new tree node - * @return + * @return a new tree wrapping the specified {@code value} */ static ChainableTree withRoot(T value) { return ChainableTreeImpl.withRoot(value); diff --git a/src/main/java/com/github/chainables/chainable/Chainables.java b/src/main/java/com/github/chainables/chainable/Chainables.java index 4077139..6b60389 100644 --- a/src/main/java/com/github/chainables/chainable/Chainables.java +++ b/src/main/java/com/github/chainables/chainable/Chainables.java @@ -50,10 +50,69 @@ private Chainables() { throw new AssertionError("Not instantiable, just stick to the static methods."); } + static class CachedChain extends Chain { + List cache = null; + + @SuppressWarnings("unchecked") + static Chain from(Iterable iterable) { + if (iterable instanceof CachedChain) { + return (CachedChain) iterable; + } else { + return new CachedChain<>(iterable); + } + } + + private CachedChain(Iterable iterable) { + super(iterable); + } + + @Override + public T get(long index) { + return (this.cache != null) ? this.cache.get(Math.toIntExact(index)) : super.get(index); + } + + @Override + public long count() { + return (this.cache != null) ? this.cache.size() : Chainables.count(this); + } + + @Override + public Iterator iterator() { + if (this.cache != null) { + // Cache already filled so return from it + return this.cache.iterator(); + } else { + return new Iterator() { + Iterator iter = iterable.iterator(); + List tempCache = new ArrayList<>(); + + @Override + public boolean hasNext() { + if (iter.hasNext()) { + return true; + } else if (cache == null) { + // The first iterator to fill the cache wins + cache = tempCache; + } + + return false; + } + + @Override + public T next() { + T next = iter.next(); + tempCache.add(next); + return next; + } + }; + } + } + } + static class Chain implements Chainable { - protected Iterable iterable; + protected Iterable iterable; - private Chain(Iterable iterable) { + private Chain(Iterable iterable) { this.iterable = (iterable != null) ? iterable : new ArrayList<>(); } @@ -61,11 +120,14 @@ static Chain empty() { return Chain.from(new ArrayList<>()); } - static Chain from(Iterable iterable) { + @SuppressWarnings("unchecked") + static Chain from(Iterable iterable) { if (iterable instanceof Chain) { return (Chain) iterable; + } else if (iterable instanceof CachedChain) { + return (CachedChain) iterable; } else { - return new Chain<>(iterable); + return new Chain(iterable); } } @@ -102,24 +164,30 @@ public T next() { }); } + @SuppressWarnings("unchecked") @Override public Iterator iterator() { - return this.iterable.iterator(); + return (Iterator) this.iterable.iterator(); } @Override public String toString() { return Chainables.join(", ", this); } + + @Override + public T get(long index) { + return (this.iterable instanceof List) ? ((List) this.iterable).get(Math.toIntExact(index)) : Chainables.get(this, index); + } } private static class ChainableQueueImpl extends Chain implements ChainableQueue { final Deque queue = new LinkedList<>(); - Iterable originalIterable; - final Iterator initialIter; + Iterable originalIterable; + final Iterator initialIter; - private ChainableQueueImpl(Iterable iterable) { + private ChainableQueueImpl(Iterable iterable) { // Specified iterable becomes initial head, but joined with actual FIFO queue super(null); this.originalIterable = iterable; @@ -155,7 +223,7 @@ public ChainableQueueImpl withLast(T...items) { this.queue.addAll(Arrays.asList(items)); } - if (!Chainables.isNullOrEmpty(this.originalIterable)) { + if (!isNullOrEmpty(this.originalIterable)) { this.iterable = Chainables.concat(this.originalIterable, this.queue); } @@ -174,7 +242,7 @@ public ChainableQueue withLast(Iterable items) { * @return * @see Chainable#afterFirst() */ - public static Chainable afterFirst(Iterable items) { + public static Chainable afterFirst(Iterable items) { return afterFirst(items, 1); } @@ -183,9 +251,9 @@ public static Chainable afterFirst(Iterable items) { * @param number * @return */ - public static Chainable afterFirst(Iterable items, long number) { + public static Chainable afterFirst(Iterable items, long number) { return (items == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - final Iterator iter = items.iterator(); + final Iterator iter = items.iterator(); long skippedNum = 0; @Override @@ -218,12 +286,12 @@ public V next() { * @see Chainable#allWhereEither(Predicate...) */ @SafeVarargs - public static boolean allWhereEither(Iterable items, Predicate... conditions) { + public static boolean allWhereEither(Iterable items, Predicate... conditions) { if (items == null) { return false; } else { - Chainable> conds = Chainable.from(conditions); - return Chainables.noneWhere(items, i -> !conds.anyWhere(c -> c.test(i))); + Chainable> conds = Chainable.from(conditions); + return noneWhere(items, i -> !conds.anyWhere(c -> c.test(i))); } } @@ -233,7 +301,7 @@ public static boolean allWhereEither(Iterable items, Predicate... cond * @return * @see Chainable#allWhere(Predicate) */ - public static boolean allWhere(Iterable items, Predicate condition) { + public static boolean allWhere(Iterable items, Predicate condition) { return allWhereEither(items, condition); } @@ -244,7 +312,7 @@ public static boolean allWhere(Iterable items, Predicate condition) { * @return {@code true} if the specified {@code iterable} has at least one item * @see Chainable#any() */ - public static boolean any(Iterable iterable) { + public static boolean any(Iterable iterable) { return !isNullOrEmpty(iterable); } @@ -254,8 +322,8 @@ public static boolean any(Iterable iterable) { * @return * @see Chainable#anyWhere(Predicate) */ - public static boolean anyWhere(Iterable items, Predicate condition) { - return Chainables.anyWhereEither(items, condition); + public static boolean anyWhere(Iterable items, Predicate condition) { + return anyWhereEither(items, condition); } /** @@ -265,11 +333,11 @@ public static boolean anyWhere(Iterable items, Predicate condition) { * @see Chainable#anyWhereEither(Predicate...) */ @SafeVarargs - public static boolean anyWhereEither(Iterable items, Predicate...conditions) { + public static boolean anyWhereEither(Iterable items, Predicate...conditions) { if (conditions == null) { return true; } else { - for (Predicate condition : conditions) { + for (Predicate condition : conditions) { if (Chainables.firstWhereEither(items, condition) != null) { return true; } @@ -285,7 +353,7 @@ public static boolean anyWhereEither(Iterable items, Predicate...condi * @return * @see Chainable#apply(Consumer) */ - public static Chainable apply(Iterable items, Consumer action) { + public static Chainable apply(Iterable items, Consumer action) { if (items == null) { return null; } else if (action == null) { @@ -293,7 +361,7 @@ public static Chainable apply(Iterable items, Consumer action) { } // Apply to all - List itemsList = Chainables.toList(items); + List itemsList = Chainables.toList(items); for (T item : itemsList) { try { action.accept(item); @@ -311,7 +379,7 @@ public static Chainable apply(Iterable items, Consumer action) { * @return * @see Chainable#apply() */ - public static Chainable apply(Iterable items) { + public static Chainable apply(Iterable items) { return apply(items, o -> {}); // NOP } @@ -321,14 +389,14 @@ public static Chainable apply(Iterable items) { * @return * @see Chainable#applyAsYouGo(Consumer) */ - public static Chainable applyAsYouGo(Iterable items, Consumer action) { + public static Chainable applyAsYouGo(Iterable items, Consumer action) { if (items == null) { return null; } else if (action == null) { return Chainable.from(items); } else { return Chainable.fromIterator(() -> new Iterator() { - final private Iterator itemIter = items.iterator(); + final private Iterator itemIter = items.iterator(); @Override public boolean hasNext() { @@ -350,7 +418,7 @@ public T next() { * @return sorted items * @see Chainable#ascending() */ - public static Chainable ascending(Iterable items) { + public static Chainable ascending(Iterable items) { return sorted(items, true); } @@ -360,7 +428,7 @@ public static Chainable ascending(Iterable items) { * @return * @see Chainable#ascending(ToStringFunction) */ - public static Chainable ascending(Iterable items, ToStringFunction keyExtractor) { + public static Chainable ascending(Iterable items, ToStringFunction keyExtractor) { return sortedBy(items, keyExtractor, true); } @@ -370,7 +438,7 @@ public static Chainable ascending(Iterable items, ToStringFunction * @return * @see Chainable#ascending(ToLongFunction) */ - public static Chainable ascending(Iterable items, ToLongFunction keyExtractor) { + public static Chainable ascending(Iterable items, ToLongFunction keyExtractor) { return sortedBy(items, keyExtractor, true); } @@ -380,7 +448,7 @@ public static Chainable ascending(Iterable items, ToLongFunction ke * @return * @see Chainable#ascending(ToDoubleFunction) */ - public static Chainable ascending(Iterable items, ToDoubleFunction keyExtractor) { + public static Chainable ascending(Iterable items, ToDoubleFunction keyExtractor) { return sortedBy(items, keyExtractor, true); } @@ -390,7 +458,7 @@ public static Chainable ascending(Iterable items, ToDoubleFunction * @param condition the condition for the returned items to satisfy * @return items before the first one is encountered taht no longer satisfies the specified condition */ - public static Chainable asLongAs(Iterable items, Predicate condition) { + public static Chainable asLongAs(Iterable items, Predicate condition) { return (condition == null) ? Chainable.from(items) : before(items, condition.negate()); } @@ -400,26 +468,10 @@ public static Chainable asLongAs(Iterable items, Predicate conditio * @param item the item that returned items must be equal to * @return items before the first one is encountered that no longer equals the specified item */ - public static Chainable asLongAsEquals(Iterable items, T item) { + public static Chainable asLongAsEquals(Iterable items, T item) { return asLongAs(items, o -> o == item); } - /** - * @param items - * @param example - * @return - * @see Chainable#ofType(Object) - */ - @SuppressWarnings("unchecked") - public static Chainable ofType(Iterable items, O example) { - Class clazz = example.getClass(); - return (Chainable) Chainable - .from(items) - .withoutNull() - .transform(i -> (clazz.isAssignableFrom(i.getClass())) ? clazz.cast(i) : null) - .withoutNull(); - } - /** * Returns items before the first item satisfying the specified condition is encountered. * @param items items to return from @@ -427,7 +479,7 @@ public static Chainable ofType(Iterable items, O example) { * @return items before the specified condition is satisfied * @see Chainable#before(Predicate) */ - public static Chainable before(Iterable items, Predicate condition) { + public static Chainable before(Iterable items, Predicate condition) { if (items == null) { return null; } else if (condition == null) { @@ -435,7 +487,7 @@ public static Chainable before(Iterable items, Predicate condition) } return Chainable.fromIterator(() -> new Iterator() { - private final Iterator iterator = items.iterator(); + private final Iterator iterator = items.iterator(); private T nextItem = null; boolean stopped = false; @@ -478,7 +530,7 @@ public T next() { * @return items before the specified item is encountered * @see Chainable#beforeValue(Object) */ - public static Chainable beforeValue(Iterable items, T item) { + public static Chainable beforeValue(Iterable items, T item) { return before(items, o -> o==item); } @@ -488,7 +540,7 @@ public static Chainable beforeValue(Iterable items, T item) { * @return * @see Chainable#breadthFirst(Function) */ - public static Chainable breadthFirst(Iterable items, Function> childTraverser) { + public static Chainable breadthFirst(Iterable items, Function> childTraverser) { return traverse(items, childTraverser, true); } @@ -500,9 +552,9 @@ public static Chainable breadthFirst(Iterable items, Function Chainable breadthFirstNotBelow( - Iterable items, - Function> childTraverser, - Predicate condition) { + Iterable items, + Function> childTraverser, + Predicate condition) { return notBelow(items, childTraverser, condition, true); } @@ -514,11 +566,11 @@ public static Chainable breadthFirstNotBelow( * @see Chainable#breadthFirstAsLongAs(Function, Predicate) */ public static Chainable breadthFirstAsLongAs( - Iterable items, - Function> childTraverser, - Predicate condition) { - final Predicate appliedCondition = (condition != null) ? condition : (o -> true); - return breadthFirst(items, o -> Chainables.whereEither(childTraverser.apply(o), c -> Boolean.TRUE.equals(appliedCondition.test(c)))); + Iterable items, + Function> childTraverser, + Predicate condition) { + final Predicate appliedCondition = (condition != null) ? condition : (o -> true); + return breadthFirst(items, o -> whereEither(childTraverser.apply(o), c -> Boolean.TRUE.equals(appliedCondition.test(c)))); } /** @@ -526,44 +578,8 @@ public static Chainable breadthFirstAsLongAs( * @return * @see Chainable#cached() */ - public static Chainable cached(Iterable items) { - return (items == null) ? Chainable.empty() : Chainable.from(new Iterable() { - List cache = null; - - @Override - public Iterator iterator() { - if (cache != null) { - // Cache already filled so return from it - return this.cache.iterator(); - } else { - return new Iterator() { - Iterator iter = items.iterator(); - List tempCache = new ArrayList<>(); - - @Override - public boolean hasNext() { - if (iter.hasNext()) { - return true; - } else { - if (cache == null) { - // The first iterator to fill the cache wins - cache = tempCache; - } - - return false; - } - } - - @Override - public T next() { - T next = iter.next(); - tempCache.add(next); - return next; - } - }; - } - } - }); + public static Chainable cached(Iterable items) { + return (items == null) ? Chainable.empty() : CachedChain.from(items); } /** @@ -572,7 +588,7 @@ public T next() { * @return * @see Chainable#cast(Class) */ - public static Chainable cast(Iterable items, Class clazz) { + public static Chainable cast(Iterable items, Class clazz) { return (items == null || clazz == null) ? Chainable.from() : transform(items, o -> clazz.cast(o)); } @@ -582,7 +598,7 @@ public static Chainable cast(Iterable items, Class clazz) { * @return * @see Chainable#chain(UnaryOperator) */ - public static Chainable chain(T item, UnaryOperator nextItemExtractor) { + public static Chainable chain(T item, UnaryOperator nextItemExtractor) { return chain(Chainable.from(item), nextItemExtractor); } @@ -592,7 +608,7 @@ public static Chainable chain(T item, UnaryOperator nextItemExtractor) * @return * @see Chainable#chainIndexed(BiFunction) */ - public static Chainable chain(T item, BiFunction nextItemExtractor) { + public static Chainable chain(T item, BiFunction nextItemExtractor) { return chainIndexed(Chainable.from(item), nextItemExtractor); } @@ -601,9 +617,9 @@ public static Chainable chain(T item, BiFunction nextItemExtr * @param nextItemExtractorFromLastTwo * @return */ - public static Chainable chain(Iterable items, BinaryOperator nextItemExtractorFromLastTwo) { + public static Chainable chain(Iterable items, BinaryOperator nextItemExtractorFromLastTwo) { return (items == null || nextItemExtractorFromLastTwo == null) ? Chainable.from(items) : Chainable.fromIterator(() -> new Iterator() { - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); T next = null; T prev = null; boolean isFetched = false; // If iter is empty, pretend it starts with null @@ -615,7 +631,7 @@ public boolean hasNext() { return false; } else if (isFetched) { return true; - } else if (Chainables.isNullOrEmpty(this.iter)) { + } else if (isNullOrEmpty(this.iter)) { // Seed iterator already finished so start the chaining this.iter = null; T temp = this.next; @@ -652,9 +668,9 @@ public T next() { * @return * @see Chainable#chainIndexed(BiFunction) */ - public static Chainable chainIndexed(Iterable items, BiFunction nextItemExtractor) { + public static Chainable chainIndexed(Iterable items, BiFunction nextItemExtractor) { return (items == null || nextItemExtractor == null) ? Chainable.from(items) : Chainable.fromIterator(() -> new Iterator() { - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); T next = null; boolean isFetched = false; // If iter is empty, pretend it starts with null boolean isStopped = false; @@ -666,7 +682,7 @@ public boolean hasNext() { return false; } else if (isFetched) { return true; - } else if (Chainables.isNullOrEmpty(this.iter)) { + } else if (isNullOrEmpty(this.iter)) { // Seed iterator already finished so start the chaining this.iter = null; this.next = nextItemExtractor.apply(this.next, index); @@ -701,9 +717,9 @@ public T next() { * @return * @see Chainable#chain(UnaryOperator) */ - public static Chainable chain(Iterable items, UnaryOperator nextItemExtractor) { + public static Chainable chain(Iterable items, UnaryOperator nextItemExtractor) { return (items == null || nextItemExtractor == null) ? Chainable.from(items) : Chainable.fromIterator(() -> new Iterator() { - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); T next = null; boolean isFetched = false; // If iter is empty, pretend it starts with null boolean isStopped = false; @@ -714,7 +730,7 @@ public boolean hasNext() { return false; } else if (isFetched) { return true; - } else if (Chainables.isNullOrEmpty(this.iter)) { + } else if (isNullOrEmpty(this.iter)) { // Seed iterator already finished so start the chaining this.iter = null; this.next = nextItemExtractor.apply(this.next); @@ -748,9 +764,9 @@ public T next() { * @param nextItemExtractor * @return {@link Chainable#chainIf(Predicate, UnaryOperator)} */ - public static Chainable chainIf(Iterable items, Predicate condition, UnaryOperator nextItemExtractor) { + public static Chainable chainIf(Iterable items, Predicate condition, UnaryOperator nextItemExtractor) { return (items == null || nextItemExtractor == null) ? Chainable.from(items) : Chainable.fromIterator(() -> new Iterator() { - final Iterator iter = items.iterator(); + final Iterator iter = items.iterator(); private T next = null; private boolean nextReady = false; @@ -796,8 +812,8 @@ public T next() { * @return * @see Chainable#collectInto(Collection) */ - public static Chainable collectInto(Iterable items, Collection targetCollection) { - return (items == null || targetCollection == null) ? Chainable.from(items) : Chainables.applyAsYouGo(items, o -> targetCollection.add(o)); + public static Chainable collectInto(Iterable items, Collection targetCollection) { + return (items == null || targetCollection == null) ? Chainable.from(items) : applyAsYouGo(items, o -> targetCollection.add(o)); } /** @@ -806,9 +822,9 @@ public static Chainable collectInto(Iterable items, Collection targ * @return * @see Chainable#concat(Function) */ - public static Chainable concat(Iterable items, Function> lister) { + public static Chainable concat(Iterable items, Function> lister) { return (lister == null || items == null) ? Chainable.from(items) : Chainable.fromIterator(() -> new Iterator() { - private final Iterator iter1 = items.iterator(); + private final Iterator iter1 = items.iterator(); private Iterator iter2 = null; @Override @@ -840,7 +856,7 @@ public T next() { * @return concatenated iterable */ // TODO Should this be removed now that concat(...) exists? - public static Chainable concat(Iterable items1, Iterable items2) { + public static Chainable concat(Iterable items1, Iterable items2) { if (items1 == null && items2 == null) { return null; } else if (Chainables.isNullOrEmpty(items1)) { @@ -849,8 +865,8 @@ public static Chainable concat(Iterable items1, Iterable items2) { return Chainable.from(items1); } else { return Chainable.fromIterator(() -> new Iterator() { - private final Iterator iter1 = items1.iterator(); - private final Iterator iter2 = items2.iterator(); + private final Iterator iter1 = items1.iterator(); + private final Iterator iter2 = items2.iterator(); @Override public boolean hasNext() { @@ -880,7 +896,7 @@ public T next() { * the item to concatenate * @return the resulting concatenation */ - public static Chainable concat(Iterable items, T item) { + public static Chainable concat(Iterable items, T item) { return concat(items, (Iterable) Arrays.asList(item)); } @@ -890,10 +906,10 @@ public static Chainable concat(Iterable items, T item) { * @see Chainable#concat(Iterable...) */ @SafeVarargs - public static Chainable concat(Iterable...itemSequences) { - return (Chainables.isNullOrEmpty(itemSequences)) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { + public static Chainable concat(Iterable...itemSequences) { + return (isNullOrEmpty(itemSequences)) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { private int i = 0; - private Iterator curIter = null; + private Iterator curIter = null; @Override public boolean hasNext() { @@ -919,7 +935,7 @@ public T next() { * @return * @see Chainable#concat(Iterable) */ - public static Chainable concat(T item, Iterable items) { + public static Chainable concat(T item, Iterable items) { return concat((Iterable) Arrays.asList(item), items); } @@ -928,11 +944,11 @@ public static Chainable concat(T item, Iterable items) { * @param item * @return true if the specified {@code item} is among the members of the specified {@code container}, else false */ - public static boolean contains(Iterable container, T item) { + public static boolean contains(Iterable container, T item) { if (container == null) { return false; } else if (!(container instanceof Set)) { - return !Chainables.isNullOrEmpty(Chainables.whereEither(container, i -> i.equals(item))); + return !isNullOrEmpty(whereEither(container, i -> i.equals(item))); } else if (item == null) { return false; } else { @@ -957,8 +973,9 @@ public static boolean containsAll(T[] container, T...items) { * @return * @see Chainable#containsAll(Object...) */ + @SuppressWarnings("unchecked") @SafeVarargs - public static boolean containsAll(Iterable container, T...items) { + public static boolean containsAll(Iterable container, T...items) { Set searchSet = new HashSet<>(Arrays.asList(items)); if (container == null) { return false; @@ -986,7 +1003,7 @@ public static boolean containsAll(Iterable container, T...items) { * @see Chainable#containsAny(Object...) */ @SafeVarargs - public static boolean containsAny(Iterable container, T...items) { + public static boolean containsAny(Iterable container, T...items) { if (container == null) { return false; } else if (items == null) { @@ -1019,16 +1036,16 @@ public static boolean containsAny(T[] container, T...items) { * @return * @see Chainable#containsSubarray(Iterable) */ - public static boolean containsSubarray(Iterable items, Iterable subarray) { + public static boolean containsSubarray(Iterable items, Iterable subarray) { if (items == null) { return false; - } else if (Chainables.isNullOrEmpty(subarray)) { + } else if (isNullOrEmpty(subarray)) { return true; } // Brute force evaluation of everything (TODO: make it lazy and faster?) - List subList = Chainables.toList(subarray); - List itemsCached = Chainables.toList(items); + List subList = toList(subarray); + List itemsCached = toList(items); for (int i = 0; i < itemsCached.size() - subList.size(); i++) { boolean matched = true; @@ -1054,14 +1071,14 @@ public static boolean containsSubarray(Iterable items, Iterable subarr * @return the number of items * @see Chainable#count() */ - public static long count(Iterable items) { + public static long count(Iterable items) { if (items == null) { return 0; } else if (items instanceof Collection) { return ((Collection)items).size(); } - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); long i = 0; for (i = 0; iter.hasNext(); i++) { iter.next(); @@ -1076,7 +1093,7 @@ public static long count(Iterable items) { * @return * @see Chainable#depthFirst(Function) */ - public static Chainable depthFirst(Iterable items, Function> childTraverser) { + public static Chainable depthFirst(Iterable items, Function> childTraverser) { return traverse(items, childTraverser, false); } @@ -1088,9 +1105,9 @@ public static Chainable depthFirst(Iterable items, Function Chainable depthFirstNotBelow( - Iterable items, - Function> childTraverser, - Predicate condition) { + Iterable items, + Function> childTraverser, + Predicate condition) { return notBelow(items, childTraverser, condition, false); } @@ -1099,7 +1116,7 @@ public static Chainable depthFirstNotBelow( * @return sorted items * @see Chainable#descending() */ - public static Chainable descending(Iterable items) { + public static Chainable descending(Iterable items) { return sorted(items, false); } @@ -1109,7 +1126,7 @@ public static Chainable descending(Iterable items) { * @return * @see Chainable#descending(ToStringFunction) */ - public static Chainable descending(Iterable items, ToStringFunction keyExtractor) { + public static Chainable descending(Iterable items, ToStringFunction keyExtractor) { return sortedBy(items, keyExtractor, false); } @@ -1183,7 +1200,7 @@ public File next() { * @return * @see Chainable#descending(ToLongFunction) */ - public static Chainable descending(Iterable items, ToLongFunction keyExtractor) { + public static Chainable descending(Iterable items, ToLongFunction keyExtractor) { return sortedBy(items, keyExtractor, false); } @@ -1193,7 +1210,7 @@ public static Chainable descending(Iterable items, ToLongFunction k * @return * @see Chainable#descending(ToDoubleFunction) */ - public static Chainable descending(Iterable items, ToDoubleFunction comparable) { + public static Chainable descending(Iterable items, ToDoubleFunction comparable) { return sortedBy(items, comparable, false); } @@ -1203,10 +1220,11 @@ public static Chainable descending(Iterable items, ToDoubleFunction * @return * @see Chainable#distinct(Function) */ - public static Chainable distinct(Iterable items, Function keyExtractor) { - return (keyExtractor == null) ? distinct(items) : Chainable.fromIterator(() -> new Iterator() { + @SuppressWarnings("unchecked") + public static Chainable distinct(Iterable items, Function keyExtractor) { + return (keyExtractor == null) ? (Chainable) distinct(items) : Chainable.fromIterator(() -> new Iterator() { final Map seen = new HashMap<>(); - final Iterator iter = items.iterator(); + final Iterator iter = items.iterator(); T next = null; V value = null; boolean hasNext = false; @@ -1247,10 +1265,10 @@ public T next() { * @return * @see Chainable#distinct() */ - public static Chainable distinct(Iterable items) { + public static Chainable distinct(Iterable items) { return Chainable.fromIterator(() -> new Iterator() { final Set seen = new HashSet<>(); - final Iterator iter = items.iterator(); + final Iterator iter = items.iterator(); T next = null; @Override @@ -1294,8 +1312,8 @@ public T next() { * @return * @see Chainable#endsWith(Iterable) */ - public static boolean endsWith(Iterable items, Iterable suffix) { - return Chainables.endsWithEither(items, suffix); + public static boolean endsWith(Iterable items, Iterable suffix) { + return endsWithEither(items, suffix); } /** @@ -1305,23 +1323,23 @@ public static boolean endsWith(Iterable items, Iterable suffix) { * @see Chainable#endsWithEither(Iterable...) */ @SafeVarargs - public static boolean endsWithEither(Iterable items, Iterable...suffixes) { - if (Chainables.isNullOrEmpty(items)) { + public static boolean endsWithEither(Iterable items, Iterable...suffixes) { + if (isNullOrEmpty(items)) { return false; } else if (suffixes == null) { return false; } - List itemList = Chainables.toList(items); - for (Iterable suffix : suffixes) { + List itemList = toList(items); + for (Iterable suffix : suffixes) { // Check each suffix - List suffixSequence = Chainables.toList(suffix); + List suffixSequence = Chainables.toList(suffix); if (suffixSequence.size() > itemList.size()) { // If different size, assume non-match and check the next suffix continue; } - Iterator suffixIter = suffixSequence.iterator(); + Iterator suffixIter = suffixSequence.iterator(); int i = 0; boolean matching = true; for (i = itemList.size() - suffixSequence.size(); i < itemList.size(); i++) { @@ -1359,14 +1377,14 @@ public static boolean endsWithEither(Iterable items, Iterable...suffix * @return * @see Chainable#equals(Iterable) */ - public static boolean equal(Iterable items1, Iterable items2) { + public static boolean equal(Iterable items1, Iterable items2) { if (items1 == items2) { return true; } else if (items1 == null || items2 == null) { return false; } else { - Iterator iterator1 = items1.iterator(); - Iterator iterator2 = items2.iterator(); + Iterator iterator1 = items1.iterator(); + Iterator iterator2 = items2.iterator(); while (iterator1.hasNext() && iterator2.hasNext()) { if (!iterator1.next().equals(iterator2.next())) { return false; @@ -1388,11 +1406,11 @@ public static boolean equal(Iterable items1, Iterable items2) { * @return the first item * @see Chainable#first() */ - public static T first(Iterable items) { + public static T first(Iterable items) { if (items == null) { return null; } else { - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); if (!iter.hasNext()) { return null; } else { @@ -1407,9 +1425,9 @@ public static T first(Iterable items) { * @return the first number of items * @see Chainable#first(long) */ - public static Chainable first(Iterable items, long number) { + public static Chainable first(Iterable items, long number) { return (items == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); long returnedCount = 0; @Override @@ -1433,14 +1451,14 @@ public T next() { * @see Chainable#firstWhereEither(Predicate...) */ @SafeVarargs - public static V firstWhereEither(Iterable items, Predicate... conditions) { + public static V firstWhereEither(Iterable items, Predicate... conditions) { if (items == null) { return null; } else if (conditions == null) { - return Chainables.first(items); + return first(items); } else { for (V item : items) { - for (Predicate condition : conditions) { + for (Predicate condition : conditions) { if (condition.test(item)) { return item; } @@ -1460,7 +1478,7 @@ public static V firstWhereEither(Iterable items, Predicate... conditio @SafeVarargs public static Chainable interleave(Iterable items1, Iterable...items2) { return (items1 == null || items2 == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - Deque> iters = new LinkedList>(Chainable + Deque> iters = new LinkedList<>(Chainable .from(items1.iterator()) .concat(Chainable.from(items2).transform(i -> i.iterator())) .toList()); @@ -1476,7 +1494,7 @@ public boolean hasNext() { @Override public T next() { - Iterator iter = this.iters.removeFirst(); + Iterator iter = this.iters.removeFirst(); if (iter != null) { this.iters.addLast(iter); return iter.next(); @@ -1487,20 +1505,30 @@ public T next() { }); } + /** + * @param items + * @param index + * @return + * @see Chainable#get(long) + */ + public static T get(Iterable items, long index) { + return afterFirst(items, index).first(); + } + /** * @param items * @param min * @return true if there are at least the specified {@code min} number of {@code items}, stopping the traversal as soon as that can be determined * @see Chainable#isCountAtLeast(long) */ - public static boolean isCountAtLeast(Iterable items, long min) { + public static boolean isCountAtLeast(Iterable items, long min) { if (min <= 0) { return true; } else if (items == null) { return false; } - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); while (min > 0 && iter.hasNext()) { iter.next(); min--; @@ -1515,14 +1543,14 @@ public static boolean isCountAtLeast(Iterable items, long min) { * @return true if there are at most the specified {@code max} number of {@code items}, stopping the traversal as soon as that can be determined * @see Chainable#isCountAtMost(long) */ - public static boolean isCountAtMost(Iterable items, long max) { + public static boolean isCountAtMost(Iterable items, long max) { if (items == null && max >= 0) { return true; } else if (items == null) { return false; } - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); while (max > 0 && iter.hasNext()) { iter.next(); max--; @@ -1537,14 +1565,14 @@ public static boolean isCountAtMost(Iterable items, long max) { * @return * @see Chainable#isCountExactly(long) */ - public static boolean isCountExactly(Iterable items, long count) { + public static boolean isCountExactly(Iterable items, long count) { if (items == null) { return count == 0; } else if (items instanceof Collection) { return ((Collection)items).size() == count; } - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); long i = 0; while (iter.hasNext()) { iter.next(); @@ -1580,7 +1608,7 @@ public static boolean isNullOrEmpty(Iterable iterable) { */ public static boolean isNullOrEmptyEither(Iterable...iterables) { for (Iterable iterable : iterables) { - if (Chainables.isNullOrEmpty(iterable)) { + if (isNullOrEmpty(iterable)) { return true; } } @@ -1595,7 +1623,7 @@ public static boolean isNullOrEmptyEither(Iterable...iterables) { * @see Chainable#iterativeContains(Object) */ @Experimental - public static Chainable iterativeContains(Iterable container, T item) { + public static Chainable iterativeContains(Iterable container, T item) { if (container == null) { return Chainable.from(false); } else if (container instanceof Set && item != null) { @@ -1622,7 +1650,7 @@ public static boolean isNullOrEmpty(Map map) { * @param iterator * @return {@code true} if the specified {@code iterator} is null or empty */ - public static boolean isNullOrEmpty(Iterator iterator) { + public static boolean isNullOrEmpty(Iterator iterator) { return (iterator != null) ? !iterator.hasNext() : true; } @@ -1633,7 +1661,7 @@ public static boolean isNullOrEmpty(Iterator iterator) { * @param iterator the iterator to traverse * @return the joined string */ - public static String join(String delimiter, Iterator iterator) { + public static String join(String delimiter, Iterator iterator) { if (iterator == null) { return null; } @@ -1671,17 +1699,17 @@ public static String join(String delimiter, Stream stream) { * @return * @see Chainable#last() */ - public static T last(Iterable items) { + public static T last(Iterable items) { T last = null; - if (Chainables.isNullOrEmpty(items)) { + if (isNullOrEmpty(items)) { // Skip } else if (items instanceof List) { // If list, then faster lookup - List list = (List)items; + List list = (List)items; last = list.get(list.size() - 1); } else { // Else, slow lookup - Iterator iter = items.iterator(); + Iterator iter = items.iterator(); while (iter.hasNext()) { last = iter.next(); } @@ -1696,9 +1724,9 @@ public static T last(Iterable items) { * @return * @see Chainable#last(int) */ - public static Chainable last(Iterable items, long count) { + public static Chainable last(Iterable items, long count) { return (items == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - final List list = Chainables.toList(items); + final List list = Chainables.toList(items); final int size = this.list.size(); long next = this.size - count; @@ -1721,7 +1749,7 @@ public T next() { * @param items the items to join * @return the joined string */ - public static String join(String delimiter, Iterable items) { + public static String join(String delimiter, Iterable items) { return join(delimiter, items.iterator()); } @@ -1731,10 +1759,10 @@ public static String join(String delimiter, Iterable items) { * @return * @see Chainable#max(Function) */ - public static T max(Iterable items, Function valueExtractor) { + public static T max(Iterable items, Function valueExtractor) { Double max = null; T maxItem = null; - if (!Chainables.isNullOrEmpty(items)) { + if (!isNullOrEmpty(items)) { for (T item : items) { Double number = valueExtractor.apply(item); if (max == null || number > max) { @@ -1753,7 +1781,7 @@ public static T max(Iterable items, Function valueExtractor) { * @return * @see Chainable#min(Function) */ - public static T min(Iterable items, Function valueExtractor) { + public static T min(Iterable items, Function valueExtractor) { Double min = null; T minItem = null; if (!Chainables.isNullOrEmpty(items)) { @@ -1775,7 +1803,7 @@ public static T min(Iterable items, Function valueExtractor) { * @return * @see Chainable#noneWhere(Predicate) */ - public static boolean noneWhere(Iterable items, Predicate condition) { + public static boolean noneWhere(Iterable items, Predicate condition) { return Chainables.noneWhereEither(items, condition); } @@ -1786,7 +1814,7 @@ public static boolean noneWhere(Iterable items, Predicate condition) { * @see Chainable#noneWhereEither(Predicate...) */ @SafeVarargs - public static boolean noneWhereEither(Iterable items, Predicate... conditions) { + public static boolean noneWhereEither(Iterable items, Predicate... conditions) { return !Chainables.anyWhereEither(items, conditions); } @@ -1797,7 +1825,7 @@ public static boolean noneWhereEither(Iterable items, Predicate... con * @return items before and including the first item where the specified condition is satisfied * @see Chainable#notAfter(Predicate) */ - public static Chainable notAfter(Iterable items, Predicate condition) { + public static Chainable notAfter(Iterable items, Predicate condition) { if (items == null) { return null; } else if (condition == null) { @@ -1805,7 +1833,7 @@ public static Chainable notAfter(Iterable items, Predicate conditio } return Chainable.fromIterator(() -> new Iterator() { - private final Iterator iterator = items.iterator(); + private final Iterator iterator = items.iterator(); private T nextItem = null; boolean stopped = false; @@ -1849,8 +1877,8 @@ public T next() { * @return * @see Chainable#notAsLongAs(Predicate) */ - public static Chainable notAsLongAs(Iterable items, Predicate condition) { - return (items != null) ? Chainable.from(items).notBefore(condition.negate()) : null; + public static Chainable notAsLongAs(Iterable items, Predicate condition) { + return (items != null) ? notBefore(items, condition.negate()) : null; } /** @@ -1859,7 +1887,7 @@ public static Chainable notAsLongAs(Iterable items, Predicate condi * @return * @see Chainable#notAsLongAsValue(Object) */ - public static Chainable notAsLongAsValue(Iterable items, T value) { + public static Chainable notAsLongAsValue(Iterable items, T value) { return notBefore(items, o -> o!=value); } @@ -1869,15 +1897,14 @@ public static Chainable notAsLongAsValue(Iterable items, T value) { * @return * @see Chainable#notBefore(Predicate) */ - //## - static Chainable notBefore(Iterable items, Predicate condition) { + static Chainable notBefore(Iterable items, Predicate condition) { if (items == null) { return null; } else if (condition == null) { return Chainable.from(items); } else { return Chainable.fromIterator(() -> new Iterator() { - final Iterator iterator = items.iterator(); + final Iterator iterator = items.iterator(); T nextItem = null; boolean start = false; @@ -1927,16 +1954,16 @@ public T next() { * @return the rest of the items * @see Chainable#notBeforeEquals(Object) */ - public static Chainable notBeforeEquals(Iterable items, T item) { + public static Chainable notBeforeEquals(Iterable items, T item) { return notBefore(items, (Predicate)(o -> o == item)); } private static Chainable notBelow( - Iterable items, - Function> childTraverser, - Predicate condition, + Iterable items, + Function> childTraverser, + Predicate condition, boolean breadthFirst) { - final Predicate appliedCondition = (condition != null) ? condition : (o -> false); + final Predicate appliedCondition = (condition != null) ? condition : (o -> false); return traverse(items, o -> Boolean.FALSE.equals(appliedCondition.test(o)) ? childTraverser.apply(o) : Chainable.empty(), breadthFirst); } @@ -1946,12 +1973,58 @@ private static Chainable notBelow( * @return * @see Chainable#notWhere(Predicate) */ - public static final Chainable notWhere(Iterable items, Predicate condition) { + public static final Chainable notWhere(Iterable items, Predicate condition) { return (condition != null) ? Chainables.whereEither(items, condition.negate()) : Chainable.from(items); } + /** + * @param items + * @param example + * @return + * @see Chainable#ofType(Object) + */ + @SuppressWarnings("unchecked") + public static Chainable ofType(Iterable items, O example) { + Class clazz = example.getClass(); + return (Chainable) withoutNull(items) + .transform(i -> (clazz.isAssignableFrom(i.getClass())) ? clazz.cast(i) : null) + .withoutNull(); + } + + /** + * @param items + * @param replacer + * @return + * @see Chainable#replace(Function) + */ + public static Chainable replace(Iterable items, Function> replacer) { + return transformAndFlatten(items, replacer).withoutNull(); + } + + /** + * @param items + * @return + * @see Chainable#reverse() + */ + public static Chainable reverse(Iterable items) { + return (items == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { + List list = Chainables.toList(items); + int nextIndex = list.size() - 1; + + @Override + public boolean hasNext() { + return (nextIndex >= 0); + } + + @Override + public T next() { + return (this.hasNext()) ? list.get(this.nextIndex--) : null; + } + }); + } + @SuppressWarnings("unchecked") - private static Chainable sorted(Iterable items, boolean ascending) { + private static Chainable sorted(Iterable items, boolean ascending) { T item; if (isNullOrEmpty(items)) { @@ -1965,13 +2038,12 @@ private static Chainable sorted(Iterable items, boolean ascending) { } } - private static Chainable sortedBy(Iterable items, BiFunction comparator, boolean ascending) { - Chainable i = Chainable.from(items); + private static Chainable sortedBy(Iterable items, BiFunction comparator, boolean ascending) { if (items == null || comparator == null) { - return i; + return Chainable.from(items); } - List list = i.toList(); + List list = toList(items); list.sort(new Comparator() { @Override @@ -1983,73 +2055,30 @@ public int compare(T o1, T o2) { return Chainable.from(list); } - private static Chainable sortedBy(Iterable items, ToStringFunction keyExtractor, boolean ascending) { - Chainable i = Chainable.from(items); - if (keyExtractor == null) { - return i; - } else { - return sortedBy(i, (o1, o2) -> Objects.compare( + private static Chainable sortedBy(Iterable items, ToStringFunction keyExtractor, boolean ascending) { + return (keyExtractor == null) ? Chainable.from(items) : sortedBy(items, (o1, o2) -> Objects.compare( keyExtractor.apply(o1), keyExtractor.apply(o2), Comparator.comparing(String::toString)), ascending); - } } - private static Chainable sortedBy(Iterable items, ToLongFunction keyExtractor, boolean ascending) { - Chainable i = Chainable.from(items); - if (keyExtractor == null) { - return i; - } else { - return sortedBy(i, (o1, o2) -> Long.compare( + private static Chainable sortedBy(Iterable items, ToLongFunction keyExtractor, boolean ascending) { + return (keyExtractor == null) ? Chainable.from(items) : sortedBy(items, (o1, o2) -> Long.compare( keyExtractor.applyAsLong(o1), keyExtractor.applyAsLong(o2)), ascending); - } - } - - private static Chainable sortedBy(Iterable items, ToDoubleFunction keyExtractor, boolean ascending) { - Chainable i = Chainable.from(items); - if (keyExtractor == null) { - return i; - } else { - return sortedBy(i, (o1, o2) -> Double.compare(keyExtractor.applyAsDouble(o1), keyExtractor.applyAsDouble(o2)), ascending); - } - } - - /** - * @param items - * @param replacer - * @return - * @see Chainable#replace(Function) - */ - public static Chainable replace(Iterable items, Function> replacer) { - return transformAndFlatten(items, replacer).withoutNull(); } - /** - * @param items - * @return - * @see Chainable#reverse() - */ - public static Chainable reverse(Iterable items) { - return (items == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - List list = Chainables.toList(items); - int nextIndex = list.size() - 1; - - @Override - public boolean hasNext() { - return (nextIndex >= 0); - } - - @Override - public T next() { - return (this.hasNext()) ? list.get(this.nextIndex--) : null; - } - }); + private static Chainable sortedBy(Iterable items, ToDoubleFunction keyExtractor, boolean ascending) { + return (keyExtractor == null) + ? Chainable.from(items) + : sortedBy(items, (o1, o2) -> Double.compare( + keyExtractor.applyAsDouble(o1), + keyExtractor.applyAsDouble(o2)), ascending); } /** - * Splits the specified {@code text} into a individual characters/ + * Splits the specified {@code text} into a individual characters. * @param text the text to split * @return a chain of characters */ @@ -2094,7 +2123,7 @@ public String next() { /** * Splits the specified {@code text} using the specified {@code delimiterChars}. - * @param text + * @param text the text to split * @param delimiterCharacters * @return the split strings, including the delimiters */ @@ -2104,9 +2133,11 @@ public static Chainable split(String text, String delimiterCharacters) { /** * Splits the specified {@code text} using the specified {@code delimiterChars}. - * @param text - * @param delimiterCharacters - * @param includeDelimiters if true, the delimiter chars are included in the returned results + *

+ * Each of the characters in the specified {@code delimiterCharacters} is used as a separator individually. + * @param text the text to split + * @param delimiterCharacters the characters to use to split the specified {@code text} + * @param includeDelimiters if {@code true}, the delimiter chars are included in the returned results, otherwise they're not * @return the split strings */ public static Chainable split(String text, String delimiterCharacters, boolean includeDelimiters) { @@ -2132,15 +2163,15 @@ public String next() { * @see Chainable#startsWithEither(Iterable...) */ @SafeVarargs - public static boolean startsWithEither(Iterable items, Iterable... prefixes) { - if (Chainables.isNullOrEmpty(items)) { + public static boolean startsWithEither(Iterable items, Iterable... prefixes) { + if (isNullOrEmpty(items)) { return false; } else if (prefixes == null) { return false; } - for (Iterable prefix : prefixes) { - Iterator prefixIterator = prefix.iterator(); + for (Iterable prefix : prefixes) { + Iterator prefixIterator = prefix.iterator(); for (T item : items) { if (!prefixIterator.hasNext()) { return true; @@ -2171,10 +2202,10 @@ public static boolean startsWithEither(Iterable items, Iterable... pre * @return * @see Chainable#sum(Function) */ - public static long sum(Iterable items, Function valueExtractor) { + public static long sum(Iterable items, Function valueExtractor) { int sum = 0; if (!Chainables.isNullOrEmpty(items)) { - Chainable numbers = Chainables.withoutNull(items).transform(valueExtractor); + Chainable numbers = withoutNull(items).transform(valueExtractor); for (Long number : numbers) { if (number != null) { sum += number; @@ -2211,7 +2242,8 @@ public static String[] toArray(Iterable items) { * @return * @see Chainable#toList() */ - public static List toList(Iterable items) { + @SuppressWarnings("unchecked") + public static List toList(Iterable items) { if (items == null) { return null; } else if (items instanceof List) { @@ -2232,7 +2264,7 @@ public static List toList(Iterable items) { * @return * @see Chainable#toMap(Function) */ - public static Map toMap(Iterable items, Function keyExtractor) { + public static Map toMap(Iterable items, Function keyExtractor) { if (items == null || keyExtractor == null) { return Collections.emptyMap(); } @@ -2248,7 +2280,7 @@ public static Map toMap(Iterable items, Function keyExtrac * @see Chainable#toQueue() */ @Experimental - public static ChainableQueue toQueue(Iterable items) { + public static ChainableQueue toQueue(Iterable items) { return new ChainableQueueImpl<>(items); } @@ -2269,9 +2301,9 @@ public static Stream toStream(Iterable items) { * @return the transformed items * @see Chainable#transform(Function) */ - public static Chainable transform(Iterable items, Function transformer) { + public static Chainable transform(Iterable items, Function transformer) { return (items == null || transformer == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - Iterator iterator = items.iterator(); + Iterator iterator = items.iterator(); @Override public boolean hasNext() { @@ -2296,9 +2328,9 @@ public O next() { * @return * @see Chainable#transformAndFlatten(Function) */ - public static Chainable transformAndFlatten(Iterable items, Function> transformer) { + public static Chainable transformAndFlatten(Iterable items, Function> transformer) { return (items == null || transformer == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - private final Iterator iterIn = items.iterator(); + private final Iterator iterIn = items.iterator(); private Iterator iterOut = null; private boolean stopped = false; @@ -2331,11 +2363,11 @@ public O next() { } private static Chainable traverse( - Iterable initials, - Function> childTraverser, + Iterable initials, + Function> childTraverser, boolean breadthFirst) { return (initials == null || childTraverser == null) ? Chainable.empty() : Chainable.fromIterator(() -> new Iterator() { - Deque> iterators = new LinkedList<>(Arrays.asList(initials.iterator())); + Deque> iterators = new LinkedList<>(Arrays.asList(initials.iterator())); T nextItem = null; Set seenValues = new HashSet<>(); @@ -2343,7 +2375,7 @@ private static Chainable traverse( public boolean hasNext() { // TODO Trips up on NULL values "by design" - should it? while (this.nextItem == null && !iterators.isEmpty()) { - Iterator currentIterator = iterators.peekFirst(); + Iterator currentIterator = iterators.peekFirst(); if (Chainables.isNullOrEmpty(currentIterator)) { iterators.pollFirst(); continue; @@ -2390,7 +2422,7 @@ public T next() { * @see Chainable#whereEither(Predicate...) */ @SafeVarargs - public static final Chainable whereEither(Iterable items, Predicate... predicates) { + public static final Chainable whereEither(Iterable items, Predicate... predicates) { if (items == null) { return Chainable.empty(); } else if (predicates == null || predicates.length == 0) { @@ -2398,7 +2430,7 @@ public static final Chainable whereEither(Iterable items, Predicate } return Chainable.fromIterator(() -> new Iterator() { - final Iterator innerIterator = items.iterator(); + final Iterator innerIterator = items.iterator(); T nextItem = null; boolean stopped = false; @@ -2418,7 +2450,7 @@ public boolean hasNext() { continue; } - for (Predicate predicate : predicates) { + for (Predicate predicate : predicates) { if (predicate.test(this.nextItem)) { return true; } @@ -2448,8 +2480,8 @@ public T next() { * @return chain without null values * @see Chainable#withoutNull() */ - public static Chainable withoutNull(Iterable items) { - return (items != null) ? Chainable.from(items).where(i -> i != null) : null; + public static Chainable withoutNull(Iterable items) { + return (items != null) ? whereEither(items, i -> i != null) : null; } } diff --git a/src/test/java/com/github/chainables/chainable/ChainableTest.java b/src/test/java/com/github/chainables/chainable/ChainableTest.java index 105ab69..a823321 100644 --- a/src/test/java/com/github/chainables/chainable/ChainableTest.java +++ b/src/test/java/com/github/chainables/chainable/ChainableTest.java @@ -364,16 +364,14 @@ public void testChain() { .last(); Chainable unseededChain = Chainable - .empty() // Test chaining without a seed + .empty(Long.class) // Test chaining without a seed .chain(o -> Math.round(Math.random() * 10)) - .first(5) - .cast(Long.class); + .first(5); Chainable trulyEmptyChain = Chainable - .empty() + .empty(String.class) .chain(i -> (i == null) ? null : "A") - .first(5) - .cast(String.class); + .first(5); // Then assertEquals(expected, actual); @@ -738,6 +736,36 @@ public void testFrom() { assertEquals(expectedTransformed, actualTransformed); } + @Test + public void testGet() { + // Given + Chainable infiniteChain = Chainable + .from(0) + .chain(i -> i + 1); + + Chainable listChain = Chainable + .from(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + + Chainable cachedChain = Chainable + .empty(Long.class) + .chain(i -> Math.round(Math.random() * 1000)) + .first(10) + .cached(); + + // When + int item5FromInfinite = infiniteChain.get(5); + int item5FromList = listChain.get(5); + long cachedCount = cachedChain.count(); + long item5FromCache = cachedChain.get(5); + long item5FromCacheAgain = cachedChain.get(5); + + // Then + assertEquals(5, item5FromInfinite); + assertEquals(5, item5FromList); + assertEquals(10, cachedCount); + assertEquals(item5FromCache, item5FromCacheAgain); + } + @SuppressWarnings("unchecked") @Test public void testInterleave() { @@ -1015,8 +1043,8 @@ public void testSplit() { int expectedChars = text.length(); // When - Chainable tokens = Chainables.split(text, " ,'\"!?.()[]{};:-+="); - Chainable chars = Chainables.split(text); + Chainable tokens = Chainable.split(text, " ,'\"!?.()[]{};:-+="); + Chainable chars = Chainable.split(text); String mergedTokens = tokens.join(); String mergedChars = chars.join(); @@ -1094,6 +1122,33 @@ public void testStreamReEntry() { assertEquals(expected, actual); } + @Test void testStreamPartialTraversal() { + // Given + Integer inputs[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + Chainable chain = Chainable.from(Stream.of(inputs)); + String first4Expected = Chainable.from(inputs).first(4).join(); + String first3Expected = Chainable.from(inputs).first(3).join(); + String first6Expected = Chainable.from(inputs).first(6).join(); + String allExpected = Chainable.from(inputs).join(); + + // When + Chainable first4 = chain.first(4); + String first4Actual = first4.join(); + Chainable first3 = first4.first(3); + String first3Actual = first3.join(); + Chainable first6 = chain.first(6); + String first6Actual = first6.join(); + String allActual = chain.join(); + String allAgainActual = chain.join(); + + // Then + assertEquals(first4Expected, first4Actual); + assertEquals(first3Expected, first3Actual); + assertEquals(first6Expected, first6Actual); + assertEquals(allExpected, allActual); + assertEquals(allExpected, allAgainActual); + } + @Test public void testSum() { // Given diff --git a/src/test/java/com/github/chainables/chainable/ChainableTreeTest.java b/src/test/java/com/github/chainables/chainable/ChainableTreeTest.java index de1d2d0..c1e35fb 100644 --- a/src/test/java/com/github/chainables/chainable/ChainableTreeTest.java +++ b/src/test/java/com/github/chainables/chainable/ChainableTreeTest.java @@ -200,6 +200,36 @@ public void testDepthFirstNotBelow() { assertEquals(expected, actual); } + @Test + public void testExamplePermutations() { + // Given + String expected = "a, b, c, aa, ab, ac, ba, bb, bc, ca, cb, cc, aaa, aab, aac, aba, abb, abc, aca, acb, acc, baa, bab, bac, bba, bbb, bbc, bca, bcb, bcc, caa, cab, cac, cba, cbb, cbc, cca, ccb, ccc"; + long maxLength = 3; + char[] alphabet = { 'a', 'b', 'c' }; + long expectedCount = Math.round((1 - Math.pow(alphabet.length, maxLength + 1))/(1 - alphabet.length)) - 1; + + // When + ChainableTree permutations = ChainableTree + .withRoot("") // Blank string at the root + .withChildValueExtractor(p -> Chainable + .empty(String.class) // Start with empty chain of strings + .chainIndexed((s, i) -> p + alphabet[i.intValue()]) // Append each consecutive letter the parent + .first(alphabet.length)); // Limit the children chain to the size of the alphabet + + // Limit the depth of the infinite tree to the level of 3-letter strings + Chainable> permutationsUptoLength5 = permutations + .notBelowWhere(t -> t.value().length() >= maxLength) // Limit tree depth to 5 levels + .breadthFirst() // Traverse breadth-first + .afterFirst() // Skip the empty root + .cached(); // Cache the evaluated sequence since it won't change when recalc'd again + + String actual = permutationsUptoLength5.join(", "); + + // Then + assertEquals(expected, actual); + assertEquals(expectedCount, permutationsUptoLength5.count()); + } + @Test public void testFirst() { // Given