4
\$\begingroup\$

I recently create a function to combine some arrays of associative arrays in a single array in response to a stackoverflow question, where you can found more details about this.

The function works as expected, but I think their description is very obscure. In addition, i choose constant T_OBJECT_CAST as option to convert each rows as object: someone can suggest a better predefined constants?


Goal:

As requested from another user (edited):

I've these two arrays:

1) [0=>['title1'=>'Title 1'], 1=>['title2'=>'Title 2']];

2) [0=>['contn1'=>'Content 1'],1=>['contn2'=>'Content 2']];

How can I combine/merge them that they look like this?

[

0=>['title1'=>'Title 1','contn1'=>'Content 1'],

1=>['title2'=>'Title 2','contn2'=>'Content 2']

]


Explanation:

(Globish here, i'm sorry)

To allow not predetermined arguments, I don't format function as multiArrayCombine( $arg1, $arg2, ... ), I use instead the func_get_args() function, that "allow user-defined functions to accept variable-length argument lists".

First of all, I check if the last argument is the predefined constant T_OBJECT_CAST: if it is, I set $asObject to True, then I pop-it off the end of arguments array; now in the $args variable I have an array with each passed arrays.

Next step: I retrieve the max key value of all passed arrays; i choose this way instead of more comfortable foreach( $array1 as $row ) to avoid to omit values if one of the other arrays have more rows than the first. Eventually not numeric keys are omitted.

Then, the main loop: I process each row of originals arrays and I add their keys and values to row that will added to returned array. If there are duplicated keys, only the last is returned.

After processing each array, i add the obtained row (converted to object if this option is passed) to returning array.


The function:

/*   Groups passed arrays in an array of associative arrays with same keys and values
 *
 *   @example          $array1 = array( array('a'=>'val1'),array('a'=>'val2') );
 *                     $array2 = array( array('b'=>'val3'),array('b'=>'val4') );
 *                     $array3 = array( array('c'=>'val5'),array(),array('c'=>'val6') );
 *                     multiArrayCombine( $array1, $array2, $array3 );
 *                     return: array
 *                     (
 *                        0 => array('a'=>'val1','b'=>'val3','c'=>'val5'),
 *                        1 => array('a'=>'val2','b'=>'val4'),
 *                        2 => array('c'=>'val6')
 *                     )
 *                     
 *   @param   array    $array1[, $array2[, $array3...]]
 *
 *   @option  const    T_OBJECT_CAST cast returned assoc arrays as stdObject
 *
 *   @return  array
 */
function multiArrayCombine()
{
    /* Get all passed parameters and T_OBJECT_CAST option: */
    $args     = func_get_args();
    $asObject = ( T_OBJECT_CAST == $args[count($args)-1] );
    if( $asObject ) array_pop( $args );

    $retval = array();          # Init array to be returned

    /* Retrieve highest passed arrays key: */
    $max = 0;
    foreach( $args as $array ) $max = max( $max, max( array_keys($array) ) );

    /* Loop for each arrays key: */
    for( $i=0; $i<=$max; $i++ )
    {
        /* Init associative array to add:  */
        $add = array();

        /* Process actual key ($i) of each passed array:  */
        foreach( $args as $array )
        {
            /* If the key ($i) exists, add  each passed array:  */
            if( is_array($array[$i]) )
            {
                foreach( $array[$i] as $key => $val )
                { $add[$key] = $val; }
            }
        }

        /* Add the obtained associative array to return array */
        if( $asObject ) $retval[] = (object) $add;
        else            $retval[] = $add;
    }

    return $retval;
}
\$\endgroup\$
  • \$\begingroup\$ Please, also specify here some details, as link might brake in some point in the future. Also, it easier to understand the code, if the whole context is located on single page. \$\endgroup\$ – Alexei Jan 28 '16 at 13:56
  • \$\begingroup\$ @Alexei added details, I hope properly. \$\endgroup\$ – fusion3k Jan 28 '16 at 14:24
3
\$\begingroup\$

I know that the question is already answered with cFreed answer but I want show another approach.

While I was finding if there are some function that do that I found this on php.net

function array_merge_recursive_distinct ( array &$array1, array &$array2 )
{
  $merged = $array1;

  foreach ( $array2 as $key => &$value )
  {
    if ( is_array ( $value ) && isset ( $merged [$key] ) && is_array ( $merged [$key] ) )
    {
      $merged [$key] = array_merge_recursive_distinct ( $merged [$key], $value );
    }
    else
    {
      $merged [$key] = $value;
    }
  }

  return $merged;
}

basically is the same that you want but only for two args so... I tried to do this pick args and send to that function in pairs

so, I write this. I tried to keep the soul of your code intact so you will see $asObject = (T_OBJECT_CAST == $args[count($args)-1]); and also if($asObject) array_pop($args);

function array_merge_recursive_unique_keys(){
    $args     = func_get_args();
    $asObject = (T_OBJECT_CAST == $args[count($args)-1]);
    if($asObject) array_pop($args);

    $ret = $args;

    if(count($args)>0){
        $partial = current($args);

        for($i = 1; $i< count($args); $i++){
            if(is_array($args[$i]) && is_array($partial)){
                $partial = array_merge_recursive_distinct($partial, $args[$i]);
            }           
        }

        if($asObject) {
            $ret = json_decode (json_encode ($partial), FALSE);
        }else{
            $ret = $partial;
        }

    }

    return $ret;
}
\$\endgroup\$
  • \$\begingroup\$ Well done! Thanks for interesting in my question. \$\endgroup\$ – fusion3k Feb 2 '16 at 18:50
3
\$\begingroup\$

I am going to use some clever and long-standing yet somewhat crytic techniques in my answer. If you are barefoot right now, don't bother to put socks on because they may come off while you read my suggested function re-write.

Your custom function means to:

  1. Pack together the incoming arrays,
  2. "Transpose" the subarrays' data (pivot the data),
  3. Filter out the empty placeholding subarrays,
  4. Flatten the data structure into an array of associative arrays or objects (conditionally)

Before I get to the slight-of-hand array function calls, I want to express that I do not support the passing of an optional T_OBJECT_CAST comparison value with the arrays to be processed. I feel that it is too disruptive to the otherwise direct purpose of the custom function. To remedy this, I recommend conditionally calling json_decode(json_encode()) on the return value after it is returned to the global scope. There are other workarounds, but this one seems cleanest to me.

Code (Demo)

function multiArrayCombine() {
    $transposed = array_map(null, ...func_get_args());
    foreach ($transposed as &$set) {
        $set = array_merge(...array_filter($set));
    }
    return $transposed;
}
$array1 = [['a' => 'val1'], ['a' => 'val2']];
$array2 = [['b' => 'val3'], ['b' => 'val4']];
$array3 = [['c' => 'val5'], [], ['c' => 'val6']];
var_export(multiArrayCombine($array1, $array2, $array3));

Output:

array (
  0 => 
  array (
    'a' => 'val1',
    'b' => 'val3',
    'c' => 'val5',
  ),
  1 => 
  array (
    'a' => 'val2',
    'b' => 'val4',
  ),
  2 => 
  array (
    'c' => 'val6',
  ),
)

Using array_map(null, ...func_get_args()); effectively unpacks the incoming arrays via the splat operator then transposes the data using array_map() with a null callback parameter. While this is far more concise than using loops, it is important to mention that this produces a complete matrix by generating null elements (which is not the case when using loop). This is little bother in your scenario because your incoming arrays contain empty placeholding arrays to be removed as well.

Calling array_filter() to remove nulls and empty arrays is swiftly done by its default behavior. However, this makes any other legitimate zero/false/null/empty values vulnerable to unintended removal. You will need to determine if this behavior is a threat to your project.

To flatten the transposed and filtered data into the desired structure, array_merge(...$transpose)) will strip the outer array level.

