array_splice() - Numerical Offsets of Associative Arrays - php

I'm trying to do something but I can't find any solution, I'm also having some trouble putting it into works so here is a sample code, maybe it'll be enough to demonstrate what I'm aiming for:
$input = array
(
'who' => 'me',
'what' => 'car',
'more' => 'car',
'when' => 'today',
);
Now, I want to use array_splice() to remove (and return) one element from the array:
$spliced = key(array_splice($input, 2, 1)); // I'm only interested in the key...
The above will remove and return 1 element (third argument) from $input (first argument), at offset 2 (second argument), so $spliced will hold the value more.
I'll be iterating over $input with a foreach loop, I know the key to be spliced but the problem is I don't know its numerical offset and since array_splice only accepts integers I don't know what to do.
A very dull example:
$result = array();
foreach ($input as $key => $value)
{
if ($key == 'more')
{
// Remove the index "more" from $input and add it to $result.
$result[] = key(array_splice($input, 2 /* How do I know its 2? */, 1));
}
}
I first though of using array_search() but it's pointless since it'll return the associative index....
How do I determine the numerical offset of a associative index?

Just grabbing and unsetting the value is a much better approach (and likely faster too), but anyway, you can just count along
$result = array();
$idx = 0; // init offset
foreach ($input as $key => $value)
{
if ($key == 'more')
{
// Remove the index "more" from $input and add it to $result.
$result[] = key(array_splice($input, $idx, 1));
}
$idx++; // count offset
}
print_R($result);
print_R($input);
gives
Array
(
[0] => more
)
Array
(
[who] => me
[what] => car
[when] => today
)
BUT Technically speaking an associative key has no numerical index. If the input array was
$input = array
(
'who' => 'me',
'what' => 'car',
'more' => 'car',
'when' => 'today',
'foo', 'bar', 'baz'
);
then index 2 is "baz". But since array_slice accepts an offset, which is not the same as a numeric key, it uses the element found at that position in the array (in order the elements appear), which is why counting along works.
On a sidenote, with numeric keys in the array, you'd get funny results, because you are testing for equality instead of identity. Make it $key === 'more' instead to prevent 'more' getting typecasted. Since associative keys are unique you could also return after 'more' was found, because checking subsequent keys is pointless. But really:
if(array_key_exists('more', $input)) unset($input['more']);

I found the solution:
$offset = array_search('more', array_keys($input)); // 2
Even with "funny" arrays, such as:
$input = array
(
'who' => 'me',
'what' => 'car',
'more' => 'car',
'when' => 'today',
'foo', 'bar', 'baz'
);
This:
echo '<pre>';
print_r(array_keys($input));
echo '</pre>';
Correctly outputs this:
Array
(
[0] => who
[1] => what
[2] => more
[3] => when
[4] => 0
[5] => 1
[6] => 2
)
It's a trivial solution but somewhat obscure to get there.
I appreciate all the help from everyone. =)

$i = 0;
foreach ($input as $key => $value)
{
if ($key == 'more')
{
// Remove the index "more" from $input and add it to $result.
$result[] = key(array_splice($input, $i , 1));
}
$i++;
}

Related

PHP - How to turn a single array to multi-dimensional array?

