Explain how this array transposing and flattening function works - php

Here this function in PHP that allows to merge any N amount of different length arrays in a fashion that output array will be in next order: Array1[0],Array2[0],..,ArrayN[0],Array1[1],Array2[1],..,ArrayN[1]... :
function array_zip_merge() {
$output = array();
// The loop incrementer takes each array out of the loop as it gets emptied by array_shift().
for ($args = func_get_args(); count($args); $args = array_filter($args)) {
// &$arg allows array_shift() to change the original.
foreach ($args as &$arg) {
$output[] = array_shift($arg);
}
}
return $output;
}
// test
$a = range(1, 10);
$b = range('a', 'f');
$c = range('A', 'B');
echo implode('', array_zip_merge($a, $b, $c)); // prints 1aA2bB3c4d5e6f78910
While I understand what each of built in functions in this example do on their own, I just cant wrap my mind how it all works together in this function, despite included explaining commentaries...
Can someone break it down for me, please? The function works great as is, its just driving me crazy that I don't understand how it works...
P.S: this function is taken from Interleaving multiple arrays into a single array question.

The arrays $a, $b and $c have 10, 6 and 2 elements respectively.
$a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$b = ['a', 'b', 'c', 'd', 'e', 'f'];
$c = ['A', 'B'];
When you feed the arrays as arguments for the array_zip_merge() function, look at the for loop. The func_get_args() will set the $args with all the arguments supplied. On start of first for loop run,
$args = [$a, $b, $c];
count($args) = 3;
At the foreach loop the array_shift will return the first element of each array resulting the $output to be like
$output = [1, 'a', 'A'];
And the arrays now look like,
$a = [2, 3, 4, 5, 6, 7, 8, 9, 10];
$b = ['b', 'c', 'd', 'e', 'f'];
$c = ['B'];
At the end of the first for loop the array_filter function will test if any array is empty and remove it from $args. Same thing will happen at the second run, and by the end of the second for loop, the variables would look like
$a = [3, 4, 5, 6, 7, 8, 9, 10];
$b = ['c', 'd', 'e', 'f'];
$c = [];
$output = $output = [1, 'a', 'A', 2, 'b', 'B'];
//because $c is empty array_filter() removes it from $args
$args = [$a, $b];
So, on the third iteration of the for loop count($args) will return 2. When the last element of $b has been removed by array_shift the count($args) will return 1. The iteration will continue until all the arrays are empty

Inside array_zip_merge, the for statement always takes the first values of each array and add them to output variable respectively.
Because array_shift removes the element it returns, on every loop the first elements are different. When it gets empty because of it, the loop has nothing to do and breaks.
If you still dont understand, ask the specific part of the code you have trouble with please.

