Distribute associative array elements into groups with a maximum sum per group - php

I need to split my associative array into bunches of not greater than 50 in each bunch. Multiple elements may be pushed into a given group to ensure that a group reaches 50 before starting a new group.
Sample input:
$array = [
'5' => 142,
'2' => 57,
'18' => 37
];
Desired result:
[
['5' => 50],
['5' => 50],
['5' => 42, '2' => 8],
['2' => 49, '18' => 1],
['18' => 36],
];

just mind games
$line = [];
// Invert array
foreach($arr as $k=>$v) {
$line = array_merge($line, array_fill(0, $v, $k));
}
// Split and count occurrences
$res = array_map('array_count_values', array_chunk($line, 50));
print_r($res);
demo

To create an array where the sum of each entry would not exceed a given amount, you can use an iterative approach.
Let's start with an empty array and a variable representing the working index of that array. As we go through the input array, we add the maximum possible remaining quantity to the new array. If we reach the limit, we increment the index variable. And we continue as long as the input array has not been completely browsed.
Code:
const MAX_SUM = 50;
$total = []; // Store the new data
$curKey = 0; // Store the current key of $total array.
foreach ($array as $key => $value) {
while ($value) {
// Get the current sum for the current key:
$curSum = array_sum($total[$curKey] ?? []);
// If the max was reached, we can go to the next key:
if ($curSum == MAX_SUM) $curKey++;
// Now, compute if the value to add (max to reach 50);
$add = $value + $curSum > MAX_SUM // If above,
? MAX_SUM - $curSum // got only the difference,
: $value; // else, use the full value.
// Add the value
$total[$curKey][$key] = $add;
// Finally, remove what we added just before.
$value -= $add;
}
}
print_r($total);
Outputs :
Array (
[0] => Array (
[5] => 50
)
[1] => Array (
[5] => 50
)
[2] => Array (
[5] => 42
[2] => 8
)
[3] => Array (
[2] => 49
[18] => 1
)
[4] => Array (
[18] => 36
)
)
See also a the nice answer of #mickmackusa.

My train of thought for this task aligns with #Syscall's "push & consume" approach.
Iterate the input array to access the keys and values.
Use an inner loop to repeatedly process the current value until it is fully consumed by subtraction. Only break the loop upon the value being reduced to zero.
With each pass of the inner loop:
Calculate the current total of all values in the group, then
Find the lesser integer value between how much room is left in the group and how much is left in the value; push that integer into the group with the current key; then subtract that integer from the current value, then
Increment the group key if the current group has reach its limit
Repeat until all input elements are reduced to zero.
Code: (Demo)
$groupLimit = 50; // declare as a variable to avoid "magic numbers" in code
$result = [];
$groupKey = 0;
foreach ($array as $key => $value) {
while ($value) {
$sum = array_sum($result[$groupKey] ?? []); // get group sum
$value -= $result[$groupKey][$key] = min($groupLimit - $sum, $value); // push key with limited value; decrease value
$groupKey += ($sum + $result[$groupKey][$key] === $groupLimit); // only increment group key if at $groupLimit
}
}
var_export($result);

Related

How to find the median of deepest subarrays of multidimensional array?

