From 1a754cf7e694afe5c7a6b5045b81f4ef5bb26abd Mon Sep 17 00:00:00 2001 From: "Tristan B. Velloza Kildaire" Date: Sat, 4 May 2024 10:51:31 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Feature:=20Generic=20tree=20and=20v?= =?UTF-8?q?isitation=20framework=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tree - WIP * Tree - Added initial dfs * Tree - By default use the `always` strat * Tree - Added a TODO * Tree - Added ability to append Tree (unittests) - Updated unittests to test appending * InclusionStratergy - Now uses the `TreeNode!(T)` instead of the `T` itself * VisitationTree - Working on a visitation tree implementation * Tree - Pass in, explcitly, the touch startergy * Tree - Correct visitation stratergy * VisitationTree (unittests) - Added missing assertions * Tree (unittests) - Added missing assertions * Methods - Added rightward shifting mechanism * Methods - Added leftwards shifting mechanism * TreeNode - Added removal - Added indexing support * Tree - Reworking opSlicwe * Tree - opSlice done * Tree - Added normal opSlice as well * Tree (unittests) - Updated test for parametwerized opSlice * Tree - Added opIndex * Tree - Cleaned up - Removed `getValue()` Tree (unittests) - Added test for `removeNode(Tree!(T))` * Containers - Added `shiftIntoLeftwards` and `shiftIntoRightwards` * Containers (unittests) - Use new methods * Containers - Cleaned up * InclusionStratergy - Documented TouchStratergy - Documented * Containers - Documented method * niknaks.arrays - Moved here niknaks.containers - Moved here * niknaks.arrays - Updated unittestesd (test shrinking) - Added docs * niknaks.arrays - Refactored * niknaks.arrays - Added this * VisitationTree - Documented * VisitationTree - Documented * Tree (unittests) - Added docs * Tree (unittests) - Moved import * Tree - Added some docs * Tree - CLeaned up * Tree - Documented * Tree - Documented * Tree - Documented - Cleaned * Always(T) - Documented * Tree - Added `opDollar()` and `@property`'d `length()` * Tree - Adde doc * Tree - Renamed `Tree!(T)` to `Graph!(T)` * Graph - Typo fix * Graph - Clean up * Graph - Renamed * Graph - Documented helper methods * Graph - Typo fix * graph - Documented `dfs(...)` * Graph - Documented `toString()` * Graph (unittests) - Added tests for `opIndex(size_t)` * Graph - Implemented `getValue()` Graph (unittests) - Added tests for `getValue()` * Graph (unittests) - Added test for `opDollar()` * README - Updated docs --- README.md | 2 +- source/niknaks/containers.d | 618 ++++++++++++++++++++++++++++++++++++ 2 files changed, 619 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 444ada0..81f7fb0 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ is expected to grow over time. * Some textual manipulation routines as well * `niknaks.containers` * Some useful container types - * Things such as `CacheMap` + * Things such as `CacheMap`, `Graph` and `VisitationTree` * `niknaks.mechanisms` * User-defined input prompter, retry mechanisms * `niknaks.config` diff --git a/source/niknaks/containers.d b/source/niknaks/containers.d index 9c1a01c..19af80c 100644 --- a/source/niknaks/containers.d +++ b/source/niknaks/containers.d @@ -11,12 +11,24 @@ import std.datetime.stopwatch : StopWatch, AutoStart; import core.thread : Thread; import core.sync.condition : Condition; import std.functional : toDelegate; +import std.string : format; +import niknaks.arrays : removeResize; version(unittest) { import std.stdio : writeln; } +version(unittest) +{ + import std.functional : toDelegate; + + private void DebugTouch(T)(Graph!(T) node) + { + writeln("Touching graph node ", node); + } +} + /** * Represents an entry of * some value of type `V` @@ -597,4 +609,610 @@ unittest // Destroy the map (such that it ends the sweeper destroy(map); +} + +/** + * A visitation stratergy + * which always returns + * `true` + */ +public template Always(T) +{ + /** + * Whatever graph node is + * provided always accept + * a visitation to it + * + * Params: + * treeNode = the node + * Returns: `true` always + */ + public bool Always(Graph!(T) treeNode) + { + version(unittest) + { + import std.stdio : writeln; + writeln("Strat for: ", treeNode); + } + return true; + } +} + +/** + * A touching stratergy + * that does nothing + */ +public template Nothing(T) +{ + /** + * Consumes a graph node + * and does zilch with it + * + * Params: + * treeNode = the node + */ + public void Nothing(Graph!(T)); +} + +/** + * The inclusion stratergy which + * will be called upon the graph + * node prior to it being visited + * during a dfs operation. + * + * It is a predicate to determine + * whether or not the graph node + * in concern should be recursed + * upon. + */ +public template InclusionStratergy(T) +{ + public alias InclusionStratergy = bool delegate(Graph!(T) item); +} + +/** + * This is called on a graph node + * as part of the first action + * that takes place during the + * visitation of said node during + * a dfs operation. + */ +public template TouchStratergy(T) +{ + public alias TouchStratergy = void delegate(Graph!(T) item); +} + +/** + * A graph of nodes. + * + * These nodes are comprised of + * two components. The first of + * which is their associated value + * of type `T`, then the second + * are their children nodes + * (if any). The latter are of + * type `Graph!(T)` and therefore + * when constructing one such node + * it can also be added as a child + * of another node, therefore + * allowing you to build your + * graph as you see fit. + * + * Some notable functionality, + * other than the obvious, + * is the pluggable dfs method + * which let's you perform + * a recursive search on + * the graph, parameterized + * by two stratergies. The first + * is the so-called `TouchStratergy` + * which specifies the function + * to be called on the current node + * when `dfs` is called on it - + * this is the first thing that + * is done. The other parameter + * is the `VisitationStratergy` + * which is a predicate that + * will be called BEFORE + * entering the dfs (recursing) + * of a candidate child node. + * With this things like trees + * can be built or rather + * _derived_ from a graph. + * This is infact what the visitation + * tree type does. + * + * See_Also: `VisitationTree` + */ +public class Graph(T) +{ + private T value; + private Graph!(T)[] children; + + /** + * Constructs a new graph with + * the given value to set + * + * Params: + * value = the value of + * this graph node + */ + this(T value) + { + this.value = value; + } + + /** + * Creates a new graph without + * associating any value with + * itself + */ + this() + { + + } + + /** + * Sets the graph node's + * associated value + * + * Params: + * value = the valye + */ + public void setValue(T value) + { + this.value = value; + } + + /** + * Obtains the value associated with + * this graph node + * + * Returns: the value `T` + */ + public T getValue() + { + return this.value; + } + + /** + * Appends another graph node + * to the array of children + * of this node's + * + * Params: + * node = the tree node + * to append + */ + public void appendNode(Graph!(T) node) + { + this.children ~= node; + } + + /** + * Removes a given graph node + * from th array of children + * of thie node's + * + * Params: + * node = the graph node to + * remove + * Returns: `true` if the node + * was found and then removed, + * otherwise `false` + */ + public bool removeNode(Graph!(T) node) + { + bool found = false; + size_t idx; + for(size_t i = 0; i < this.children.length; i++) + { + found = this.children[i] == node; + if(found) + { + idx = i; + break; + } + } + + if(found) + { + this.children = this.children.removeResize(idx); + return true; + } + + return false; + } + + /** + * Checks if the given type is + * that of a graph node + * + * Returns: `true` if so, `false` + * otherwise + */ + private static bool isGraphNodeType(E)() + { + return __traits(isSame, E, Graph!(T)); + } + + /** + * Checks if the given type is + * that of a graph node's value + * type + * + * Returns: `true` if so, `false` + * otherwise + */ + private static bool isGraphValueType(E)() + { + return __traits(isSame, E, T); + } + + /** + * Returns a slice of the requested + * type. This is either `Graph!(T)` + * or `T` itself, therefore returning + * an array of either + * + * Returns: an array of the requested + * type of children + */ + public E[] opSlice(E)() + if(isGraphNodeType!(E) || isGraphValueType!(E)) + { + // If the children as graph nodes is requested + static if(isGraphNodeType!(E)) + { + return this.children; + } + // If the children as values themselves is requested + else static if(isGraphValueType!(E)) + { + T[] slice; + foreach(Graph!(T) tnode; this.children) + { + slice ~= tnode.value; + } + return slice; + } + } + + /** + * Returns an array of all the childrens' + * associated values + * + * Returns: a `T[]` + */ + public T[] opSlice() + { + return opSlice!(T)(); + } + + /** + * Returns the element of the child + * at the given index. + * + * The type `E` can be specified + * as either `Graph!(T)` or `T` + * which will hence return a node + * from the children array at the + * given index of that type (either + * the child node or the child node's + * value). + * + * Params: + * idx = the index + * Returns: the type `E` + */ + public E opIndex(E)(size_t idx) + if(isGraphNodeType!(E) || isGraphValueType!(E)) + { + // If the child as a graph node is requested + static if(isGraphNodeType!(E)) + { + return this.children[idx]; + } + // If the child as a value itself is requested + else static if(isGraphValueType!(E)) + { + return this.children[idx].value; + } + } + + /** + * Returns the value of + * the child node at + * the provided index + * + * Params: + * idx = the index + * Returns: the value + */ + public T opIndex(size_t idx) + { + return opIndex!(T)(idx); + } + + /** + * Returns the number + * of children attached + * to this node + * + * Returns: the count + */ + @property + public size_t length() + { + return this.children.length; + } + + /** + * Returns the number + * of children attached + * to this node + * + * Returns: the count + */ + public size_t opDollar() + { + return this.length; + } + + /** + * Performs a depth first search + * on the graph by firstly calling + * the `TouchStratergy` on the current + * node and then iterating over all + * of its children and only recursing + * on each of them if the `InclusionStratergy` + * allows it. + * + * The touch stratergy is called + * as part of the first line of code + * in the call to the dfs on a + * given graph node. + * + * Note that is you don't have a good + * inclusion stratergy and touch startergy + * then you may have a stack overflow + * occur if your graph has cycles + * + * Params: + * strat = the `InclusionStratergy` + * touch = the `TouchStratergy` + * Returns: a `T[]` + */ + public T[] dfs + ( + InclusionStratergy!(T) strat = toDelegate(&Always!(T)), + TouchStratergy!(T) touch = toDelegate(&Nothing!(T)) + ) + { + version(unittest) + { + writeln("dfs entry: ", this); + } + + T[] collected; + scope(exit) + { + version(unittest) + { + writeln("leaving node ", this, " with collected ", collected); + } + } + + // Touch + touch(this); // root[x] + + foreach(Graph!(T) child; this.children) // subtree[x], + { + if(strat(child)) + { + version(unittest) + { + writeln("dfs, strat good for child: ", child); + } + + // Visit + collected ~= child.dfs(strat, touch); + } + else + { + version(unittest) + { + writeln("dfs, strat ignored for child: ", child); + } + } + } + + // "Visit" + collected ~= this.value; + + + return collected; + } + + /** + * Returns a string representation + * of this node and its value + * + * Returns: a `string` + */ + public override string toString() + { + return format("GraphNode [val: %s]", this.value); + } +} + +/** + * Test out usage of the `Graph!(T)` + */ +unittest +{ + Graph!(string) treeOfStrings = new Graph!(string)("Top"); + + Graph!(string) subtree_1 = new Graph!(string)("1"); + Graph!(string) subtree_2 = new Graph!(string)("2"); + Graph!(string) subtree_3 = new Graph!(string)("3"); + + treeOfStrings.appendNode(subtree_1); + treeOfStrings.appendNode(subtree_2); + treeOfStrings.appendNode(subtree_3); + + assert(treeOfStrings.opIndex!(Graph!(string))(0) == subtree_1); + assert(treeOfStrings.opIndex!(Graph!(string))(1) == subtree_2); + assert(treeOfStrings.opIndex!(Graph!(string))(2) == subtree_3); + + assert(treeOfStrings[0] == subtree_1.getValue()); + assert(treeOfStrings[1] == subtree_2.getValue()); + assert(treeOfStrings[2] == subtree_3.getValue()); + + assert(treeOfStrings.opDollar() == 3); + + InclusionStratergy!(string) strat = toDelegate(&Always!(string)); + TouchStratergy!(string) touch = toDelegate(&DebugTouch!(string)); + + string[] result = treeOfStrings.dfs(strat, touch); + writeln("dfs: ", result); + + assert(result[0] == "1"); + assert(result[1] == "2"); + assert(result[2] == "3"); + assert(result[3] == "Top"); + + + auto i = treeOfStrings.opSlice!(Graph!(string))(); + writeln("Siblings: ", i); + assert(i[0] == subtree_1); + assert(i[1] == subtree_2); + assert(i[2] == subtree_3); + + auto p = treeOfStrings.opSlice!(string)(); + writeln("Siblings (vals): ", p); + assert(p == treeOfStrings[]); + + + assert(treeOfStrings.removeNode(subtree_1)); + assert(!treeOfStrings.removeNode(subtree_1)); +} + +/** + * A kind-of a graph which has the ability + * to linearize all of its nodes which + * results in performing a depth first + * search resulting in the collection of + * all nodes into a single array with + * elements on the left hand side being + * the most leafiest (and left-to-right + * on the same depth are in said order). + * + * It also marks a node as visited on + * entry to it via the dfs call to it. + * + * When dfs is performed, a child node + * is only recursed upon if it has not + * yet been visited. + * + * With all this, it means a graph of + * relations can be flattened into an + * array. + */ +public class VisitationTree(T) : Graph!(T) +{ + private bool visisted; + + /** + * Constructs a new node + * + * Params: + * value = the value + */ + this(T value) + { + super(value); + } + + /** + * Performs the linearization + * + * Returns: the linearized list + */ + public T[] linearize() + { + return dfs(toDelegate(&_shouldVisit), toDelegate(&_touch)); + } + + /** + * The inclusion startergy + * + * Params: + * tnode = the graph node + * Returns: `true` if not + * yet visited or incompatible + * node type + */ + private static bool _shouldVisit(Graph!(T) tnode) + { + VisitationTree!(T) vnode = cast(VisitationTree!(T))tnode; + return vnode && !vnode.isVisited(); + } + + /** + * The touching stratergy + * + * Only works on compatible + * graph nodes + * + * Params: + * tnode = the tree node + */ + private static void _touch(Graph!(T) tnode) + { + VisitationTree!(T) vnode = cast(VisitationTree!(T))tnode; + if(vnode) + { + vnode.mark(); + } + } + + /** + * Marks this node as + * visited + */ + private void mark() + { + this.visisted = true; + } + + /** + * Checks this node has been + * visited + * + * Returns: `true` if visited, + * otherwise `false` + */ + private bool isVisited() + { + return this.visisted; + } +} + +/** + * Tests out using the visitation tree + */ +unittest +{ + VisitationTree!(string) root = new VisitationTree!(string)("root"); + + VisitationTree!(string) thing = new VisitationTree!(string)("subtree"); + root.appendNode(thing); + thing.appendNode(root); + + string[] linearized = root.linearize(); + writeln(linearized); + + assert(linearized[0] == "subtree"); + assert(linearized[1] == "root"); } \ No newline at end of file