0

I have the following method which should iterate through a tree while invoking actions on each node plus once every time it steps up or down the hierarchy. The logic itself works flawless, but I realized that my unit test fails because the if-statement which checks for already invoked elements fails to do what it is logical meant to do.

public static void IterateWithHierarchyFeedback(TBase start,
    Func<TBase, IEnumerable<TBase>> childSelector,
    Action<TBase> invoker,
    Action stepUpInvocator,
    Action stepDownInvocator,
    bool invokeOnStart)
{
    Stack<DynamicIterator> iterationStack = new Stack<DynamicIterator>();
    iterationStack.Push(new DynamicIterator(start, childSelector(start).GetEnumerator()));

    while (iterationStack.Count > 0)
    {
        var current = iterationStack.Pop();
        // HERE it fails because current.Invoked = true but invoker() is still executed
        if (!current.Invoked && invokeOnStart || current.Current != start)
            invoker(current.Current);

        if (current.Enumerator == null || !current.Enumerator.MoveNext())
        {
            if (current.Current != start)
                stepUpInvocator();
            continue;
        }

        stepDownInvocator();

        current.Invoked = true;
        iterationStack.Push(current);
        iterationStack.Push(new DynamicIterator(current.Enumerator.Current,
            childSelector(current.Enumerator.Current)?.GetEnumerator()));
        continue;
    }
}

This is my unit test:

[Test] public async Task TestIterateWithFeedback() { StringBuilder b = new StringBuilder();

DynamicIterate<Tree>.Downwards.IterateWithHierarchyFeedback(_tree, t => t.Children,
    tree => b.Append(tree.ReturnValue.ToString()),
    () => b.Append('<'),
    () => b.Append('>'),
    true);

Assert.Warn(b.ToString());
const string expected = "1>2>3>4<>5<<>6<<>7>>";
Assert.AreEqual(expected, b.ToString());

}

And here you see that the output is not what it should be. That's because invoker() is called on elements that were already invoked, otherwise the output would be correct when it comes to order:

  2)   Expected string length 20 but was 23. Strings differ at index 8.
  Expected: "1>2>3>4<>5<<>6<<>7>>"
  But was:  "1>2>3>4<3>5<3<2>6<2<>7<"

Can anyone explain to me why this happens? I found this but even without debugger it occurs. I also tried to change my state object (DynamicIterator) from struct to class (as I initially thought this might be an issue with the async variation. But that's not changing anything either.

SharpShade
  • 1,761
  • 2
  • 32
  • 45
  • 4
    Have you considered that `current.Current != start` may be true, and therefore because of the OR operation in your condition, `invoker()` will be called. Your condition is effectively `((!current.Invoked && invokeOnStart) || current.Current != start)` (note my added parenthesis). – Matthew Watson Oct 23 '20 at 11:42
  • Use Debug.writeline() to see the content of "current.Current" and "start" on the "output" window. As stated in the above comment, "current" maybe different from "start" thus logical statement is calculated to be true. – AntiqTech Oct 23 '20 at 11:50
  • I figured that when the first statement `!current.Invoked` is false that the rest automatically isn't evaluated but I probably thought wrong about it with the OR. Your expression is a bit wrong though, this is what works `if (!current.Invoked && (invokeOnStart || current.Current != start))`. Thanks though! – SharpShade Oct 23 '20 at 12:38
  • @SharpShade I was showing you what your code was doing rather than what it was supposed to do. :) – Matthew Watson Oct 23 '20 at 12:46
  • Yes I know and I appreciate that. Solved my issue :) – SharpShade Oct 23 '20 at 13:09

1 Answers1

0

I'm not sure that the problem is, but the iteration code looks more complicated than needed. I would propose something like this:

    public static IEnumerable<(T Node, int Level)> DepthFirstWithlevel<T>(
           T self, 
           Func<T, IEnumerable<T>> selector)
    {
        var stack = new Stack<(T Node, int Level)>();
        stack.Push((self, 0));
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;
            foreach (var child in selector(current.Node))
            {
                stack.Push((child, current.Level + 1));
            }
        }
    }

This would return each node in the tree, together with the level in the tree the node has, with 0 being the root. If you specifically need methods to be called for each level change, you can make that with a separate method, something like this:

    public static void IterateWithHierarchyFeedback<T>(
        T self, 
        Func<T, IEnumerable<T>> selector, 
        Action<T> invoker, 
        Action stepUp, 
        Action stepDown)
    {
        int currentLevel = 0;
        foreach (var (node, level) in DepthFirstWithLevel(self, selector))
        {
            while (currentLevel < level)
            {
                currentLevel++;
                stepDown();
            }

            while (currentLevel > level)
            {
                currentLevel--;
                stepUp();
            }

            invoker(node);
        }
    }

For the tree

A - B - C
  |  ⌞ D
   ⌞E - F

It will print A>E>F<B>DC, i.e. It will traverse the bottom branches first (insert a .Reverse() after selector(current.Node) to change this). And it will go from the F to B directly, without revisiting A.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Thanks, although as pointed out in the comments of my question it was just a bad logic expression that caused the issue. This method should replace recursion by an iterative approach (depth-first) but as generic implementation for any hierarchic data type. I will compare though if your implementation might be better performance-wise. – SharpShade Oct 23 '20 at 14:57