\$\endgroup\$
2
\$\begingroup\$

Problem description

but I think their description is very obscure

I must confess that I agree with that :)

First I couldn't at all understand the @option argument usage, especially as you drop it if present.

Also you've precised that "Eventually not numeric keys are omitted", but I can't see anything so in your code.

Code improvement

You can simplify this statement:

$asObject = ( T_OBJECT_CAST == $args[count($args)-1] );

using one of the old (and somewhat forgotten :) array functions:

$asObject = ( T_OBJECT_CAST == end($args) );

Another pont is that it lacks control in this statement:

if( isset($array[$i]) )

so when processing an empty subarray (like in $array3 in your example), it fires a Notice "Undefined index" error. To avoid it you shoud write:

if( isset($array[$i]) AND is_array($array[$i]) )

Strategy

For the processing method itself I suggest an alternative way, again using old array functions (current() and next()), avoiding to look for the arrays max length.
Looks like this:

function multiArrayCombine() {
  $arrays = func_get_args();
  do {
    $newSub = [];
    foreach ($arrays as &$array) {
      while (!is_numeric(key($array))) {
        if (!next($array)) {
          break;
        }
      }
      if (($current = current($array)) != FALSE) {
        $newSub[key($current)] = reset($current);
      }
      next($array);
    }
    if ($newSub) {
      $newArray[] = $newSub;
    }
  } while ($newSub);
  return $newArray;
}

It takes advantage of the fact that current() iterates each array whatever its length, merely returning FALSE once end reached.

Note that:

  • in foreach ($arrays as &$array) we pass &$array by reference: if not, the passed $array would always be in its reset state.
  • in if (($current = current($array)) != FALSE) we don't test by !== (which would check only the end state) but != so at the same time checking for empty subarray too.
\$\endgroup\$
  • \$\begingroup\$ Thanks for yr detailed answer! No suggestion about better desc? About yr notes: the @opt is used to set $asObject, dropped to have only arrays in $args; in my code (not in your), not-numeric keys are omitted when I set max key val and then perform a for loop (only numbers); ok for the code improvement notes, very useful; concerning strategy, yr solution doesn't work as expected (a sort of it was my 1st try), because I have to maintain original key index values: in yr code that fails if a key that is in an array is omitted in another1 (eval.in/509788). Thanks 4 precious help! \$\endgroup\$ – fusion3k Jan 29 '16 at 13:15
  • \$\begingroup\$ @fusion3k "No suggestion about better desc?": something which exposes the requirements rather than explaining how your function meets them. Also you'd better dropping the @option process from the code, since the posted version doesn't include its usage. Regarding my code, sorry, I didn't pay enough attention to the returned structure: it's now corrected in my answer. The same applies to not-numeric keys. All that works fine with the example you provided. \$\endgroup\$ – cFreed Jan 29 '16 at 14:17
  • \$\begingroup\$ @fusion3k In the other hand I used you eval.in link and realized something that was totally "hidden" in your initial question: you want subarray keys to be used as criterions for the combination, while without this latter example the combination seemed to follow the sequence! So it totally changes the way to consider the problem... \$\endgroup\$ – cFreed Jan 29 '16 at 14:18
  • \$\begingroup\$ yes, you are right: the example provided at the beginning of question is ambiguous. Usually i don't put example in function description. In this case I do because unable to find a proper synthetic description! BTW, don't wast another time with this! Ultimately, i don't know if the function is really useful, even more than for the user that asked it! Thanks again for attention and for useful (and pretty) suggestions about code improvement. \$\endgroup\$ – fusion3k Jan 29 '16 at 14:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.