The custom function is effectively a transposing and flattening technique that will accommodate a non-matrix set of data. By non-matrix, I mean by pushing the three arrays into a single array, there will be columnar gaps in the data set. Using implode(array_merge(...array_map(null, $a, $b, $c))) will work by happenstance (because you are imploding the data with an empty string as glue, but in other cases, the generation of null elements will potentially skew the results.
I find the borrowed script to be very hard to interpret. The for loop would be much simpler as a foreach(). Instead of calling func_get_args(), the incoming columnar data could have been declared a variable using the spread operator in the function definition.
Here is one way to perform the 3-step task (without implode):
Transpose the data from the 3 arrays (isolate columns of data)
Filter null values out of the columnar data
Flatten the filtered two dimensional array (array_merge(...$tranposedAndfiltered))
Code: (Demo)
var_export(
array_merge(
...array_map(
fn(...$col) => array_filter(
$col,
fn($v) => !is_null($v)
),
$a,
$b,
$c
)
)
);
If it is simpler to perform the transposition without the spread operator and null filtering, just use nested foreach loops before flattening and imploding. (Demo)
$output = [];
foreach ([$a, $b, $c] as $i => $array) {
foreach ($array as $k => $v) {
$output[$k][$i] = $v;
}
}
var_export(implode(array_merge(...$output)));
See Stack Overflow's array transpose canonical: Transposing multidimensional arrays in PHP

Related

How would I identify how many arguments are numerals using variable length parameters in PHP

I am teaching myself php and I came across these variable length functions included in PHP. They are three being
func_get_args ()
func_num_args ()
func_get_arg ($i)
I understand how to use them so far, I would like to know how divide the arguments or arrays to know which ones are strings and which are numerical. for example
$data = funcArgSort('red', 'green', 21, 'blue', 67);
One you get the list of args you can use array_filter() to pull out the arguments that are int's and the ones that are strings.
You can use is_int() and is_string() respectively.
Then if you can merge them back into one array and sort before returning.
For example:
<?php
function funcArgSort() {
$args = func_get_args();
// this gets an array of numbers
$nums = array_filter($args, fn ($x) => is_int($x));
// this gets an array of strings
$strings = array_filter($args, fn ($x) => is_string($x));
// this merges the two arrays
$merged = [...$nums, ...$strings];
// Note that `sort` returns a boolean and mutates the array
// so we don't want to return the result of `sort`
sort($merged);
return $merged;
}
var_dump(funcArgSort('c', 3, 'b', 2, 'a', 1));
// output: ['a', 'b', 'c', 1, 2, 3]
Another alternative would be to use a loop to iterate through each argument and build up a list of numbers and strings. In this way, you only have to loop through the list of arguments once (which may or may not be a performance issue for your needs).
<?php
function funcArgSort() {
$args = func_get_args();
$nums = [];
$strings = [];
// Loop through each argument
foreach($args as $arg) {
// If it's a number, push it onto the nums array
if (is_int($arg)) $nums[] = $arg;
// If it's a string, push it onto the strings array
if (is_string($arg)) $strings[] = $arg;
}
$merged = [...$strings, ...$nums];
sort($merged);
return $merged;
}
var_dump(funcArgSort('c', 3, 'b', 2, 'a', 1));
// output: ['a', 'b', 'c', 1, 2, 3]
And another alternative, if your use-case is as contrived as the examples above you could just sort the args and it will work as such:
function funcArgSort() {
$args = func_get_args();
sort($args);
return $args;
}
var_dump(funcArgSort('c', 3, 'b', 2, 'a', 1));
// output: ['a', 'b', 'c', 1, 2, 3]

Check if one of multiple variables exists in array

$array = ['a', 'b', 'c', 'd'];
$vars = ['a', 'f', 'g'];
foreach ($vars as $var) {
if (in_array($var, $array)) {
return true;
} else {
return false;
}
}
How can i check if one of the $vars exists in $array? Only one of them needs to be true if all the others are false it is not a problem, But if there is more than 1 true value,
For example ['a', 'b', 'c', 'g'] i want the function to stop at the first true value and ends the process.
Simply with array_intersect function:
$arr = ['a', 'b', 'c', 'd'];
$vars = ['a', 'f', 'g'];
$result = count(array_intersect($vars, $arr)) == 1;
var_dump($result); // true
For me, the code you've got in the question is pretty much the best way to go about this. You just need to move the return false to after the loop, so that all values get processed:
foreach ($vars as $var) {
if (in_array($var, $array)) {
return true;
}
}
return false;
This will work identically to a solution with array_intersect, but has the advantage of only needing to process the minimum amount of the loop. Consider the following data:
$vars = [1, 2, 3, 4, ... 1,000,000,000];
$array = [1, 10, ...];
A solution using array_intersect will need check every element in $vars against every element in $array, whereas a solution that breaks out of a loop will only need to check until the first match. You could optimise this further by using two nested foreach loops if both your arrays are very large.
Like I mentioned in the comment - if your arrays are small then just use array_intersect, you won't notice any difference, and the code is a bit more readable.

how to get array_diff to ignore duplicates in php? [duplicate]

I am using array_diff() to take values out of array1 which are found in array2. The issue is it removes all occurrences from array1, as the PHP documentations makes note of. I want it to only take out one at a time.
$array1 = array();
$array1[] = 'a';
$array1[] = 'b';
$array1[] = 'a';
$array2 = array();
$array2[] = 'a';
It should return an array with one 'a' and one 'b', instead of an array with just 'b';
Just for the fun of it, something that just came to mind. Will work as long as your arrays contain strings:
$a = array('a','b','a','c');
$b = array('a');
$counts = array_count_values($b);
$a = array_filter($a, function($o) use (&$counts) {
return empty($counts[$o]) || !$counts[$o]--;
});
It has the advantage that it only walks over each of your arrays just once.
See it in action.
How it works:
First the frequencies of each element in the second array are counted. This gives us an arrays where keys are the elements that should be removed from $a and values are the number of times that each element should be removed.
Then array_filter is used to examine the elements of $a one by one and remove those that should be removed. The filter function uses empty to return true if there is no key equal to the item being examined or if the remaining removal count for that item has reached zero; empty's behavior fits the bill perfectly.
If neither of the above holds then we want to return false and decrement the removal count by one. Using false || !$counts[$o]-- is a trick in order to be terse: it decrements the count and always evaluates to false because we know that the count was greater than zero to begin with (if it were not, || would short-circuit after evaluating empty).
Write some function that removes elements from first array one by one, something like:
function array_diff_once($array1, $array2) {
foreach($array2 as $a) {
$pos = array_search($a, $array1);
if($pos !== false) {
unset($array1[$pos]);
}
}
return $array1;
}
$a = array('a', 'b', 'a', 'c', 'a', 'b');
$b = array('a', 'b', 'c');
print_r( array_diff_once($a, $b) );

Merge two arrays alternatively [duplicate]

This question already has answers here:
Merge two flat indexed arrays of equal size so that values are pushed into the result in an alternating fashion
(2 answers)
Closed 6 years ago.
What I want is an efficient (without looping) way to merge arrays in the way that the first element of the resulting array is the first element of the first array, the second element of the resulting array is the second element of the second array (alternatively)... etc
Example:
$arr1 = array(1, 3, 5);
$arr2 = array(2, 4, 6);
$resultingArray = array(1, 2, 3, 4, 5, 6);
assuming both arrays have the same length.
$arr1 = array(1, 3, 5);
$arr2 = array(2, 4, 6);
$new = array();
for ($i=0; $i<count($arr1); $i++) {
$new[] = $arr1[$i];
$new[] = $arr2[$i];
}
var_dump($new);
Not that I'd really advocate this "hack", but this'll do:
$result = array();
array_map(function ($a, $b) use (&$result) { array_push($result, $a, $b); }, $arr1, $arr2);
It really just hides a double loop behind array_map, so, meh...

Convert a numeric array of indexes to an ordered array of elements from an associative array of objects indexed by those indexes

Note: I should be clear my desire is to do this functionally, in one statement. I can do this easily with loops but that's not what I'm interested in.
I have two arrays: a numeric array of indexes, A, and an associative array, B, of objects O indexed by the elements of array A.
I want to produce an array of O in the order of the elements of A--in other words, map the indexes into real objects, based on the associative array B.
For example:
A = [ 3, 4, 2, 1 ];
B = [ 1=>"one", 2=>"two", 3=>"three", 4=>"four" ]
I want:
[ "three", "four", "two", "one" ]
Also, incidentally I'm also curious to learn what this concept is called. It's kind of like mapping, but specifically involves indexes into another array, as opposed to a function.
$A = array(3, 4, 2, 1);
$B = array(1=>"one", 2=>"two", 3=>"three", 4=>"four");
foreach($A as $i) $R[] = $B[$i];
var_dump($R);
I am just adding a little bit, if anyone is still interested in using "array_map".
<?php
$A = array( 3, 4, 2, 1);
$B = array( 1=>"one", 2=>"two", 3=>"three", 4=>"four" );
print_r($A);echo '<br/>';
print_r($B);echo '<br/>';
function matchAtoB($sourceA, $sourceB)
{
global $B;
return $B[$sourceA];
}
$O = array_map("matchAtoB", $A, $B);
print_r($O);
?>
So the function can only receive an element of each array at a time (not the whole array) and it will loop/repeat itself automatically until all elements in the array are processed.
Cheers,
You don't need a loop, you can access the elements right away:
$three = $b[$a[0]]; // "three"
$four = $b[$a[1]]; // "four"
$two = $b[$a[2]]; // "two"
$one = $b[$a[3]]; // "one"
You could see this as a 'lazy' or 'just in time' way of accomplishing the same goal, but without the ahead cost of indexing the hash map.
If you want the array explicitly, without the additional lookup, you will need a loop.
I'm not sure if this has a name but the combination of a 'datastore' or 'hash map' combined with an ordered array of keys is not an uncommon one.
$order = array(3, 4, 2, 1);
$input = array(1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four');
Using array_map() (PHP 5.3 >)
$output = array_map(function($v) use($input) {
return $input[$v];
}, $order);
However, this is essentially the same as doing the following:
$output = array();
foreach ($order as $o) {
$output[] = $input[$o];
}
I can't honestly see a shorter way of doing this.
NullUserException posted the answer in a comment:
array_map(function ($v) use ($b) { return $b[$v]; }, $a);

Categories