Viewed   103 times

I've had this problem bending my mind for a while now (head cold doesn't help either!), basically I have a PHP array which looks like this example:

$array[0][0] = 'apples';
$array[0][1] = 'pears';
$array[0][2] = 'oranges';

$array[1][0] = 'steve';
$array[1][1] = 'bob';

And I would like to be able to produce from this a table with every possible combination of these, but without repeating any combinations (regardless of their position), so for example this would output

Array 0            Array 1
apples             steve
apples             bob
pears              steve
pears              bob

But I would like for this to be able to work with as many different arrays as possible.

 Answers

4

this is called "cartesian product", php man page on arrays http://php.net/manual/en/ref.array.php shows some implementations (in comments).

and here's yet another one:

function array_cartesian() {
    $_ = func_get_args();
    if(count($_) == 0)
        return array(array());
    $a = array_shift($_);
    $c = call_user_func_array(__FUNCTION__, $_);
    $r = array();
    foreach($a as $v)
        foreach($c as $p)
            $r[] = array_merge(array($v), $p);
    return $r;
}

$cross = array_cartesian(
    array('apples', 'pears',  'oranges'),
    array('steve', 'bob')
);

print_r($cross);
Wednesday, December 21, 2022
2

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...

Saturday, August 27, 2022
 
5

Without making use of loops.

<?php
    $myArrayInit = [1 => 'red', 30 => 'orange', 25 => 'velvet', 45 => 'pink']; //<-- Your actual array
    $offsetKey = 25; //<--- The offset you need to grab

    //Lets do the code....
    $n = array_keys($myArrayInit); //<---- Grab all the keys of your actual array and put in another array
    $count = array_search($offsetKey, $n); //<--- Returns the position of the offset from this array using search
    $new_arr = array_slice($myArrayInit, 0, $count + 1, true);//<--- Slice it with the 0 index as start and position+1 as the length parameter.
    print_r($new_arr);

Output :

Array
(
    [1] => red
    [30] => orange
    [25] => velvet
)
Friday, November 18, 2022
 
1

Use a recursive function:

// Note this method returns a boolean and not the array
function recur_ksort(&$array) {
   foreach ($array as &$value) {
      if (is_array($value)) recur_ksort($value);
   }
   return ksort($array);
}
Thursday, November 10, 2022
 
2

Edited

  1. For completeness, I'm adding here the OP's code:

    def partition0(max_range, S):
        K = len(max_range)
        return np.array([i for i in itertools.product(*(range(i+1) for i in max_range)) if sum(i)<=S])
    
  2. The first approach is pure np.indices. It's fast for small input but consumes a lot of memory (OP already pointed out it's not what he meant).

    def partition1(max_range, S):
        max_range = np.asarray(max_range, dtype = int)
        a = np.indices(max_range + 1)
        b = a.sum(axis = 0) <= S
        return (a[:,b].T)
    
  3. Recurrent approach seems to be much better than those above:

    def partition2(max_range, max_sum):
        max_range = np.asarray(max_range, dtype = int).ravel()        
        if(max_range.size == 1):
            return np.arange(min(max_range[0],max_sum) + 1, dtype = int).reshape(-1,1)
        P = partition2(max_range[1:], max_sum)
        # S[i] is the largest summand we can place in front of P[i]            
        S = np.minimum(max_sum - P.sum(axis = 1), max_range[0])
        offset, sz = 0, S.size
        out = np.empty(shape = (sz + S.sum(), P.shape[1]+1), dtype = int)
        out[:sz,0] = 0
        out[:sz,1:] = P
        for i in range(1, max_range[0]+1):
            ind, = np.nonzero(S)
            offset, sz = offset + sz, ind.size
            out[offset:offset+sz, 0] = i
            out[offset:offset+sz, 1:] = P[ind]
            S[ind] -= 1
        return out
    
  4. After a short thought, I was able to take it a bit further. If we know in advance the number of possible partitions, we can allocate enough memory at once. (It's somewhat similar to cartesian in an already linked thread.)

    First, we need a function which counts partitions.

    def number_of_partitions(max_range, max_sum):
        '''
        Returns an array arr of the same shape as max_range, where
        arr[j] = number of admissible partitions for 
                 j summands bounded by max_range[j:] and with sum <= max_sum
        '''
        M = max_sum + 1
        N = len(max_range) 
        arr = np.zeros(shape=(M,N), dtype = int)    
        arr[:,-1] = np.where(np.arange(M) <= min(max_range[-1], max_sum), 1, 0)
        for i in range(N-2,-1,-1):
            for j in range(max_range[i]+1):
                arr[j:,i] += arr[:M-j,i+1] 
        return arr.sum(axis = 0)
    

    The main function:

    def partition3(max_range, max_sum, out = None, n_part = None):
        if out is None:
            max_range = np.asarray(max_range, dtype = int).ravel()
            n_part = number_of_partitions(max_range, max_sum)
            out = np.zeros(shape = (n_part[0], max_range.size), dtype = int)
    
        if(max_range.size == 1):
            out[:] = np.arange(min(max_range[0],max_sum) + 1, dtype = int).reshape(-1,1)
            return out
    
        P = partition3(max_range[1:], max_sum, out=out[:n_part[1],1:], n_part = n_part[1:])        
        # P is now a useful reference
    
        S = np.minimum(max_sum - P.sum(axis = 1), max_range[0])
        offset, sz  = 0, S.size
        out[:sz,0] = 0
        for i in range(1, max_range[0]+1):
            ind, = np.nonzero(S)
            offset, sz = offset + sz, ind.size
            out[offset:offset+sz, 0] = i
            out[offset:offset+sz, 1:] = P[ind]
            S[ind] -= 1
        return out
    
  5. Some tests:

    max_range = [3, 4, 6, 3, 4, 6, 3, 4, 6]
    for f in [partition0, partition1, partition2, partition3]:
        print(f.__name__ + ':')
        for max_sum in [5, 15, 25]:
            print('Sum %2d: ' % max_sum, end = '')
            %timeit f(max_range, max_sum)
        print()
    
    partition0:
    Sum  5: 1 loops, best of 3: 859 ms per loop
    Sum 15: 1 loops, best of 3: 1.39 s per loop
    Sum 25: 1 loops, best of 3: 3.18 s per loop
    
    partition1:
    Sum  5: 10 loops, best of 3: 176 ms per loop
    Sum 15: 1 loops, best of 3: 224 ms per loop
    Sum 25: 1 loops, best of 3: 403 ms per loop
    
    partition2:
    Sum  5: 1000 loops, best of 3: 809 µs per loop
    Sum 15: 10 loops, best of 3: 62.5 ms per loop
    Sum 25: 1 loops, best of 3: 262 ms per loop
    
    partition3:
    Sum  5: 1000 loops, best of 3: 853 µs per loop
    Sum 15: 10 loops, best of 3: 59.1 ms per loop
    Sum 25: 1 loops, best of 3: 249 ms per loop
    

    And something larger:

    %timeit partition0([3,6] * 5, 20)
    1 loops, best of 3: 11.9 s per loop
    
    %timeit partition1([3,6] * 5, 20)
    The slowest run took 12.68 times longer than the fastest. This could mean that an intermediate result is being cached 
    1 loops, best of 3: 2.33 s per loop
    # MemoryError in another test
    
    %timeit partition2([3,6] * 5, 20)
    1 loops, best of 3: 877 ms per loop
    
    %timeit partition3([3,6] * 5, 20)
    1 loops, best of 3: 739 ms per loop
    
Friday, December 23, 2022
 
kraf
 
Only authorized users can answer the search term. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :