From 8357d7f5d89ad50b53c8519fb3ec9562df0e626a Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 14 Oct 2018 08:55:09 +0100 Subject: [PATCH] [tree] Add TreeBuilder (#57) * [tree] TreeBuilder: whenEmptyBuilderBuildThenTreeIsAnEmptyLeaf() * [tree] Provide default implementation of count() * [tree] Extract TreeMapper * [tree] MutableTree added * [tree] TreeBuilder: whenAddLeafThenTreeHasLeaf() * [tree] TreeBuilder: whenAddSubTreeThenTreeHasSubTree() * [maybe] Add Maybe.findFirst(Stream) and matchValue(Function, Supplier) * [tree] TreeBuilder: whenAddGrandChildThenTreeHasGrandChild() * [tree] TreeBuilder: whenAddMultipleChildrenThenTreeHasAllChildren() * [tree] Tree: avoid leaking MutableTree type as parameters * [changelog] update * [changelog][readme] Update to include TreeBuilder --- CHANGELOG | 6 +- README.org | 130 +++++++++++++- images/treebuilder-example.dot | 8 + images/treebuilder-example.svg | 91 ++++++++++ src/main/java/net/kemitix/mon/maybe/Just.java | 8 + .../java/net/kemitix/mon/maybe/Maybe.java | 28 +++ .../java/net/kemitix/mon/maybe/Nothing.java | 8 + .../net/kemitix/mon/tree/GeneralisedTree.java | 28 +-- .../net/kemitix/mon/tree/MutableTree.java | 168 ++++++++++++++++++ .../kemitix/mon/tree/MutableTreeBuilder.java | 91 ++++++++++ src/main/java/net/kemitix/mon/tree/Tree.java | 37 +++- .../net/kemitix/mon/tree/TreeBuilder.java | 88 +++++++++ .../java/net/kemitix/mon/tree/TreeMapper.java | 55 ++++++ src/test/java/net/kemitix/mon/MaybeTest.java | 55 ++++++ .../kemitix/mon/tree/GeneralisedTreeTest.java | 4 +- .../net/kemitix/mon/tree/MutableTreeTest.java | 108 +++++++++++ .../net/kemitix/mon/tree/TreeBuilderTest.java | 109 ++++++++++++ 17 files changed, 986 insertions(+), 36 deletions(-) create mode 100644 images/treebuilder-example.dot create mode 100644 images/treebuilder-example.svg create mode 100644 src/main/java/net/kemitix/mon/tree/MutableTree.java create mode 100644 src/main/java/net/kemitix/mon/tree/MutableTreeBuilder.java create mode 100644 src/main/java/net/kemitix/mon/tree/TreeBuilder.java create mode 100644 src/main/java/net/kemitix/mon/tree/TreeMapper.java create mode 100644 src/test/java/net/kemitix/mon/tree/MutableTreeTest.java create mode 100644 src/test/java/net/kemitix/mon/tree/TreeBuilderTest.java diff --git a/CHANGELOG b/CHANGELOG index e7a4579..6bd5222 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,13 @@ CHANGELOG ========= -1.2.1 +2.0.0 ----- +* [BREAKING] [tree] Tree.item() now returns a Maybe +* [tree] Add TreeBuilder +* [maybe] Add static Maybe.findFirst(Stream) +* [maybe] Add matchValue(Function, Supplier) * Bump kemitix-parent from 5.1.1 to 5.2.0 (#55) * Bump junit from 4.12 to 5.3.1 [#56) diff --git a/README.org b/README.org index 89230a7..aa35759 100644 --- a/README.org +++ b/README.org @@ -255,7 +255,7 @@ have a =get()= method. Unlike =Optional=, when a =map()= results in a =null=, the =Maybe= will - continue to be a =Just=. =Optional= would switch to being empty. [[http://blog.vavr.io/the-agonizing-death-of-an-astronaut/][vavi.io + continue to be a =Just=. =Optional= would switch to being empty. [[http://blog.vavr.io/the-agonizing-death-of-an-astronaut/][vavr.io follows the same behaviour as =Maybe=]]. #+BEGIN_SRC java @@ -385,6 +385,16 @@ #+END_SRC +**** =static Maybe findFirst(Stream stream)= + + Creates a Maybe from the first item in the stream, or nothing if the stream is empty. + + #+BEGIN_SRC java + final Maybe just3 = Maybe.findFirst(Stream.of(3, 4, 2, 4)); + final Maybe nothing = Maybe.findFirst(Stream.empty()); + #+END_SRC + + *** Instance Methods **** =Maybe filter(Predicate predicate)= @@ -421,7 +431,8 @@ **** =void match(Consumer just, Runnable nothing)= - Matches the Maybe, either just or nothing, and performs either the Consumer, for Just, or Runnable for nothing. + Matches the Maybe, either just or nothing, and performs either the + Consumer, for Just, or Runnable for nothing. #+BEGIN_SRC java Maybe.maybe(getValue()) @@ -432,6 +443,20 @@ #+END_SRC +**** = R matchValue(Function justMatcher, Supplier nothingMatcher)= + + Matches the Maybe, either just or nothing, and performs either the + Function, for Just, or Supplier for nothing. + + #+BEGIN_SRC java + final String value = Maybe.maybe(getValue()) + .matchValue( + just -> Integer.toString(just), + () -> "nothing" + ); + #+END_SRC + + **** =T orElse(T otherValue)= A value to use when Maybe is Nothing. @@ -907,6 +932,25 @@ #+END_SRC +**** =static TreeBuilder builder(final Class type)= + + Create a new TreeBuilder starting with an empty tree. + + #+BEGIN_SRC java + final TreeBuilder builder = Tree.builder(Integer.class); + #+END_SRC + + +**** =static TreeBuilder builder(final Tree tree)= + + Create a new TreeBuilder for the given tree. + + #+BEGIN_SRC java + final Tree tree = ...; + final TreeBuilder builder = Tree.builder(tree); + #+END_SRC + + *** Instance Methods **** = Tree map(Function f)= @@ -920,13 +964,13 @@ #+END_SRC -**** =Optional item()= +**** =Maybe item()= - Returns the contents of the Tree node within an Optional. + Returns the contents of the Tree node within a Maybe. #+BEGIN_SRC java final Tree tree = ...; - final Optional result = tree.item(); + final Maybe result = tree.item(); #+END_SRC @@ -951,6 +995,82 @@ #+END_SRC +** TreeBuilder + + A mutable builder for a Tree. Each TreeBuilder allows modification of a + single Tree node. You can use the =select(childItem)= method to get a + TreeBuilder for the subtree that has the given child item. + + #+BEGIN_SRC java + final TreeBuilder builder = Tree.builder(); + builder.set(12).addChildren(Arrays.asList(1, 3, 5, 7)); + final TreeBuilder builderFor3 = builder.select(3); + builderFor3.addChildren(Arrays.asList(2, 4)); + final Tree tree = builder.build(); + #+END_SRC + + Will produce a tree like: + + [[file:images/treebuilder-example.svg]] + + +*** Static Constructors + + None. The TreeBuilder is instantiated by =Tree.builder()=. + +*** Instance Methods + +**** =Tree build()= + + Create the immutable Tree. + + #+BEGIN_SRC java + final TreeBuilder builder = Tree.builder(); + final Tree tree = builder.build(); + #+END_SRC + + +**** =TreeBuilder item(T item)= + + Set the current Tree's item and return the TreeBuilder. + + #+BEGIN_SRC java + #+END_SRC + + +**** =TreeBuilder add(Tree subtree)= + + Adds the subtree to the current tree. + + #+BEGIN_SRC java + #+END_SRC + + +**** =TreeBuilder addChild(T childItem)= + + Add the Child item as a sub-Tree. + + #+BEGIN_SRC java + #+END_SRC + + +**** =TreeBuilder addChildren(List children)= + + Add all the child items as subTrees. + + #+BEGIN_SRC java + #+END_SRC + + +**** =Maybe> select(T childItem)= + + Create a TreeBuilder for the subTree of the current Tree that has the + childItem. + + #+BEGIN_SRC java + #+END_SRC + + ** Lazy A Lazy evaluated expression. Using a Supplier to provide the value, only diff --git a/images/treebuilder-example.dot b/images/treebuilder-example.dot new file mode 100644 index 0000000..c3bab28 --- /dev/null +++ b/images/treebuilder-example.dot @@ -0,0 +1,8 @@ +digraph G { + 12 -> 1; + 12 -> 3; + 12 -> 5; + 12 -> 7; + 3 -> 2; + 3 -> 4; +} diff --git a/images/treebuilder-example.svg b/images/treebuilder-example.svg new file mode 100644 index 0000000..26451ec --- /dev/null +++ b/images/treebuilder-example.svg @@ -0,0 +1,91 @@ + + + + + + +G + + + +12 + +12 + + + +1 + +1 + + + +12->1 + + + + + +3 + +3 + + + +12->3 + + + + + +5 + +5 + + + +12->5 + + + + + +7 + +7 + + + +12->7 + + + + + +2 + +2 + + + +3->2 + + + + + +4 + +4 + + + +3->4 + + + + + diff --git a/src/main/java/net/kemitix/mon/maybe/Just.java b/src/main/java/net/kemitix/mon/maybe/Just.java index 3c67907..9a7c96b 100644 --- a/src/main/java/net/kemitix/mon/maybe/Just.java +++ b/src/main/java/net/kemitix/mon/maybe/Just.java @@ -118,6 +118,14 @@ final class Just implements Maybe { justMatcher.accept(value); } + @Override + public R matchValue( + final Function justMatcher, + final Supplier nothingMatcher + ) { + return justMatcher.apply(value); + } + @Override public Maybe or(final Supplier> alternative) { return this; diff --git a/src/main/java/net/kemitix/mon/maybe/Maybe.java b/src/main/java/net/kemitix/mon/maybe/Maybe.java index d28418f..3041bdc 100644 --- a/src/main/java/net/kemitix/mon/maybe/Maybe.java +++ b/src/main/java/net/kemitix/mon/maybe/Maybe.java @@ -81,6 +81,19 @@ public interface Maybe extends Functor> { return just(value); } + /** + * Creates a Maybe from the first item in the stream, or nothing if the stream is empty. + * + * @param stream the Stream + * @param the type of the stream + * @return a Maybe containing the first item in the stream + */ + public static Maybe findFirst(Stream stream) { + return stream.findFirst() + .map(Maybe::just) + .orElse(Maybe.nothing()); + } + /** * Checks if the Maybe is a Just. * @@ -173,11 +186,26 @@ public interface Maybe extends Functor> { /** * Matches the Maybe, either just or nothing, and performs either the Consumer, for Just, or Runnable for nothing. * + *

Unlike {@link #matchValue(Function, Supplier)}, this method does not return a value.

+ * * @param justMatcher the Consumer to pass the value of a Just to * @param nothingMatcher the Runnable to call if the Maybe is a Nothing */ public abstract void match(Consumer justMatcher, Runnable nothingMatcher); + /** + * Matches the Maybe, either just or nothing, and performs either the Function, for Just, or Supplier for nothing. + * + *

Unlike {@link #match(Consumer, Runnable)}, this method returns a value.

+ * + * @param justMatcher the Function to pass the value of a Just to + * @param nothingMatcher the Supplier to call if the Maybe is a Nothing + * @param the type of the matched result + * + * @return the matched result, from either the justMatcher or the nothingMatcher + */ + public abstract R matchValue(Function justMatcher, Supplier nothingMatcher); + /** * Maps the Maybe into another Maybe only when it is nothing. * diff --git a/src/main/java/net/kemitix/mon/maybe/Nothing.java b/src/main/java/net/kemitix/mon/maybe/Nothing.java index b007c06..2d4791c 100644 --- a/src/main/java/net/kemitix/mon/maybe/Nothing.java +++ b/src/main/java/net/kemitix/mon/maybe/Nothing.java @@ -100,6 +100,14 @@ final class Nothing implements Maybe { nothingMatcher.run(); } + @Override + public R matchValue( + final Function justMatcher, + final Supplier nothingMatcher + ) { + return nothingMatcher.get(); + } + @Override public Maybe or(final Supplier> alternative) { return alternative.get(); diff --git a/src/main/java/net/kemitix/mon/tree/GeneralisedTree.java b/src/main/java/net/kemitix/mon/tree/GeneralisedTree.java index 5e4cb7a..5c4ef78 100644 --- a/src/main/java/net/kemitix/mon/tree/GeneralisedTree.java +++ b/src/main/java/net/kemitix/mon/tree/GeneralisedTree.java @@ -22,10 +22,10 @@ package net.kemitix.mon.tree; import lombok.EqualsAndHashCode; +import net.kemitix.mon.maybe.Maybe; import java.util.*; import java.util.function.Function; -import java.util.stream.Collectors; /** * A generic tree of trees and objects. @@ -37,7 +37,7 @@ import java.util.stream.Collectors; * @author Paul Campbell (pcampbell@kemitix.net) */ @EqualsAndHashCode -class GeneralisedTree implements Tree { +class GeneralisedTree implements Tree, TreeMapper { private final T item; private final List> subTrees; @@ -62,30 +62,12 @@ class GeneralisedTree implements Tree { */ @Override public Tree map(final Function f) { - return Tree.of(f.apply(item), mapSubTrees(f)); + return Tree.of(f.apply(item), mapTrees(f, subTrees())); } @Override - public Optional item() { - return Optional.ofNullable(item); - } - - private List> mapSubTrees(final Function f) { - return subTrees.stream() - .map(subTree -> subTree.map(f::apply)) - .collect(Collectors.toList()); - } - - /** - * Counts the number of items in the subtree. - * - * @return the sum of the subtrees, plus 1 if there is an item in this node - */ - @Override - @SuppressWarnings("avoidinlineconditionals") - public int count() { - return (item != null ? 1 : 0) - + subTrees.stream().mapToInt(Tree::count).sum(); + public Maybe item() { + return Maybe.maybe(item); } /** diff --git a/src/main/java/net/kemitix/mon/tree/MutableTree.java b/src/main/java/net/kemitix/mon/tree/MutableTree.java new file mode 100644 index 0000000..4674e14 --- /dev/null +++ b/src/main/java/net/kemitix/mon/tree/MutableTree.java @@ -0,0 +1,168 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon.tree; + +import lombok.EqualsAndHashCode; +import net.kemitix.mon.maybe.Maybe; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * A mutable {@link Tree}. + * + * @param the type of the objects help in the tree + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +@EqualsAndHashCode +@SuppressWarnings("methodcount") +class MutableTree implements Tree, TreeMapper { + + private final List> mySubTrees = new ArrayList<>(); + + private T item; + + /** + * Create a new {@link MutableTree}. + * + * @param the type of the {@link Tree}. + * + * @return the MutableTree + */ + static MutableTree create() { + return new MutableTree<>(); + } + + /** + * Create a new mutable tree to hold object of a type. + * + * @param item the item for the leaf node + * @param the type of the object to be held in the tree + * + * @return a empty mutable tree + */ + static MutableTree leaf(final B item) { + return MutableTree.create().set(item); + } + + /** + * Create a new mutable tree to hold the single item. + * + * @param item the item for the tree node + * @param subtrees the subtrees of the tree node + * @param the type of the item + * @return a leaf node of a generalised tree + */ + static MutableTree of(final B item, final Collection> subtrees) { + return MutableTree.create().set(item).subTrees(subtrees); + } + + /** + * Duplicate, or cast if possible, an existing Tree as a {@link MutableTree}. + * @param tree the tree to duplicate/cast + * @param the type of the tree + * @return the mutable tree + */ + static MutableTree of(final Tree tree) { + if (tree instanceof MutableTree) { + return (MutableTree) tree; + } + final T item = tree.item().orElse(null); + final List> subtrees = tree.subTrees() + .stream().map(MutableTree::of).collect(Collectors.toList()); + return MutableTree.of(item, subtrees); + } + + @Override + public MutableTree map(final Function f) { + final MutableTree mutableTree = MutableTree.create(); + final List> trees = subTreesAsMutable().stream() + .map(subTree -> subTree.map(f)) + .collect(Collectors.toList()); + return mutableTree + .set(f.apply(item)) + .subTrees(trees); + } + + @Override + public Maybe item() { + return Maybe.maybe(item); + } + + /** + * Sets the item of the tree. + * + * @param newItem the new item + * + * @return the tree + */ + MutableTree set(final T newItem) { + this.item = newItem; + return this; + } + + @Override + @SuppressWarnings("unchecked") + public List> subTrees() { + final Stream> mutableTreeStream = mySubTrees.stream(); + final Stream> treeStream = mutableTreeStream.map(Tree.class::cast); + final List> treeList = treeStream.collect(Collectors.toList()); + return treeList; + } + + /** + * Sets the subTrees of the tree. + * + * @param subTrees the subtrees + * @return the tree + */ + MutableTree subTrees(final Collection> subTrees) { + this.mySubTrees.clear(); + this.mySubTrees.addAll(subTrees); + return this; + } + + /** + * Adds the subtree to the existing subtrees. + * + * @param subtree the subtree + * @return the current tree + */ + MutableTree add(final Tree subtree) { + mySubTrees.add(MutableTree.of(subtree)); + return this; + } + + /** + * The mutable subtrees of the tree. + * + * @return a list of Trees + */ + List> subTreesAsMutable() { + return mySubTrees; + } +} diff --git a/src/main/java/net/kemitix/mon/tree/MutableTreeBuilder.java b/src/main/java/net/kemitix/mon/tree/MutableTreeBuilder.java new file mode 100644 index 0000000..da07431 --- /dev/null +++ b/src/main/java/net/kemitix/mon/tree/MutableTreeBuilder.java @@ -0,0 +1,91 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon.tree; + +import net.kemitix.mon.maybe.Maybe; + +import java.util.function.Function; + +/** + * Builder for a {@link Tree}. + * + * @param the type of the tree + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +class MutableTreeBuilder implements TreeBuilder { + + private final MutableTree root; + + /** + * Create empty tree builder. + */ + MutableTreeBuilder() { + root = MutableTree.create(); + } + + /** + * Create a tree builder to work with the given tree. + * + * @param tree the tree to build upon + */ + MutableTreeBuilder(final MutableTree tree) { + root = tree; + } + + @Override + public Tree build() { + return root.map(Function.identity()); + } + + @Override + public TreeBuilder item(final T item) { + root.set(item); + return this; + } + + @Override + public TreeBuilder add(final Tree subtree) { + root.add(subtree); + return this; + } + + @Override + public TreeBuilder addChild(final T childItem) { + root.add(MutableTree.leaf(childItem)); + return this; + } + + @Override + public Maybe> select(final T childItem) { + return Maybe.findFirst( + root.subTreesAsMutable() + .stream() + .filter((MutableTree tree) -> matchesItem(childItem, tree)) + .map(Tree::builder)); + } + + private Boolean matchesItem(final T childItem, final MutableTree tree) { + return tree.item().map(childItem::equals).orElse(false); + } + +} diff --git a/src/main/java/net/kemitix/mon/tree/Tree.java b/src/main/java/net/kemitix/mon/tree/Tree.java index 941461b..e7933c9 100644 --- a/src/main/java/net/kemitix/mon/tree/Tree.java +++ b/src/main/java/net/kemitix/mon/tree/Tree.java @@ -22,11 +22,11 @@ package net.kemitix.mon.tree; import net.kemitix.mon.Functor; +import net.kemitix.mon.maybe.Maybe; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.function.Function; /** @@ -62,22 +62,49 @@ public interface Tree extends Functor> { return new GeneralisedTree<>(item, subtrees); } + /** + * Create a new {@link TreeBuilder} starting with an empty tree. + * + * @param type the type + * @param the type of the tree + * + * @return a TreeBuilder + */ + public static TreeBuilder builder(final Class type) { + return new MutableTreeBuilder<>(); + } + + /** + * Create a new {@link TreeBuilder} for the given tree. + * + * @param tree the tree to build upon + * @param the type of the tree + * + * @return a TreeBuilder + */ + public static TreeBuilder builder(final Tree tree) { + return new MutableTreeBuilder<>(MutableTree.of(tree)); + } + @Override public abstract Tree map(Function f); /** * Return the item within the node of the tree, if present. * - * @return an Optional containing the item + * @return a Maybe containing the item */ - public abstract Optional item(); + public abstract Maybe item(); /** * Count the number of item in the tree, including subtrees. * - * @return the number of items + * @return the sum of the subtrees, plus 1 if there is an item in this node */ - public abstract int count(); + public default int count() { + return item().matchValue(x -> 1, () -> 0) + + subTrees().stream().mapToInt(Tree::count).sum(); + } /** * The subtrees of the tree. diff --git a/src/main/java/net/kemitix/mon/tree/TreeBuilder.java b/src/main/java/net/kemitix/mon/tree/TreeBuilder.java new file mode 100644 index 0000000..6c33420 --- /dev/null +++ b/src/main/java/net/kemitix/mon/tree/TreeBuilder.java @@ -0,0 +1,88 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon.tree; + +import net.kemitix.mon.maybe.Maybe; + +import java.util.List; + +/** + * Mutable builder for a {@link Tree}. + * + * @param the type of the tree to build + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +public interface TreeBuilder { + + /** + * Create the immutable {@link Tree}. + * + * @return a {@link Tree} + */ + public abstract Tree build(); + + /** + * Set the current {@link Tree}'s item. + * + * @param item the item for the current {@link Tree} + * + * @return the TreeBuilder + */ + public abstract TreeBuilder item(T item); + + /** + * Adds the subtree to the current tree. + * + * @param subtree the tree to add + * + * @return the TreeBuilder + */ + public abstract TreeBuilder add(Tree subtree); + + /** + * Add the Child item as a subTree. + * + * @param childItem the item to add as a subtree + * @return the TreeBuilder + */ + public abstract TreeBuilder addChild(T childItem); + + /** + * Add all the child items as subTrees. + * + * @param children the items to add as a subtree + * @return the TreeBuilder + */ + public default TreeBuilder addChildren(List children) { + children.forEach(this::addChild); + return this; + } + + /** + * Create a TreeBuilder for the subTree of the current Tree that has the childItem. + * + * @param childItem the item of search the subtrees for + * @return a Maybe containing the TreeBuilder for the subtree, or Nothing if there child item is not found + */ + public abstract Maybe> select(T childItem); +} diff --git a/src/main/java/net/kemitix/mon/tree/TreeMapper.java b/src/main/java/net/kemitix/mon/tree/TreeMapper.java new file mode 100644 index 0000000..6453f06 --- /dev/null +++ b/src/main/java/net/kemitix/mon/tree/TreeMapper.java @@ -0,0 +1,55 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2018 Paul Campbell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies + * or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.kemitix.mon.tree; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Maps a list of Trees. + * + * @param the type of the objects help in the tree + * + * @author Paul Campbell (pcampbell@kemitix.net) + */ +interface TreeMapper { + + /** + * Map the Trees. + * + * @param f the mapping function + * @param trees the trees to map + * @param the type of the resulting Trees + * + * @return a List of mapped sub-trees + */ + public default List> mapTrees( + final Function f, + final List> trees + ) { + return trees.stream() + .map(subTree -> subTree.map(f)) + .collect(Collectors.toList()); + } + +} diff --git a/src/test/java/net/kemitix/mon/MaybeTest.java b/src/test/java/net/kemitix/mon/MaybeTest.java index efc3fa9..4da89c9 100644 --- a/src/test/java/net/kemitix/mon/MaybeTest.java +++ b/src/test/java/net/kemitix/mon/MaybeTest.java @@ -1,6 +1,7 @@ package net.kemitix.mon; import net.kemitix.mon.maybe.Maybe; +import org.assertj.core.api.AbstractIntegerAssert; import org.assertj.core.api.WithAssertions; import org.junit.jupiter.api.Test; @@ -8,7 +9,9 @@ import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Stream; import static net.kemitix.mon.maybe.Maybe.*; @@ -252,6 +255,26 @@ class MaybeTest implements WithAssertions { assertThat(flag).isTrue(); } + @Test + void just_whenMatchValue_thenJustTriggers() { + //given + final Maybe maybe = just(1); + //when + final String result = maybe.matchValue(integer -> "just", () -> "nothing"); + //then + assertThat(result).isEqualTo("just"); + } + + @Test + void nothing_whenMatchValue_thenNothingTriggers() { + //given + final Maybe maybe = nothing(); + //when + final String result = maybe.matchValue(integer -> "just", () -> "nothing"); + //then + assertThat(result).isEqualTo("nothing"); + } + @Test void just_isJust_isTrue() { //given @@ -311,4 +334,36 @@ class MaybeTest implements WithAssertions { //then assertThat(result.toOptional()).contains("two"); } + + @Test + void emptyStream_findFirst_isNothing() { + //given + final Stream stream = Stream.empty(); + //when + final Maybe result = Maybe.findFirst(stream); + //then + assertThat(result.isNothing()).isTrue(); + } + + @Test + void singleItemStream_findFirst_isJustItem() { + //given + final String item = "item"; + final Stream stream = Stream.of(item); + //when + final Maybe result = Maybe.findFirst(stream); + //then + assertThat(result.toOptional()).contains(item); + } + + @Test + void multipleItemStream_findFirst_isJustFirst() { + //given + final String first = "first"; + final Stream stream = Stream.of(first, "second"); + //when + final Maybe result = Maybe.findFirst(stream); + //then + assertThat(result.toOptional()).contains(first); + } } diff --git a/src/test/java/net/kemitix/mon/tree/GeneralisedTreeTest.java b/src/test/java/net/kemitix/mon/tree/GeneralisedTreeTest.java index 8cbaa35..cade535 100644 --- a/src/test/java/net/kemitix/mon/tree/GeneralisedTreeTest.java +++ b/src/test/java/net/kemitix/mon/tree/GeneralisedTreeTest.java @@ -15,7 +15,7 @@ class GeneralisedTreeTest implements WithAssertions { //when final Tree leaf = Tree.leaf(null); //then - assertThat(leaf.item()).isEmpty(); + assertThat(leaf.item().isNothing()).isTrue(); } @Test @@ -25,7 +25,7 @@ class GeneralisedTreeTest implements WithAssertions { //when final Tree leaf = Tree.leaf(item); //then - assertThat(leaf.item()).contains(item); + assertThat(leaf.item().toOptional()).contains(item); } @Test diff --git a/src/test/java/net/kemitix/mon/tree/MutableTreeTest.java b/src/test/java/net/kemitix/mon/tree/MutableTreeTest.java new file mode 100644 index 0000000..2092321 --- /dev/null +++ b/src/test/java/net/kemitix/mon/tree/MutableTreeTest.java @@ -0,0 +1,108 @@ +package net.kemitix.mon.tree; + +import org.assertj.core.api.WithAssertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +class MutableTreeTest implements WithAssertions { + + @Test + void canCreateAnEmptyLeaf() { + //when + final Tree leaf = MutableTree.create(); + //then + assertThat(leaf.item().isNothing()).isTrue(); + } + + @Test + void canCreateANonEmptyLeaf() { + //given + final String item = "item"; + //when + final MutableTree leaf = MutableTree.create(); + leaf.set(item); + //then + assertThat(leaf.item().toOptional()).contains(item); + } + + @Test + void emptyLeafHasCountZero() { + //given + final Tree tree = MutableTree.create(); + //when + final int count = tree.count(); + //then + assertThat(count).isZero(); + } + + @Test + void nonEmptyLeafHasCountOne() { + //given + final MutableTree tree = MutableTree.create(); + tree.set("value"); + //when + final int count = tree.count(); + //then + assertThat(count).isEqualTo(1); + } + + @Test + void canCreateTreeWithSubTrees() { + //given + final String leafItem = "leaf"; + final MutableTree leaf = MutableTree.leaf(leafItem); + final MutableTree tree = MutableTree.create(); + //when + tree.subTrees(singletonList(leaf)); + //then + assertThat(tree.subTrees()).containsExactly(leaf); + } + + @Test + void canMapNestedTrees() { + //given + final UUID uid1 = UUID.randomUUID(); + final UUID uid2 = UUID.randomUUID(); + final UUID uid3 = UUID.randomUUID(); + final String sid1 = uid1.toString(); + final String sid2 = uid2.toString(); + final String sid3 = uid3.toString(); + final MutableTree tree = MutableTree.of( + uid1, + asList( + MutableTree.leaf(uid2), + MutableTree.leaf(uid3))); + //when + final Tree result = tree.map(UUID::toString); + //then + final MutableTree expectedTree = MutableTree.of( + sid1, + asList( + MutableTree.leaf(sid2), + MutableTree.leaf(sid3))); + assertThat(result).isEqualToComparingFieldByFieldRecursively(expectedTree); + } + + @Test + void canCloneNonMutableTree() { + //given + final UUID rootItem = UUID.randomUUID(); + final UUID leafItem = UUID.randomUUID(); + final Tree immutableTree = Tree.of(rootItem, singletonList(Tree.leaf(leafItem))); + //when + final MutableTree mutableTree = MutableTree.of(immutableTree); + //then + assertThat(mutableTree.count()).isEqualTo(2); + assertThat(mutableTree.item().toOptional()).contains(rootItem); + final List> subTrees = mutableTree.subTrees(); + assertThat(subTrees).hasSize(1); + assertThat(subTrees.get(0).item().toOptional()).contains(leafItem); + } + +} \ No newline at end of file diff --git a/src/test/java/net/kemitix/mon/tree/TreeBuilderTest.java b/src/test/java/net/kemitix/mon/tree/TreeBuilderTest.java new file mode 100644 index 0000000..4a2a89a --- /dev/null +++ b/src/test/java/net/kemitix/mon/tree/TreeBuilderTest.java @@ -0,0 +1,109 @@ +package net.kemitix.mon.tree; + +import lombok.RequiredArgsConstructor; +import net.kemitix.mon.maybe.Maybe; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.UUID; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +class TreeBuilderTest { + + @Test + void whenEmptyBuilderBuildThenTreeIsAnEmptyLeaf() { + //when + final Tree result = Tree.builder(Node.class).build(); + //then + assertThat(result.count()).isZero(); + assertThat(result.item().isNothing()).isTrue(); + } + + @Test + void whenAddLeafThenTreeHasLeaf() { + //given + final Node node = createANode(); + //when + final Tree result = + Tree.builder(Node.class) + .item(node).build(); + //then + assertThat(result.count()).isEqualTo(1); + assertThat(result.item().toOptional()).contains(node); + } + + @Test + void whenAddSubTreeThenTreeHasSubTree() { + //given + final Tree subtree = MutableTree.leaf(createANode()); + //when + final Tree result = + Tree.builder(Node.class) + .item(createANode()) + .add(subtree) + .build(); + //then + assertThat(result.count()).isEqualTo(2); + assertThat(result.subTrees()).contains(subtree); + } + + @Test + void whenAddGrandChildThenTreeHasGrandChild() { + //given + final Node rootNode = new Node("root"); + final Node childNode = new Node("child"); + final Node grandchildNode = new Node("grandchild"); + //when + final TreeBuilder rootBuilder = + Tree.builder(Node.class) + .item(rootNode) + .addChild(childNode); + rootBuilder.select(childNode) + .map(childBuilder -> childBuilder.addChild(grandchildNode)); + final Tree result = rootBuilder.build(); + //then + assertThat(result.count()).isEqualTo(3); + assertThat(result).isEqualToComparingFieldByFieldRecursively( + MutableTree.of(rootNode, Collections.singleton( + MutableTree.of(childNode, Collections.singleton( + MutableTree.leaf(grandchildNode)))))); + } + + @Test + void whenAddMultipleChildrenThenTreeHasAllChildren() { + //given + final Node rootNode = new Node("root"); + final Node child1Node = createANode(); + final Node child2Node = createANode(); + final Node child3Node = createANode(); + //when + final Tree result = + Tree.builder(Node.class) + .item(rootNode) + .addChildren(asList(child1Node, child2Node, child3Node)) + .build(); + //then + assertThat(result.count()).isEqualTo(4); + assertThat(result).isEqualToComparingFieldByFieldRecursively( + MutableTree.of(rootNode, asList( + MutableTree.leaf(child1Node), + MutableTree.leaf(child2Node), + MutableTree.leaf(child3Node)))); + } + + private Node createANode() { + return new Node(createAName()); + } + + private String createAName() { + return UUID.randomUUID().toString(); + } + + @RequiredArgsConstructor + private static class Node { + private final String name; + } + +}