I have a four-level multidimensional array. I need to sort in ascending order (ASC) the numeric "leaves" in order to calculate the median of the values.
I tried array_walk_recursive(), array_multisort(), usort(), etc. but was unable to find a working solution.
Here's a schematic of the array:
(
[2017-05-01] => Array
(
[DC] => Array
(
[IT] => Array
(
[0] => 90
[1] => 0
)
[DE] => Array
(
[0] => 18
[1] => 315
[2] => 40
[3] =>
[4] => 69
)
[Other] => Array
(
[0] => 107
[1] => 46
[2] =>
[3] =>
[4] => 27
[5] => 22
)
)
)
)
This will output the deepest subarrays' median values using the input array's structure.
I'm including hard-casting of median values (one or both in a subset) as integers in the event that the value(s) are empty strings. I'll also assume that you will want 0 as the output if a subset is empty.
Code: (Demo)
$array=[
'2017-05-01'=>[
'DC'=>[
'IT'=>[90, 0],
'DE'=>[18, 315, 40, '', 69, 211],
'Other'=>[107, 46, '', '', 27, 22]
]
],
'2017-05-02'=>[
'DC'=>[
'IT'=>[70, 40, 55],
'DE'=>['', 31, 4, '', 9],
'Other'=>[1107, 12, 0, 20, 1, 11, 21]
]
],
'fringe case'=>[
'DC'=>[
'IT'=>[],
'DE'=>['', '', '', 99],
'Other'=>['', 99]
]
]
];
foreach ($array as $k1 => $lv1) {
foreach ($lv1 as $k2 => $lv2) {
foreach ($lv2 as $k3 => $lv3) {
sort($lv3); // order values ASC
$count = sizeof($lv3); // count number of values
$index = floor($count / 2); // get middle index or upper of middle two
if (!$count) { // count is zero
$medians[$k1][$k2][$k3] = 0;
} elseif ($count & 1) { // count is odd
$medians[$k1][$k2][$k3] = (int)$lv3[$index]; // single median
} else { // count is even
$medians[$k1][$k2][$k3] = ((int)$lv3[$index-1] + (int)$lv3[$index]) / 2; // dual median
}
}
}
}
var_export($medians);
Output:
array (
'2017-05-01' =>
array (
'DC' =>
array (
'IT' => 45,
'DE' => 54.5,
'Other' => 24.5,
),
),
'2017-05-02' =>
array (
'DC' =>
array (
'IT' => 55,
'DE' => 4,
'Other' => 12,
),
),
'fringe case' =>
array (
'DC' =>
array (
'IT' => 0,
'DE' => 0,
'Other' => 49.5,
),
),
)
*for the record, $count & 1 is a bitwise comparison that determines if the value is odd without performing arithmetic (and is the most efficient way of performing this check within php).
*also, if you wanted to simply overwrite the values of the input array, you could modify by reference by writing & before $lv1, $lv2, and $lv3 in the foreach declarations then save the median value to $lv3. Demo The benefit in doing so removes key declarations and making your code more brief.
As it turns out, there is a way to do what the OP seeks using a combination of usort() and array_walk(), each of which takes a callback, as follows:
<?php
// median code:
//http://www.mdj.us/web-development/php-programming/calculating-the-median-average-values-of-an-array-with-php/
function calculate_median($arr) {
sort($arr);
$count = count($arr); //total numbers in array
$middleval = floor(($count-1)/2); // find the middle value, or the lowest middle value
if($count % 2) { // odd number, middle is the median
$median = $arr[$middleval];
} else { // even number, calculate avg of 2 medians
$low = $arr[$middleval];
$high = $arr[$middleval+1];
$median = (($low+$high)/2);
}
return $median;
}
$a = [];
$a["2017-05-01"] = ["DC"];
$a["2017-05-01"]["DC"]["IT"] = [90,0];
$a["2017-05-01"]["DC"]["DE"] = [18,315,40,"",69];
$a["2017-05-01"]["DC"]["Other"] = [107,46,"","",27,22];
function sort_by_order ($a, $b)
{
if ($a == "") $a = 0;
if ($b == "") $b = 0;
return $a - $b;
}
function test($item,$key){
echo $key," ";
if (is_array($item)) {
echo array_keys($item)[1],"\n";
$popped = array_pop($item);
foreach ($popped as $key => $arr) {
usort($arr, 'sort_by_order');
echo "Median ($key): ",calculate_median( $arr ),"\n";
}
}
}
array_walk($a, 'test');
See demo here. Also, see this example based on the OP's sandbox.
Although the OP's code does not show the array keys as quoted, beware they should be in the actual code, otherwise PHP will do math with 2017-05-01 and you'll see a key of 2011. Interesting read here about usort.
The median code I extracted from here.
Interestingly, the conventional wisdom about sorting numbers to determine the median is not necessarily the only way to obtain that result. Apparently, it can also be done and perhaps more efficiently by finding a pivot number and dividing the series of numbers into three parts (see this response).

PHP: take out duplicate digits from an array then print them out

