1

I've got a multidimensional array containing some id's, stored in keys called 'name'. Each entry can have other sub-arrays, containing other id's. The array is dynamic; the depth and entries are unknown. Here is an example:

Array
(
    [0] => Array
        (
            [name] => test1
            [subs] => Array
                (
                    [0] => Array
                        (
                            [name] => test2
                        )

                    [1] => Array
                        (
                            [name] => test3
                            [subs] => Array
                                   (
                                       [name] => test4
                                   )
                        )

                )

        )

    [1] => Array
        (
            [name] => test5
        )
)

Now I want to convert this multidimensional array to a 'flat' array, while keeping hold of the depth. The scope of the new array is some kind of table of contents, where the key represents a chapter and the value an id. For example, 'test4' should be chapter 1.2.1, 'test2' should be 1.1 and 'test5' should be chapter 2. Each level deeper means the entry is a child of the parent level. Therefore I have to store every previous depth-'level' while looping the array. So far I haven't found a way to do this.

QUESTION UPDATE:

I've got the first part working. Now I want to add new chapters to the array, and have the chapter numbers of the existing entries update themselves. The array now looks like this:

Array
(
    [1] => test1
    [1.1] => test2
    [1.2] => test3
    [1.2.1] => test4
    [2] => test5
)

So now I would like to add chapter 'test6' as first-child of 1.2, which means the current 1.2.1 would become 1.2.2 and the new child will be 1.2.1 instead.

PhearOfRayne
  • 4,990
  • 3
  • 31
  • 44
carlo
  • 700
  • 2
  • 9
  • 25
  • 2
    I dont see a notable difference to your question: http://stackoverflow.com/questions/7854940/php-walk-through-multidimensional-array-while-preserving-keys - i'd suggest the same possible duplicates again. – Gordon Oct 25 '11 at 13:18

3 Answers3

3

Code:

// Mmmm... functiony goodness
function array_to_toc ($in, &$out, $level = '') {
  if (!$level) $out = array(); // Make sure $out is an empty array at the beginning
  foreach ($in as $key => $item) { // Loop items
    $thisLevel = ($level) ? "$level.".($key + 1) : ($key + 1); // Get this level as string
    $out[$thisLevel] = $item['name']; // Add this item to $out
    if (isset($item['subs']) && is_array($item['subs']) && count($item['subs'])) array_to_toc($item['subs'],$out,$thisLevel); // Recurse children of this item
  }
}

// Here is your test data (slightly modified - I think you stated it wrong in the question)
$array = array (
  0 => array (
    'name' => 'test1',
    'subs' => array (
      0 => array (
        'name' => 'test2'
      ),
      1 => array (
        'name' => 'test3',
        'subs' => array (
          0 => array (
            'name' => 'test4'
          )
        )
      )
    )
  ),
  1 => array (
    'name' => 'test5'
  )
);

// $result is passed by reference and will hold the output after the function has run
$result = array();
array_to_toc($array, $result);

print_r($result);

Output:

Array
(
    [1] => test1
    [1.1] => test2
    [1.2] => test3
    [1.2.1] => test4
    [2] => test5
)

Demo

EDIT

These two (plus one supporting) functions allow you add and remove chapters from the input array by chapter reference. Then, you can recalculate the TOC from the new structure.

function chapter_exists ($array, $chapterId) {
  $chapterParts = explode('.',$chapterId);
  foreach ($chapterParts as &$chapter) $chapter--;
  $lastId = array_pop($chapterParts);
  return eval('return isset($array['.implode("]['subs'][",$chapterParts).((count($chapterParts)) ? "]['subs'][" : '')."$lastId]);");
}

function add_chapter (&$array, $chapterId, $item) {
  $chapterParts = explode('.',$chapterId);
  foreach ($chapterParts as &$chapter) $chapter--; // Decrement all the values
  $lastId = array_pop($chapterParts);
  if (count($chapterParts) && !chapter_exists($array, implode('.',$chapterParts))) return FALSE; // Return FALSE if the level above the chapter we are adding doesn't exist
  if (chapter_exists($array, $chapterId)) { // See if the chapter reference already exists
    eval('array_splice($array'.((count($chapterParts)) ? '['.implode("]['subs'][",$chapterParts)."]['subs']" : '').",$lastId,0,array(\$item));"); // Insert an item
  } else {
    eval('$array['.implode("]['subs'][",$chapterParts).((count($chapterParts)) ? "]['subs'][" : '')."$lastId] = \$item;"); // Insert an item
  }
  return TRUE;
}

function remove_chapter (&$array, $chapterId) {
  $chapterParts = explode('.',$chapterId);
  foreach ($chapterParts as &$chapter) $chapter--; // Decrement all the values
  $lastId = array_pop($chapterParts);
  return (chapter_exists($array, $chapterId)) ? eval('$removed = array_splice($array'.((count($chapterParts)) ? '['.implode("]['subs'][",$chapterParts)."]['subs']" : '').",$lastId,1); return array_shift(\$removed);") : FALSE;
}

The best way to demonstrate how they work is with an example. Say we start with the array structure above, which is held in a variable called $structure. As we know, our resulting TOC array looks like this:

Array
(
    [1] => test1
    [1.1] => test2
    [1.2] => test3
    [1.2.1] => test4
    [2] => test5
)

Now, we decide we want to remove chapter 1.2 and all it's sub-chapters - we can do this:

// Remove the chapter from $structure
remove_chapter($structure, '1.2');
// recalculate the TOC
array_to_toc($structure, $result2);

print_r($result2);
/*
  Outputs:
  Array
  (
      [1] => test1
      [1.1] => test2
      [2] => test5
  )
*/

