Skip to content

[FEATURE REQUEST] _array_walk_recursive with path and reference processing #13630

Open
@gzhegow1991

Description

@gzhegow1991

Description

  1. RecursiveArrayIterator + RecursiveIteratorIterator still works without path/level collection
  2. flags SELF_FIRST/CHILD_FIRST works strange:
    2.1. SELF_FIRST returns "DEPTH_FIRST + as-is" (traverse using stack)
    2.2. CHILD_FIRST returns unpredictable values, guess it should returns [ child first, then all parents ], so:
    2.2.1. first return non-arrays
    2.2.2. then empty arrays
    2.2.3. then arrays.
    Instead it returns child-parent-child-parent-child-parent-parent-parent.
    At least it should return or:
    a) child-child-child-parent-parent-parent-parent
    b) child-parent-parent-child-parent-parent-child-parent
    Instead it returns unpredictable collapse of two expectations. Maybe because variant (b) requires repeating of parents after each child, and the iterator tries to output non-repeating just in time, and repeating - after all non-repeated is yielded.

To return non-arrays you have to first traverse arrays, so if you try to change order of yield you non-expectedly await until iterator finished the job for at least one branch. If tree is "unlimited", i mean [ buiding tree at runtime ] its possible to never reach any child and trap to unlimited cycle without any child received. Thats non-practical case, runtime error i guess, btw. And as the point, changing yield order is not an opportunity of traversal, but opportunity of sort results of that traversal.

Main benefit - receive path of the element, and use breadth-first to traverse parents before the children. If you process tree - you sometimes needed to traverse parents tree, but if you print tree - you most of times needed depth-traversal. Its not about child/self first stuff.

<?php
/**
 * array_walk_recursive, designed for:
 * - receive path to the element (and level of the tree using count() function)
 * - replace value or parent by reference (and possible retraverse that replacement) (use case: object-to-array then traverse children)
 * - traverse depth-first/breadth-first, т.е. (1 -> 1.1 -> 2 -> 2.1) vs (1 -> 2 -> 1.1 -> 2.1)
 * - include/exclude leaves/empty-arrays/parents to the output
 *
 * @param array $array
 * @param int     $flags
 *
 * @return Iterator<array, mixed>|Generator<array, mixed>
 */
define('_ARRAY_WALK_DEPTH_FIRST', 0b00000001);
define('_ARRAY_WALK_BREADTH_FIRST', 0b00000010);
define('_ARRAY_WALK_WITH_LEAVES', 0b00000100);
define('_ARRAY_WALK_WITHOUT_LEAVES', 0b00001000);
define('_ARRAY_WALK_WITH_EMPTY_ARRAYS', 0b00010000);
define('_ARRAY_WALK_WITHOUT_EMPTY_ARRAYS', 0b00100000);
define('_ARRAY_WALK_WITH_PARENTS', 0b01000000);
define('_ARRAY_WALK_WITHOUT_PARENTS', 0b10000000);
function &_array_walk_recursive(array &$array, int $flags = null) : \Generator
{
    if (! $array) return;

    $flags = $flags ?? 0;

    $isModeFirst = _ARRAY_WALK_DEPTH_FIRST | _ARRAY_WALK_BREADTH_FIRST;
    $isDepthFirst = (bool) ($flags & _ARRAY_WALK_DEPTH_FIRST);
    $isBreadthFirst = (bool) ($flags & _ARRAY_WALK_BREADTH_FIRST);
    $_both = $flags & $isModeFirst;
    if (($_both === 0) || ($_both === $isModeFirst)) {
        $isDepthFirst = true;
        $isBreadthFirst = false;
    }

    $isLeaves = _ARRAY_WALK_WITH_LEAVES | _ARRAY_WALK_WITHOUT_LEAVES;
    $withLeaves = (bool) ($flags & _ARRAY_WALK_WITH_LEAVES);
    $_both = $flags & $isLeaves;
    if (($_both === 0) || ($_both === $isLeaves)) {
        $withLeaves = true;
    }

    $isEmptyArrays = _ARRAY_WALK_WITH_EMPTY_ARRAYS | _ARRAY_WALK_WITHOUT_EMPTY_ARRAYS;
    $withEmptyArrays = (bool) ($flags & _ARRAY_WALK_WITH_EMPTY_ARRAYS);
    $_both = $flags & $isEmptyArrays;
    if (($_both === 0) || ($_both === $isEmptyArrays)) {
        $withEmptyArrays = false;
    }

    $isParents = _ARRAY_WALK_WITH_PARENTS | _ARRAY_WALK_WITHOUT_PARENTS;
    $withParents = (bool) ($flags & _ARRAY_WALK_WITH_PARENTS);
    $_both = $flags & $isParents;
    if (($_both === 0) || ($_both === $isParents)) {
        $withParents = false;
    }

    $queue = [];
    $stack = [];
    if ($isBreadthFirst) {
        $buffer =& $queue;
    } elseif ($isDepthFirst) {
        $buffer =& $stack;
    }

    // > src, path
    $buffer[] = [ &$array, [] ];

    $isRoot = true;
    while ( ! empty($buffer) ) {
        if ($isBreadthFirst) {
            $cur = array_shift($buffer);
        } elseif ($isDepthFirst) {
            $cur = array_pop($buffer);
        }

        $isEmpty = empty($cur[ 0 ]);
        $isArray = is_array($cur[ 0 ]);

        $isLeaf = ! $isArray;
        $isEmptyArray = $isArray || $isEmpty;
        $isParent = $isArray && ! $isEmpty;

        if (! $isRoot) {
            if (false
                || ($withLeaves && $isLeaf)
                || ($withEmptyArrays && $isEmptyArray)
                || ($withParents && $isParent)
            ) {
                yield $cur[ 1 ] => $cur[ 0 ];

                // > value could be changed by reference
                $isEmpty = empty($cur[ 0 ]);
                $isArray = is_array($cur[ 0 ]);

                $isParent = $isArray && ! $isEmpty;
            }
        }

        if ($isParent) {
            if ($isBreadthFirst) {
                reset($cur[ 0 ]);

            } elseif ($isDepthFirst) {
                end($cur[ 0 ]);
            }

            while ( null !== ($kk = key($cur[ 0 ])) ) {
                $fullpath = $cur[ 1 ];
                $fullpath[] = $kk;

                $buffer[] = [ &$cur[ 0 ][ $kk ], $fullpath ];

                if ($isBreadthFirst) {
                    next($cur[ 0 ]);

                } elseif ($isDepthFirst) {
                    prev($cur[ 0 ]);
                }
            }
        }

        $isRoot = false;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions