nshew13
nshew13

Reputation: 3097

simple way in PHP to split path string into breadth-first array

Using PHP 5.2, I'm trying to parse an arbitrary number of path/directory strings into an array, such that they can be processed breadth-first. This is to allow me to script a sparse checkout from a Subversion repository, telescoping the indicated paths. All the paths on the same level have to be specified in the same svn update --depth empty statement.

I get the desired output, but I wonder if there's a cleaner way to do this. (And, yes, I know there are changes needed for efficiency.)

EDIT I modified the original post to handle cases of multiple children in the same parent. My revised code is

$main = array(
    'a/b/c1/',
    'a/b/c2/d/e1',
    'a/b/c2/d/e2',
    'A/B/',
    'alpha/beta/gamma/delta/epsilon'
);

$splits = array();
$max = 0;
for ($i=0; $i<count($main); $i++) {
    $splits[$i] = explode(DIRECTORY_SEPARATOR, trim($main[$i], DIRECTORY_SEPARATOR));
    if (count($splits[$i]) > $max) {
        $max = count($splits[$i]);
    }
}

for ($i=0; $i<$max; $i++) {
    $levels[$i] = array();

    for ($path=0; $path<count($splits); $path++) {
        if (array_key_exists($i, $splits[$path])) {
            $levels[$i][] = implode(DIRECTORY_SEPARATOR, array_slice($splits[$path], 0, $i+1));
        }
    }
    $levels[$i] = array_unique($levels[$i]);
    sort($levels[$i]);  // just to reset indices
}

This changes my output structure to the following, which both provides unique directories at each level and retains sibling nodes.

Array
(
    [0] => Array
        (
            [0] => A
            [1] => a
            [2] => alpha
        )

    [1] => Array
        (
            [0] => A/B
            [1] => a/b
            [2] => alpha/beta
        )

    [2] => Array
        (
            [0] => a/b/c1
            [1] => a/b/c2
            [2] => alpha/beta/gamma
        )

    [3] => Array
        (
            [0] => a/b/c2/d
            [1] => alpha/beta/gamma/delta
        )

    [4] => Array
        (
            [0] => a/b/c2/d/e1
            [1] => a/b/c2/d/e2
            [2] => alpha/beta/gamma/delta/epsilon
        )

)

In my code, I then iterate over the final $levels array. Unfortunately, this still requires two iterations: one for depth empty and one for depth infinity, but I'm sure that could be worked out.

$count = count($levels);
for ($i=0; $i<$count; $i++) {
    echo '<p>', 'svn update --set-depth empty ', implode(' ', $levels[$i]), "</p>\n";
}

$count = count($main);
for ($i=0; $i<$count; $i++) {
    echo '<p>', 'svn update --set-depth infinity ', $main[$i], "</p>\n";
}

Upvotes: 1

Views: 544

Answers (2)

Quentin Pradet
Quentin Pradet

Reputation: 4771

Here's my implementation (it got easier with your latest request):

$levels = array();

for ($i=0; $i<count($main); $i++) {
        $splits = explode(DIRECTORY_SEPARATOR, trim($main[$i], DIRECTORY_SEPARATOR));
        $current = array();
        /* Load every subpath in an array*/
        for($j=0; $j<count($splits); $j++) {
                $current[$j . "hack"] = implode("/", array_slice($splits, 0, $j+1));
        }
        $levels = array_merge_recursive($levels, $current);
}

/* Removes duplicates and resets indices */
array_walk($levels, function(&$l, $i) { $l = array_unique($l); sort($l); });

What makes this implementation simple is that I handle each path separately. I only have one loop, and join the results with array_merge_recursive. For example, with "a/b" and "a/c", my code :

  1. creates array(0 => array("a"), 1 => array("a/b")) and array(0 => array("a"), 1 => array("a/c"))
  2. joins them with array_merge_recursive which gives array(0 => array("a", "a"), 1 => array("a/b", "a/c"))
  3. removes unique values with array_unique
  4. resets the indices with sort

Note: I need to use $j + "hack", otherwise array_merge_recursive won't merge the values as expected (try it for yourself).

Upvotes: 0

Eugen Rieck
Eugen Rieck

Reputation: 65304

$levels=array();
$depth=0;
$i=0;
foreach ($main as $m) {
  $m=explode('/',$m);
  while (sizeof($m)<$depth) $m[]=null;
  $d=0;
  foreach ($m as $mm) {
    if ($d>$depth) {
      if (!$mm) break;
      $depth=$d;
      $levels[$d]=array();
      for ($j=0;$j<=$i;$j++) $levels[$d][$j]=null;
    }
    $levels[$d][$i]=$mm;
    $d++;
  }
  $i++;
}

looks like a good alternative with only one traversion of the array. In short you don't use one pass to decide on the depth, but if you encounter a deeper entry, you just fill the relevant places in the array retroactively with nulls.

$depth has the depth-1 after the loop.

Edit:

This does yet handle a case of multiple children in the same parent, but I am unsure if it does so the way you want

Upvotes: 2

Related Questions