Now lets say we want to add a chapter called test6 as chapter 1.1, and test2 will be re-indexed to 1.2 - we'll be working with the result of the above example for this one:

// Add the new chapter to $structure
add_chapter($structure, '1.1', array('name'=>'test6'));
// recalculate the TOC
array_to_toc($structure, $result3);

print_r($result3);
/*
  Outputs:
  Array
  (
      [1] => test1
      [1.1] => test6
      [1.2] => test2
      [2] => test5
  )
*/

OK, seems fairly simple. But what if we wanted to move a sub-chapter, so it was at the top level of the tree? Let's go back to our original version of $structure to demonstrate this - we'll move chapter 1.2, so that it is now chapter 3:

/*
  A quick reminder of what we are starting with:
  Array
  (
      [1] => test1
      [1.1] => test2
      [1.2] => test3
      [1.2.1] => test4
      [2] => test5
  )
*/

// Remove the chapter from $structure - this time, we'll catch the items we remove in a variable
$removed = remove_chapter($structure, '1.2');
// Add it again, only this time as chapter 3
add_chapter($structure, '3', $removed);

// recalculate the TOC
array_to_toc($structure, $result4);

print_r($result4);
/*
  Outputs:
  Array
  (
      [1] => test1
      [1.1] => test2
      [2] => test5
      [3] => test3
      [3.1] => test4
  )
*/

Hopefully I've explained it well enough there.

chapter_exists() returns a boolean. Fairly self explanatory as to what it means, if feel. Pass the $structure array as the first parameter, and the chapter ID you want to check as the second. This function is required, as it is used by the other two internally.

add_chapter() returns a boolean, so you can test whether the operation was successful. It will fail if the parent of the chapter doesn't exist - for example, if you try to add 1.2.1 when 1.2 hasn't been defined, it won't work. If you add a chapter that already exists, all the chapter numbers at that level will be shifted up by 1.

remove_chapter() will return the item that was removed on success (i.e. an array) or boolean FALSE on failure - it will fail if you try and remove a chapter that doesn't exist.

NB: I had to make heavy use of eval() for this, in order to accommodate for arbitrary level depth. I hate to use it, but I couldn't think of any other way - if anyone reading this has any bright ideas about alternative approaches (preferably that don't involve some nightmarish looping structure), please let me know...

DaveRandom
  • 87,921
  • 11
  • 154
  • 174
  • Thanks this works for me. Now I'm wondering how to add new chapters while updating the numbers of the existing ones. Please check my update above. – carlo Oct 26 '11 at 09:59
  • Can't you just supply a modified input array to the above function, to rebuild the TOC from scratch? It would be *alot* easier than trying to modify the `$result` array directly... – DaveRandom Oct 26 '11 at 10:08
  • That would be fine as well, but how should I insert new entries in that input array? Sorry, I have little affinity with multidimensional arrays... – carlo Oct 26 '11 at 10:11
  • Slight modification of the above function, so you don't have to declare `$result` as an array before the function (i.e. it still works the same, but you don't need the `$result = array();` line before you call it). – DaveRandom Oct 26 '11 at 10:11
  • Given the above example (that you want to add a new child as the *first* child of `1.2`), you would do this: `array_unshift($originalArray[0]['subs'][1]['subs'], $newItem);`. It can get quite confusing this, the functions that will help you are [`array_unshift()`](http://uk.php.net/manual/en/function.array-unshift.php) (insert at the beginning), [`array_push()`](http://uk.php.net/manual/en/function.array-push.php) (insert at the end) and [`array_splice()`](http://uk.php.net/manual/en/function.array-splice.php) (insert in the middle) – DaveRandom Oct 26 '11 at 10:19
  • All those functions will re-index the array - so your chapter numbers will be sorted for you automatically – DaveRandom Oct 26 '11 at 10:21
  • Keep in mind that you need to subtract 1 from each section number - which is why `1.2` is referenced as `[0]['subs'][1]['subs']` – DaveRandom Oct 26 '11 at 10:31
  • I'm not getting much further :( I assume the $thisLevel variable is no longer needed? I succeed in adding the new entry to a subs array of an existing one, however on the wrong place.. Could you please give me another hint in the right direction? – carlo Oct 26 '11 at 10:58
  • You don't want to modify the function, just the input array - `$thisLevel` is definitely required in the function in order to maintain the tree position as it iterates through it! Give me 10 minutes, I'll write some functions to allow you to modify the input array more easily. – DaveRandom Oct 26 '11 at 11:00
  • But why should the input array be modified? I assume the input array should be passed to the function as a whole in order to insert a new entry in it? Otherwhise, how can the function return a whole array including the new entries? – carlo Oct 26 '11 at 11:37
0
function array_flat($array, $prefix = '') {
$result = array();

foreach ($array as $key => $value) {
    $new_key = $prefix . (empty($prefix) ? '' : '.') . $key;

    if (is_array($value)) {
        $result = array_merge($result, array_flat($value, $new_key));
    } else {
        $result[$new_key] = $value;
    }
}

return $result;
}
Tom
  • 761
  • 3
  • 15
  • 33
0
function toc(array $data, array $level = array()) {
    $toc = array();

    foreach ($data as $i => $node) {
        $currentLevel = array_merge($level, array($i + 1));
        $toc[] = join('.', $currentLevel) . ': ' . $node['name'];
        if (!empty($node['subs'])) {
            $toc = array_merge($toc, toc($node['subs'], $currentLevel));
        }
    }

    return $toc;
}

echo join("\n", toc($array));
deceze
  • 510,633
  • 85
  • 743
  • 889