I have a single array like:
array (
'0' => 1,
'1' => 3,
'2' => 4
)
and I want to turn it to a multi-dimensional array like:
array (
'1' =>
array (
'3' => '4'
),
)
Another single array :
array (
'0' => 'a',
'1' => 'b',
'2' => 'c',
'3' => 'd'
)
to :
array (
'a' =>
array (
'b' =>
array (
'c' => 'd'
),
),
),
How can I achieve this?
Note:
The array is dynamic, not always containing 3 or 4 elements.
There are multiple ways to achieve this:
Simple for loop
First, use array_pop() to pop (i.e. store and remove) the last value from the array. Then iterate from the end of the array, successively creating an array with the values (via array_values()):
function transformArray($input) {
$accumulator = array_pop($input);
$values = array_values($input);
for($i = count($input)-1; $i > -1; $i--) {
$accumulator = array($values[$i] => $accumulator);
}
return $accumulator;
}
See it demonstrated in this playground example
foreach loop
Using the code from the previous example, the for loop can be replaced with a foreach loop by utilizing array_reverse(). That way we don't have to worry about when to start and stop the index values, index in to the array, etc.
function transformArray($input) {
$accumulator = array_pop($input);
$values = array_reverse(array_values($input));
foreach($values as $value) {
$accumulator = array($value => $accumulator);
}
return $accumulator;
}
See it demonstrated in this playground example.
Functional approach: array_reduce()
Array_reduce() can be used to simplify the iteration. The first argument of array_reduce() is the input array, the second is a callback function and the third is the initial value. The parameters for the callback function are the accumulator value (from previous iterations) and the array element at the current array index). The initial value is used as the accumulator value for the first iteration, so we can pass the value that was popped from the end of the array.
function transformArray($input) {
$accumulator = array_pop($input);
$values = array_reverse(array_values($input));
return array_reduce($values, function($accumulator, $element) {
return array($element => $accumulator);
}, $accumulator);
}
See it demonstrated in this playground example.
Recursive Approach
Perhaps the simplest technique is to use a recursive function by employing array_shift() to take the first element off the array and return an array with that element as the key and the value is the return value of calling the same function, unless there is only one item, in which case that one item is returned.
function transformArray($input) {
if (count($input) == 1) {
return $input[0];
}
return array(array_shift($input) => transformArray($input));
}
See it demonstrated in this playground example.
If it has always 3 elements:
$temp = array (
'0' => 1,
'1' => 3,
'2' => 4
);
$result = array();
$result[$temp['0']] = array($temp['1'] => $temp['2']);
Although I think there are better strategies, it's possible to use eval() for this purpose:
<?php
/* Example array */
$arr1 = array(
'0' => 'a',
'1' => 'b',
'2' => 'c',
'3' => 'd'
);
$array_string = '';
/* Build array string */
for($i = 0; $i < count($arr1); $i++){
if($i < (count($arr1) - 1)){
$array_string .= 'array ( \''. $arr1[$i] .'\' => ';
} else {
$array_string .= ' \''. $arr1[$i] .'\'';
}
}
/* Close array string */
for($y = 0; $y < count($arr1); $y++){
if($y < (count($arr1) - 2)){
$array_string .= ' ), ';
} else {
$array_string .= ' )';
break;
}
}
/* Evaluate array string as PHP code and put it in $result */
$result = eval("return $array_string;");
/* Print the result */
echo '<pre>';
print_r($result);
echo '</pre>';
?>
Result:
Array
(
[a] => Array
(
[b] => Array
(
[c] => d
)
)
)
But as I said, keep this quote in mind:
If eval() is the answer, you're almost certainly asking the wrong question. -- Rasmus Lerdorf, BDFL of PHP
Ask yourself why you need this. Is there another strategy available for what you want?

Shuffle the order of keys in an associative array, if they have the same values?