I'm probably [super]overthinking this. I'm trying to analyze an array with values like [1,9], [4,6] [5,5], [6,4], [9,1] and duplicate digits (I'm having a super brain fart and can't even remember the term for numbers like this) remove (the last two) so that only [1,9], [4,6] [5,5] are printed.
I was thinking that turning this array into a string and using preg_match, but I'm pretty sure this wouldn't work even if I had the correct regex.
If you have an array of pairs like this:
$x = array(
array(1,9),
array(4,6),
array(5,5),
array(6,4),
array(9,1)
);
Here is one way to get the unique pairs:
foreach ($x as $pair) {
sort($pair);
$unique_pairs[implode(',', $pair)] = $pair;
}
This uses string representations of each sorted pair as keys in a new array, so the result will have distinct values by definition.
As far as the printing them out part of your question, once you have the unique values you can loop over them and print them out in whichever format you like, for example:
foreach ($unique_pairs as $pair) { vprintf("[%d,%d]<br>", $pair); }
It looks like elements are distributed symmetrically.
We can cut the array in two halves and get only the first half with array_slice():
$array = array(
array(1,9),
array(4,6),
array(5,5),
array(6,4),
array(9,1),
);
print_r(array_slice($array, 0, ceil(count($array) / 2)));
Result:
Array(
[0] => Array(
[0] => 1
[1] => 9
)
[1] => Array(
[0] => 4
[1] => 6
)
[2] => Array(
[0] => 5
[1] => 5
)
)
Demo at Codepad.
ceil() is used to round the number up to the next highest integer if there is an even number of items in the array. Example: if there is 3 items in the array, 5 / 2 will return 2.5, we want 3 items so we use ceil(2.5) which gives 3.
Example with 3 items:
$array = array(
array(1,9),
array(5,5),
array(9,1),
);
print_r(array_slice($array, 0, ceil(count($array) / 2)));
Result:
Array(
[0] => Array(
[0] => 1
[1] => 9
)
[1] => Array(
[0] => 5
[1] => 5
)
)
Example with 4 items:
$array = array(
array(1,9),
array(7,7),
array(7,7),
array(9,1),
);
print_r(array_slice($array, 0, ceil(count($array) / 2)));
Result:
Array(
[0] => Array(
[0] => 1
[1] => 9
)
[1] => Array(
[0] => 7
[1] => 7
)
)
If I'm correct in understanding what you are trying to do, you want to remove the final 2 elements from the array?
There is a function in PHP called array_pop that removes the final element from the array.
$array = array_pop($array);
So if you run this twice, you will remove the final 2 elements from the array.
This is how I'd do it (and I hope I am not overthinking this :))
$stringArray = array();
$stringArray[] = '1,9';
$stringArray[] = '4,6';
$stringArray[] = '5,5';
$stringArray[] = '6,4';
$stringArray[] = '9,1';
foreach($stringArray as &$numString) {
$numString = explode(',', $numString);
usort($numString, function($a, $b) {return $a - $b;});
$numString = implode(',', $numString);
}
$a = array_unique($a);
print_r($a);
You basically explode every element into a subarray, sort it and then implode it back. After calling the array_unique, you're left with unique values in the array.
The output would be
Array
(
[0] => 1,9
[1] => 4,6
[2] => 5,5
)
The result you suggest treats [a,b] as equivalent to [b,a] which makes the problem a lot more complex. The code below gives the result you asked for, but without really understanding what the problem is that you are trying to fix and whether [1,9] is equivalent to [9,1] in the solution:
$a=array(array(1,9),array(4,6),...
$dup=array();
for ($i=0; $i<count($a) -1; $i++) {
for ($j=$i+1; $j<count($a); $j++) {
if (($a[$i][0]==$a[$j[0] && $a[$i][1]==$a[$j[1])
|| ($a[$i][0]==$a[$j[1] && $a[$i][1]==$a[$j[0])) {
$dup[]=$j;
}
}
}
foreach ($dup as $i) {
unset($a[$i]);
}
So I'm actually going to assume your question to have a different meaning than everyone else did. I believe what you're asking is:
How do you filter out array items where a reverse of the item has already been used?
<?php
// The example set you gave
$numberSets = [[1, 9], [4, 6], [5, 5], [6, 4], [9, 1]];
// Initialize an empty array to keep track of what we've seen
$keys = [];
// We use array filter to get rid of items we don't want
// (Notice that we use & on $keys, so that we can update the variable in the global scope)
$numberSets = array_filter($numberSets, function($set) use(&$keys) {
// Reverse the array
$set = array_reverse($set);
// Create a string of the items
$key = implode('', $set);
// Get the reverse of the numbers
$reversedKey = strrev($key);
// If the palindrome of our string was used, return false to filter
if (isset($keys[$reversedKey])) {
return false;
}
// Set the key so it's not used again
// Since $keys is being passed by reference it is updated in global scope
$keys[$key] = true;
// Return true to NOT filter this item, since it or it's reverse were not matched
return true;
});
var_dump($numberSets);

How to fetch one array value from the array?

Hi I have array like this
Array (
[0] => stdClass Object (
[id] => 1930 [value] => 20)
[1] => stdClass Object (
[id] => 1931 [value] => 30 )
[2] => stdClass Object (
[id] => 1937 [value] => 30 )
[3] => stdClass Object (
[id] => 1938 [value] => 20 )
)
I want to fetch random array from this. The Id which has greater value(%) should be fetched more time (That value is %).
Make a another array and repeat the index of array for the number of times its value is and then fetch the values randomly this would do what you want
The above mentioned array would be
array(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,)
$arr = Array (
(Object) array (id => 1930, value => 20),
(Object) array (id => 1931, value => 30),
(Object) array (id => 1937, value => 30),
(Object) array (id => 1938, value => 20));
// count sum of all values
$sum = array_reduce($arr, function ($c, $i) { return $c += $i->value; }, 0);
// get random
$rand = mt_rand(0, $sum);
echo $rand . " ";
// select 1st item that sum of values >= random value
$new = array_filter($arr, function ($i) use (&$rand) { if($rand > 0 && ($rand -= $i->value) <= 0) return true; else return false; });
// change key to 0
$new = array_values($new);
echo $new[0]->id;
One way to solve this is to make a list of the cummulative sum of the values in the array, for instance if the values are 3, 1, and 2 the cummulative sums would be 3, 4 and 6. Then when you want to get a random element, you generate a random integer between 1 maximum sum, and then pick the element whose cumulative sum is largest of those whos cumulative sum is smaller than or equal to the random number. In that way, the chanse of any element being picked is proportional to its value.
So here is the setup, creating the cumulative sums:
var $cumsum = array();
var $sum = 0;
foreach($array as $obj) {
$sum += $obj->value;
array_push($cumsum, $sum);
}
var $maxsum = end($cumsum);
And here is the function actually picking an element:
function getRandomObject() {
var $r = random(1, $max);
if($r < $cumsum[0]) return $array[0];
for($i = 0; $i < count($cumsum); $i++)
if($cumsum[$i]) > $r)
return $array[$i--];
return $array[$i];
}
Disclaimar: I have not tested this code, so don't expect it to run on a copy paste. Also, whatever code you use you should probably make sure it returns elements with the right probability using a monte carlo method.

How to conditionally merge a multi-dimensional array

I have a multi-dimensional array:
Array
(
[10] => Array
(
[from] => Jack
[to] => Terry
[Bribe] => 0
[Joke_Payment] => 0
[Corrupt_Support] => 1
[Legitimate_Support] => 0
[Obfuscation] => 1
[Legal_Enforcement] => 0
[Whistleblower] => 0
)
[11] => Array
(
[from] => Terry
[to] => Jack
[Bribe] => 0
[Joke_Payment] => 0
[Corrupt_Support] => 1
[Legitimate_Support] => 0
[Obfuscation] => 0
[Legal_Enforcement] => 1
[Whistleblower] => 0
)
)
I want to update the above array to like this: have a 1 (going from from to to) and a -1 for the opposite direction, a 2 signify for "both directions.
Array ( [10] => Array
(
[from] => Jack
[to] => Terry
[Bribe] => 0
[Joke_Payment] => 0
[Corrupt_Support] => 2
[Legitimate_Support] => 0
[Obfuscation] => 1
[Legal_Enforcement] => -1
[Whistleblower] => 0
) }
How can I firstly calculate their intersect and then update the original array?
They will have the same amount to keys, and when from matches to, and to matches from element, I want to combine these two arrays into one. A '1' is used to indicate that the property relates in the from to to direction, and a -1 indicates that the property goes in the reverse direction (from to to from).
My current code is:
$fileRelation = explode("\r", $end);
$rowsRelation = array_map('str_getcsv', $fileRelation);
$headerRelation = array_shift($rowsRelation);
$csvArrRelation = array();
$countR = count($headerRelation);
foreach ($rowsRelation as $key => &$row) {
$index = 2;$sum = 0;$sumArr = array();
while ($index < $countR) {
if ($row[$index]>0) {
$sumArr[$sum]=$index; //store row $headerRelation index
$sum++;
}
$index++;
}
if($sum > 0){ //remove element if no relationship exist
foreach ($csvArrRelation as $k => $a) {
if (in_array($row[0], $csvArrRelation[$k]) && in_array($row[1], $csvArrRelation[$k])){
$p = array_values($a);$i = 2;$cp= count($p);
while($i < $cp ){
if($row[$i] == $p[$i]){
$a[$headerRelation[$i]] += $row[$i];
}else{
$a[$headerRelation[$i]] -= $row[$i];
}
$i++;
}
unset( $rowsRelation[$key] );
}
}
$csvArrRelation[] = array_combine($headerRelation, $row);
}
}
I won't write this for you, but here is a good start:
$newRelations = [];
$deletions = [];
foreach ($rowsRelation as $key1 => $row1)
{
foreach ($rowsRelation as $key2 => $row2)
{
// Don't compare with self
if ($key1 != $key2)
{
// Have we found a reverse match?
if (
$row1['from'] == $row2['to'] &&
$row1['to'] == $row2['from']
)
{
$newRelations[] = myMerge($row1, $row2);
$deletions[] = $key1;
$deletions[] = $key2;
}
}
}
}
// Delete old rows
foreach ($deletions as $deleteKey)
{
unset($rowsRelation[$deleteKey]);
}
// Add in new rows
$rowsRelation = array_merge($rowsRelation, $newRelations);
function myMerge(array $row1, array $row2)
{
// todo
}
My strategy is to compare each row with every other, and if a reverse match is found, we know a merge is due. To avoid corrupting the foreach loops, I add the new array value to a different array, and log which keys I wish to delete from the original. These are then merged/deleted after the work is done.
I assume this is some kind of game, so let's call "player 1" the player whose name appears in "from" field and "player 2" the one whose name appears in "to" field.
According to what I understood, you consider player 1's values positive and player 2's negative. Then you sum both values in "from player 1 to player 2" record and discard "from player 2 to player 1" record.
According to that rule, Corrupt_Support should end up with a value of 0, not 2.
Basically, after merging your records, you will not be able to tell the difference between no actions at all and actions that have cancelled each other.
Besides, there is no rule to chose a "winner", i.e. which "from" player will be retained, leading to potentially impractical results.
If you have 3 players Abe, Bob and Cal, you can end up with any possible order, for instance
["Abe vs Bob", "Bob vs Cal" and "Cal vs Abe"] or
["Abe vs Bob", "Abe vs Cal" and "Cal vs Bob"].
Looking up actions for Bob will be rather problematic in the latter case.
Now if you still want to do that, here is a way that takes advantage of PHP hash tables to speed up the merge and choses the "winner" according to alphabetic order:
// use "from player 1 to player 2" relation as key
foreach ($relations as $r) $relation_pairs[$r['from'].'+'.$r['to']] = $r;
// pick the "winners" according to alphabetic order
ksort ($relation_pairs);
// merge relations
while (list($k,$r) = each($relation_pairs)) // this will take elements deletion into account immediately, contrary to foreach
{
$symetric = $r['to'].'+'.$r['from'];
if (isset($relation_pairs[$symetric])) // found a symetric relation
{
foreach ($relation_pairs[$symetric] as $field=>$value) // combine all numeric fields
if (is_numeric ($value))
$relation_pairs[$k][$field] -= $value;
unset ($relation_pairs[$symetric]); // get rid of symetric relation
}
}
$compressed_relations = array_values ($relation_pairs); // get rid of keys

PHP Is it possible to sum elements of an array?

I have an array which has multiple sets of data.
Is it possible to sum elements (not all) of the array. For example, is it possible to sum the first 5 sets of data in an array, then then next 7, then the next three etc.
EDIT:
I've tried the following but with not joy:
for ($p=0; $p<=8; $p++){
$tot = 0;
$resp = 0;
$tot = $tot + $row4['Total_Staff'];
$resp = $resp + $row4['Total_Resp'];
}
Clearly I am not using this correctly!
Array Output (print_r output):
Array ( [department] => Central>ACME>BusDev [Total_Staff] => 4 [Total_Resp] => 0 )
Array ( [department] => Central>ACME>ChemDev [Total_Staff] => 7 [Total_Resp] => 0 )
Array ( [department] => Central>ACME>Admin [Total_Staff] => 1 [Total_Resp] => 0 )
Array ( [department] => Central>ACME>Chemistry [Total_Staff] => 4 [Total_Resp] => 0 )
Use array_splice() to remove $numOfElems elements from the array and the return them. Use array_sum() to calculate the sum. Repeat the process until the array is empty.
$array = array(/* your array */)
$sets = array(5,7,3);
$sum_arr = array();
foreach ($sets as $numOfElems) {
while (!empty($array)) {
$elem_arr = array_splice($array, 0, $numOfElems);
$sum[] = array_sum($elem_arr);
}
}
print_r($sum_arr);
This will repeat the process with the same sets until it reaches the end of array. If you only want count($sets) number of iterations, then perhaps, you could change while to if.
Demo

Categories