Given an associative array like this, how can you shuffle the order of keys that have the same value?
array(a => 1,
b => 2, // make b or c ordered first, randomly
c => 2,
d => 4,
e => 5, // make e or f ordered first, randomly
f => 5);
The approach I tried was to turn it into a structure like this and shuffle the values (which are arrays of the original keys) and then flatten it back into the original form. Is there a simpler or cleaner approach? (I'm not worried about efficiency, this is for small data sets.)
array(1 => [a],
2 => [b, c], // shuffle these
4 => [d],
5 => [e, f]); // shuffle these
function array_sort_randomize_equal_values($array) {
$collect_by_value = array();
foreach ($array as $key => $value) {
if (! array_key_exists($value, $collect_by_value)) {
$collect_by_value[$value] = array();
}
// note the &, we want to modify the array, not get a copy
$subarray = &$collect_by_value[$value];
array_push($subarray, $key);
}
arsort($collect_by_value);
$reordered = array();
foreach ($collect_by_value as $value => $array_of_keys) {
// after randomizing keys with the same value, create a new array
shuffle($array_of_keys);
foreach ($array_of_keys as $key) {
array_push($reordered, $value);
}
}
return $reordered;
}
I rewrote the entire code, since I found another way which is a lot simpler and faster than the old one(If you are still interested in the old one see the revision):
old code (100,000 executions): Ø 4.4 sec.
new code (100,000 executions): Ø 1.3 sec.
Explanation
First we get all unique values from the array with array_flip(), since then the values are the keys and you can't have duplicate keys in an array we have our unique values. We also create an array $result for then storing our result in it and $keyPool for storing all keys for each value.
Now we loop through our unique values and get all keys which have the same value into an array with array_keys() and save it in $keyPool with the value as key. We can also right away shuffle() the keys array, so that they are already random:
foreach($uniqueValues as $value => $notNeeded){
$keyPool[$value] = array_keys($arr, $value, TRUE);
shuffle($keyPool[$value]);
}
Now we can already loop through our original array and get a key with array_shift() from the $keyPool for each value and save it in $result:
foreach($arr as $value)
$result[array_shift($keyPool[$value])] = $value;
Since we already shuffled the array the keys already have a random order and we just use array_shift(), so that we can't use the key twice.
Code
<?php
$arr = ["a" => 1, "b" => 1, "c" => 1, "d" => 1, "e" => 1, "f" => 2,
"g" => 1, "h" => 3, "i" => 4, "j" => 5, "k" => 5];
function randomize_duplicate_array_value_keys(array $arr){
$uniqueValues = array_flip($arr);
$result = [];
$keyPool = [];
foreach($uniqueValues as $value => $notNeeded){
$keyPool[$value] = array_keys($arr, $value, TRUE);
shuffle($keyPool[$value]);
}
foreach($arr as $value)
$result[array_shift($keyPool[$value])] = $value;
return $result;
}
$result = randomize_duplicate_array_value_keys($arr);
print_r($result);
?>
(possible) output:
Array (
[b] => 1
[g] => 1
[a] => 1
[e] => 1
[d] => 1
[f] => 2
[c] => 1
[h] => 3
[i] => 4
[k] => 5
[j] => 5
)
Footnotes
I used array_flip() instead of array_unique() to get the unique values from the array, since it's slightly faster.
I also removed the if statement to check if the array has more than one elements and needs to be shuffled, since with and without the if statement the code runs pretty much with the same execution time. I just removed it to make it easier to understand and the code more readable:
if(count($keyPool[$value]) > 1)
shuffle($keyPool[$value]);
You can also make some optimization changes if you want:
Preemptively return, if you get an empty array, e.g.
function randomize_duplicate_array_value_keys(array $arr){
if(empty($arr))
return [];
$uniqueValues = array_flip($arr);
$result = [];
//***
}
Preemptively return the array, if it doesn't have duplicate values:
function randomize_duplicate_array_value_keys(array $arr){
if(empty($arr))
return [];
elseif(empty(array_filter(array_count_values($arr), function($v){return $v > 1;})))
return [];
$uniqueValues = array_flip($arr);
$result = [];
//***
}
Here's another way that iterates through the sorted array while keeping track of the previous value. If the previous value is different than the current one, then the previous value is added to a new array while the current value becomes the previous value. If the current value is the same as the previous value, then depending on the outcome of rand(0,1) either the previous value is added to the new list as before, or the current value is added to the new list first:
<?php
$l = ['a' => 1,'b' => 2, 'c' => 2,
'd' => 4,'e' => 5,'f' => 5];
asort($l);
$prevK = key($l);
$prevV = array_shift($l); //initialize prev to 1st element
$shuffled = [];
foreach($l as $k => $v) {
if($v != $prevV || rand(0,1)) {
$shuffled[$prevK] = $prevV;
$prevK = $k;
$prevV = $v;
}
else {
$shuffled[$k] = $v;
}
}
$shuffled[$prevK] = $prevV;
print_r($shuffled);

Find highest value in multidimensional array [duplicate]

This question already has answers here:
Get min and max value in PHP Array
(9 answers)
Closed 2 years ago.
The Problem
I have a multidimensional array similar to the one below. What I'm trying to achieve is a way to find and retrieve from the array the one with the highest "Total" value, now I know there's a function called max but that doesn't work with a multidimensional array like this.
What I've thought about doing is creating a foreach loop and building a new array with only the totals, then using max to find the max value, which would work, the only issue would then be retrieving the rest of the data which relates to that max value. I'm not sure that's the most efficient way either.
Any ideas?
Array
(
[0] => Array
(
[Key1] => Key1
[Total] => 13
)
[1] => Array
(
[Key2] => Key2
[Total] => 117
)
[2] => Array
(
[Key3] => Key3
[Total] => 39
)
)
Since PHP 5.5 you can use array_column to get an array of values for specific key, and max it.
max(array_column($array, 'Total'))
Just do a simple loop and compare values or use array_reduce. # is an error suppressor; it hides the fact that $a['total'] is not declared before it is accessed on the first iteration. Demo
$data = array_reduce($data, function ($a, $b) {
return #$a['Total'] > $b['Total'] ? $a : $b ;
});
print_r($data);
// Array( [Key2] => Key2 [Total] => 117 )
It could also be written with arrow function syntax which has been avaiable since PHP7.4. Demo
var_export(
array_reduce(
$data,
fn($result, $row) =>
$result['Total'] > $row['Total']
? $result
: $row,
['Key1' => null, 'Total' => PHP_INT_MIN]
)
);
// array ('Key2' => 'Key2', 'Total' => 117,)
It's so basic algorithm.
$max = -9999999; //will hold max val
$found_item = null; //will hold item with max val;
foreach($arr as $k=>$v)
{
if($v['Total']>$max)
{
$max = $v['Total'];
$found_item = $v;
}
}
echo "max value is $max";
print_r($found_item);
Working demo
I know this question is old, but I'm providing the following answer in response to another question that pointed here after being marked as a duplicate. This is another alternative I don't see mentioned in the current answers.
I know there's a function called max but that doesn't work with a multidimensional array like this.
You can get around that with array_column which makes getting the maximum value very easy:
$arr = [['message_id' => 1,
'points' => 3],
['message_id' => 2,
'points' => 2],
['message_id' => 3,
'points' => 2]];
// max value
$max = max(array_column($arr, 'points'));
Getting the associative key is where it gets a little more tricky, considering that you might actually want multiple keys (if $max matches more than one value). You can do this with an anonymous function inside array_map, and use array_filter to remove the null values:
// keys of max value
$keys = array_filter(array_map(function ($arr) use ($max) {
return $arr['points'] == $max ? $arr['message_id'] : null;
}, $arr));
Output:
array(1) {
[0]=>
int(1)
}
If you do end up with multiples keys but are only interested in the first match found, then simply reference $keys[0].
another simple method will be
$max = array_map( function( $arr ) {
global $last;
return (int)( ( $arr["Total"] > $last ) ? $arr["Total"] : $last );
}, $array );
print_r( max( $max ) );
<?php
$myarray = array(
0 => array(
'Key1' => 'Key1',
'Total' => 13,
),
1 => array(
'Key2' => 'Key2',
'Total' => 117,
),
2 => array(
'Key2' => 'Key3',
'Total' => 39,
),
);
$out = array();
foreach ($myarray as $item) {
$out[] = $item['Total'];
}
echo max($out); //117
unset($out, $item);
Can be done using array_walk(array_walk_recursive if needed)
$arr is the array you want to search in
$largestElement = null;
array_walk($arr, function(&$item, $key) use (&$largestElement) {
if (!is_array($largestElement) || $largestElement["Total"] < $item["Total"]) {
$largestElement = $item;
}
});
You can use php usort function:
http://php.net/manual/en/function.usort.php
A pretty illustrative example is given here:
<?php
function cmp($a, $b)
{
return strcmp($a["fruit"], $b["fruit"]);
}
$fruits[0]["fruit"] = "lemons";
$fruits[1]["fruit"] = "apples";
$fruits[2]["fruit"] = "grapes";
usort($fruits, "cmp");
while (list($key, $value) = each($fruits)) {
echo "\$fruits[$key]: " . $value["fruit"] . "\n";
}
?>
So it will sort the max value to the last array index.
Output:
$fruits[0]: apples
$fruits[1]: grapes
$fruits[2]: lemons
This example is given on aforementioned link
array_reduce accepts a 3rd "initial" parameter. Use this to avoid the bad practice of using "#" error suppression :
$data = array_reduce($data, function ($a, $b) {
return $a['Total'] > $b['Total'] ? $a : $b ;
},['Total' => 0]);
print_r($data);
PHP 7.4
$data = array_reduce($data, fn(a,b) => $a['Total'] > $b['Total'] ? $a : $b, ['Total' => 0]);

array_pop() with Key

Consider the following array
$array = array('fruit' => 'apple',
'vegetable' => 'potato',
'dairy' => 'cheese');
I wanted to use array_pop to get the last key/value pair.
However, one will note that after the following
$last = array_pop($array);
var_dump($last);
It will output only the value (string(6) "cheese")
How can I "pop" the last pair from the array, preserving the key/value array structure?
Check out array_slice() http://php.net/manual/en/function.array-slice.php
Last argument true is to preserve keys.
When you pass the offset as negative, it starts from the end. It's a nice trick to get last elements without counting the total.
$array = [
"a" => 1,
"b" => 2,
"c" => 3,
];
$lastElementWithKey = array_slice($array, -1, 1, true);
print_r($lastElementWithKey);
Outputs:
Array
(
[c] => 3
)
try
end($array); //pointer to end
each($array); //get pair
You can use end() and key() to the the key and the value, then you can pop the value.
$array = array('fruit' => 'apple', 'vegetable' => 'potato', 'dairy' => 'cheese');
$val = end($array); // 'cheese'
// Moves array pointer to end
$key = key($array); // 'dairy'
// Gets key at current array position
array_pop($array); // Removes the element
// Resets array pointer
Why not using new features? The following code works as of PHP 7.3:
// As simple as is!
$lastPair = [array_key_last($array) => array_pop($array)];
The code above is neat and efficient (as I tested, it's about 20% faster than array_slice() + array_pop() for an array with 10000 elements; and the reason is that array_key_last() is really fast). This way the last value will also be removed.
Tip: You can also extract key and value separately:
[$key, $value] = [array_key_last($array), array_pop($array)];
This should work, just don't do it inside a foreach loop (it'll mess up the loop)
end($array); // set the array pointer to the end
$keyvaluepair = each($array); // read the key/value
reset($array); // for good measure
Edit: Briedis suggests array_slice() which is probably a better solution
Another option:
<?php
end($array);
list($key, $value) = each($array);
array_pop($array);
var_dump($key, $value);
?>
Try this:
<?php
function array_end($array)
{
$val = end($array);
return array(array_search($val, $array) => $val);
}
$array = array(
'fruit' => 'apple',
'vegetable' => 'potato',
'dairy' => 'cheese'
);
echo "<pre>";
print_r(array_end($array));
?>
Output:
Array
(
[dairy] => cheese
)

Hard PHP nut : how to insert items into one associate array?

Yes, this is a bit of a trick question; one array (without copies), as opposed to any odd array. Let me explain, so let's start here ;
$a = array ( 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4, 'five' => 5, 'six' => 6 ) ;
Pretend that this array is long, over a hundred long. I loop through it, step by step, but at some point (let's make up that this happens at the second item) something happens. Maybe the data is funky. Nevertheless, we need to add some items to it for later processing, and then keep looping through it, without losing the current position. Basically, I would like to do something like this ;
echo current ( $a ) ; // 'two'
array_insert ( $a, 'four', 'new_item', 100 ) ;
echo current ( $a ) ; // 'two'
Definition for array_insert is ( $array, $key_where_insert_happens, $new_key, $new_value ) ; Of course $new_key and $new_value should be wrapped in an array wrapper, but that's besides the point right now. Here's what I want to see happening after the above code having ran ;
print_r ( $a ) ; // array ( 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4, 'new_item' => 100, 'five' => 5, 'six' => 6 ) ;
echo current ( $a ) ; // 'two'
Whenever you use array_splice, array_slice, array_push or most of the other array fiddling functions, you basically create a copy of the array, and then you can copy it back, but this breaks the reference to the original array and the position as well, and my loop above breaks. I could use direct reference (ie. $a['new_item'] = 'whatever;) or put it at the end, but none of these will insert items into a given position.
Any takers? How can I do a true insert into an associative array directly (that's being processed elsewhere)? My only solution so far is to ;
record the position (current())
Do the splice/insert (array_slice)
overwrite old array with new ($old = $new)
search the new position (first reset() then looping through to find it [!!!!!!])
Surely there's a better, simpler and more elegant way for doing something that's currently kludgy, heavy and poorly hobbled together? Why isn't there a array_set_position ( $key ) function that quickly can help this out, or an array_insert that works directly on the same array (or both)?
Maybe I'm not understanding you correctly but have you looked into array_splice()?
This answer might also interest you.
Would something like this work?
function array_insert($input, $key, $value)
{
if (($key = array_search($key, array_keys($input))) !== false)
{
return array_splice($input, $key, 1, $value);
}
return $input;
}
This was the best I could come up with:
$a = array
(
'one' => 1,
'two' => 2,
'three' => 3,
'four' => 4,
'five' => 5,
'six' => 6,
);
ph()->Dump(next($a)); // 2
array_insert($a, 'four', array('new_item' => 100));
ph()->Dump(current($a)); // 2
function array_insert(&$array, $key, $data)
{
$k = key($array);
if (array_key_exists($key, $array) === true)
{
$key = array_search($key, array_keys($array)) + 1;
$array = array_slice($array, null, $key, true) + $data + array_slice($array, $key, null, true);
while ($k != key($array))
{
next($array);
}
}
}
ph()->Dump($a);
/*
Array
(
[one] => 1
[two] => 2
[three] => 3
[four] => 4
[new_item] => 100
[five] => 5
[six] => 6
)
*/
I don't think it's possible to set an array internal pointer without looping.
Rather than using the associative array as the event queue, keep a separate integer-indexed array. Loop over an explicit index variable rather than using the internal array position.
$events = array_keys($a);
for ($i=0; $i < count($events); ++$i) {
...
/* if there's an event to add after $j */
array_splice($events, $j+1, 0, array($new_event_key));
$a[$new_event_key] = $new_event_data;
...
}
To keep things more cohesive, you can package the two arrays into an event queue class.

Categories