Iterate through a php multi dimensional array with specific condition - php

I have a php array below and i want to know how to get number of companies who did a training course. Look below:
Array
(
[0] => Array
(
[date_creation] => Apr 10, 2021 10:17 pm
[idformation] => 84
[idsociete] => 7
[training] => ELECTRICAL SAFETY TRAINING
[company] => ALUCAM
)
[1] => Array
(
[date_creation] => Apr 10, 2021 10:55 pm
[idformation] => 84
[idsociete] => 7
[training] => ELECTRICAL SAFETY TRAINING
[company] => ALUCAM
)
[2] => Array
(
[date_creation] => Apr 12, 2021 03:27 pm
[idformation] => 104
[idsociete] => 201
[training] => FORKLIFT, JLG SCISSOR LIFT, AERAL PLATFORM
[company] => US EMBASSY
)
);
Each array represents the record of a worker in the database from a company say Alucam and did training Electrical safety.
So from the array above i want to get something like:
2 Alucams did electrical safety as seen in the array.
I just need a clue on how to get the count of persons who did a particular training from the array.
Please help

I assume you can have the same training from different companies, opposite case you can simplified the code.
Input data (I simplified your input array, including only the fields I need):
$workers = array(array("training" => "ELECTRICAL SAFETY TRAINING", "company" => "ALUCAM"),
array("training" => "ELECTRICAL SAFETY TRAINING", "company" => "ALUCAM"),
array("training" => "FORKLIFT, JLG SCISSOR LIFT, AERAL PLATFORM", "company" => "US EMBASSY"),
array("training" => "FORKLIFT, JLG SCISSOR LIFT, AERAL PLATFORM", "company" => "ALUCAM")
);
Php code:
$trainingCount = array();
foreach($workers as $worker) {
$training = $worker["training"];
$company = $worker["company"];
if(! array_key_exists($training, $trainingCount)) {
$trainingCount[$training] = array();
}
if(! array_key_exists($company, $trainingCount[$training])) {
$trainingCount[$training][$company] = 0;
}
$trainingCount[$training][$company]++;
}
Result:
array('ELECTRICAL SAFETY TRAINING' => array('ALUCAM' => 2), 'FORKLIFT, JLG SCISSOR LIFT, AERAL PLATFORM' => array('US EMBASSY' => 1, 'ALUCAM' => 1));

Effectively you have a list of employees with their training listed in a comma separated list?
So basically you need to iterate through the list stripping out the information you require (company & training). Then every time you get a match you increment the matching data.
There are a few ways to do this the simplest would be to iterate through the results to create an array which looks something like...
$countArray = [
"Alucam" => [
"ELECTRICAL SAFETY TRAINING" = 2,
],
];
The code would look like:
$countArray = [];
// Generate the array
foreach ($array as $employee) {
$trainingList = array_map("trim", explode(",", $employee["training"]));
foreach ($trainingList as $training) {
$countArray[$employee["company"]][$training] = ($countArray[$employee["company"]][$training] ?? 0) + 1;
}
}
// Generate the output
foreach ($countArray as $companyName => $training) {
foreach ($training as $trainingName => $trainingCount) {
echo "{$trainingCount} {$companyName} did {$trainingName}", PHP_EOL;
}
}
/*
Output:
2 ALUCAM did ELECTRICAL SAFETY TRAINING
1 US EMBASSY did FORKLIFT
1 US EMBASSY did JLG SCISSOR LIFT
1 US EMBASSY did AERAL PLATFORM
*/
However, this does mean you can have "unusual" characters in array keys which could lead to problems further down the line. So you may do better with a slightly more complicated approach (i.e. having index arrays for the company and training names) which gives an array a little something like...
$countArray = [
'company' => [
0 => 'ALUCAM',
1 => 'US EMBASSY',
],
'training' => [
0 => 'ELECTRICAL SAFETY TRAINING',
1 => 'FORKLIFT',
2 => 'JLG SCISSOR LIFT',
3 => 'AERAL PLATFORM',
],
'count' => [
0 => [
0 => 2,
],
1 => [
1 => 1,
2 => 1,
3 => 1,
],
],
];
The code would look like:
// Generate the array
foreach ($array as $employee) {
if (false === ($companyIndex = array_search($employee["company"], $countArray["company"]))) {
$companyIndex = count($countArray["company"]);
$countArray["company"][] = $employee["company"];
}
$trainingList = array_map("trim", explode(",", $employee["training"]));
foreach ($trainingList as $training) {
if (false === ($trainingIndex = array_search($training, $countArray["training"]))) {
$trainingIndex = count($countArray["training"]);
$countArray["training"][] = $training;
}
$countArray["count"][$companyIndex][$trainingIndex] = ($countArray["count"][$companyIndex][$trainingIndex] ?? 0) + 1;
}
}
// Generate the output
foreach ($countArray["count"] as $companyKey => $companyCount) {
$companyName = $countArray["company"][$companyKey];
foreach ($companyCount as $trainingKey => $trainingCount) {
$trainingName = $countArray["training"][$trainingKey];
echo "{$trainingCount} {$companyName} did {$trainingName}", PHP_EOL;
}
}

You can use array_count_values and array_column to achieve something like this: You can modify as required.
$arr = [
['date_creation' => 'Apr 10, 2021 10:17 pm', 'idformation' => 84, 'idsociete' => 7, 'training' => 'ELECTRICAL SAFETY TRAINING', 'company' => 'ALUCAM'],
['date_creation' => 'Apr 10, 2021 10:17 pm', 'idformation' => 84, 'idsociete' => 7, 'training' => 'ELECTRICAL SAFETY TRAINING', 'company' => 'ALUCAM'],
['date_creation' => 'Apr 12, 2021 03:27 pm', 'idformation' => 104, 'idsociete' => 201, 'training' => 'FORKLIFT, JLG SCISSOR LIFT, AERAL PLATFORM', 'company' => 'US EMBASSY'],
];
$training = 'ALUCAM';
$companies = array_count_values(array_column($arr, 'company'))[$training]; // outputs: 2

Related

How to treat rows with certain common columns as same row when iterating the result?

My query result returns an array that contains rows with company's structure (group/department/team) as columns and several other data columns, for example:
[
0 => [
group => "g1",
department => "d1",
team => null,
data_col1 => "some_data1",
data_col2 => "some_data2"
],
1 => [
group => "g1",
department => "d1",
team => null,
data_col1 => "some_data3",
data_col2 => "some_data4"
],
2 => [
group => "g1",
department => "d1",
team => "t3",
data_col1 => "some_data5",
data_col2 => "some_data6"
3 => [
group => "g4",
department => "d6",
team => "t11",
data_col1 => "some_data7"
data_col2 => "some_data8"
]
]
I want to "group" the results into an array with common group/department/team so that I can treat all the results with common group/department/team as single row and get the data from them in the same iteration.
The expected structure would be similar to:
[
0 => [
group => "g1",
department => "d1",
team => null,
data => [
0 => [data_col1 => "some_data1", data_col2 => "some_data2"],
1 => [data_col1 => "some_data3", data_col2 => "some_data4"]
],
1 => [
group => "g1",
department => "d1",
team => "t3",
data => [
0 => [data_col1 => "some_data5", data_col2 => "some_data6"]
],
2 => [
group => "g4",
department => "d6",
team => "t11",
data => [
0 => [data_col1 => "some_data7", data_col2 => "some_data8"]
]
]
How can I convert the original array to the desired one, or it's not efficient to do that? (i.e. to perform some demanding task to restructure the array). What would be the best thing to treat the rows with common group/department/team as the same row?
This is a requirement to summarize individual records into groups. The data needs to be reformatted in each group.
Method: By using a 'read ahead' technique we can remove the need to test each record to find out whether to add it to the current group or start a new group. The data is already ordered by team. We just need to iterate over each complete group in turn and append to the output.
The main point about the 'read ahead' technique is that read the next record immediately after you have processed it. This normally means that you cannot use a foreach loop as you often need to read a record before the end of the loop.
The code provided has O(n) runtime.
Note the code is commented to try and make it clear what is happening. The code is very basic PHP. It should work on all versions 5.6 and above I have tested it on 8.1.12.
Test: https://onlinephp.io/c/f8e59
<?php
$inputList = loadTestData();
print_r($inputList);
// -------------------------------------------------
// Output - Team data by: Group - Department - Team
// -------------------------------------------------
$outputList = array();
// -------------------------------------------------------------------------
// State
//
// $currentTeamKey: This is the full key of the current team being processed
// ------------------------------------------------------------------------- */
$groupTeamKey = [];
// The current team record being processed
$currentTeamDetails = null;
// -------------------------------------------------
// o Initialize Iterator
// o Read Ahead
// ------------------------------------------------- */
reset($inputList);
$currentTeamDetails = current($inputList); // always have a record to process
// -------------------------------------------------
// Control
// -------------------------------------------------
while ($currentTeamDetails !== false) { // end of input?
// Process ONE Team completely -- there must be at least one record in the group and it is ready to process
$groupTeamKey = getTeamKey($currentTeamDetails);
$groupTeamData = [];
// accumulate all the team data for the current team
while ( $currentTeamDetails !== false // end of input
&& $groupTeamKey === getTeamKey($currentTeamDetails)) {
// Do not need to know the names of the data columns or the number of them.
$groupTeamData[] = getTeamDataRow($currentTeamDetails, $groupTeamKey);
$currentTeamDetails = next($inputList); // read ahead
}
// add the team data to the current key and append to the output
$groupTeamKey['data'] = $groupTeamData;
$outputList[] = $groupTeamKey;
}
// -------------------------------------------------
print_r($outputList);
exit;
// -------------------------------------------------
// subroutines
// -------------------------------------------------
// -------------------------------------------------
// Team Full Key: <group> <dept> <team>
//
function getTeamKey(array $teamDetails = null)
{
return [ 'group' => $teamDetails['group'],
'department' => $teamDetails['department'],
'team' => $teamDetails['team'],
];
}
// -------------------------------------------------
// Team Data as 1 row
// Assume you don't know the data column names but you do know the keys!
//
function getTeamDataRow(array $teamDetails, array $currentTeamKey)
{
$allDataRows = array_diff_assoc($teamDetails, $currentTeamKey);
$dataRow = [];
foreach ($allDataRows as $key => $value) {
$dataRow[$key] = $value;
}
return $dataRow;
}
//-------------------
// Test Data
//
function loadTestData()
{
return [
0 => [
'group' => "g1",
'department' => "d1",
'team' => null,
'data_col1' => "some_data1",
'data_col2' => "some_data2",
],
1 => [
'group' => "g1",
'department' => "d1",
'team' => null,
'data_col1' => "some_data3",
'data_col2' => "some_data4",
],
2 => [
'group' => "g1",
'department' => "d1",
'team' => "t3",
'data_col1' => "some_data5",
'data_col2' => "some_data6",
],
3 => [
'group' => "g4",
'department' => "d6",
'team' => "t11",
'data_col1' => "some_data7",
'data_col2' => "some_data8",
],
];
}

Convert flat array to multidimensional array with dynamic keys. Is it possible?

First of all, I have following data returned from database. I will have two different data as below respectively
sum1
count1
sm__state_name__
om__order_date__year
om__order_date__quarter
om__order_date__month
5645000
4
Luanda
2017
3
8
213985939.8600001
1606
Luanda
2017
3
9
7729331.52
119
Benguela
2017
3
9
1012936
17
Zaire
2017
3
9
1054883
19
Bie
2017
3
9
2347944
26
Cuando Cubango
2017
3
9
428769.6000000001
60
Bengo
2017
3
9
6444569
86
Huila
2017
3
9
4914030
25
Cunane
2017
3
9
1167200
26
Cuanza North
2017
3
9
750080
10
Cuanza Sul
2017
3
9
2178100
6
Huambo
2017
3
9
1099934
25
Lunda North
2017
3
9
410135
12
Malange
2017
3
9
In array format
array (
0 =>
array (
'sum1' => '5645000',
'count1' => '4',
'sm__state_name__' => 'Luanda',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '8',
),
1 =>
array (
'sum1' => '213985939.8600001',
'count1' => '1606',
'sm__state_name__' => 'Luanda',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
2 =>
array (
'sum1' => '352839.60000000003',
'count1' => '9',
'sm__state_name__' => NULL,
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
3 =>
array (
'sum1' => '7729331.52',
'count1' => '119',
'sm__state_name__' => 'Benguela',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
4 =>
array (
'sum1' => '1012936',
'count1' => '17',
'sm__state_name__' => 'Zaire',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
5 =>
array (
'sum1' => '1054883',
'count1' => '19',
'sm__state_name__' => 'Bie',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
6 =>
array (
'sum1' => '2347944',
'count1' => '26',
'sm__state_name__' => 'Cuando Cubango',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
7 =>
array (
'sum1' => '428769.6000000001',
'count1' => '60',
'sm__state_name__' => 'Bengo',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
8 =>
array (
'sum1' => '6444569',
'count1' => '86',
'sm__state_name__' => 'Huila',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
9 =>
array (
'sum1' => '4914030',
'count1' => '25',
'sm__state_name__' => 'Cunane',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
10 =>
array (
'sum1' => '1167200',
'count1' => '26',
'sm__state_name__' => 'Cuanza North',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
11 =>
array (
'sum1' => '750080',
'count1' => '10',
'sm__state_name__' => 'Cuanza Sul',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
12 =>
array (
'sum1' => '2178100',
'count1' => '6',
'sm__state_name__' => 'Huambo',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
13 =>
array (
'sum1' => '1099934',
'count1' => '25',
'sm__state_name__' => 'Lunda North',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
14 =>
array (
'sum1' => '410135',
'count1' => '12',
'sm__state_name__' => 'Malange',
'om__order_date__year' => '2017',
'om__order_date__quarter' => '3',
'om__order_date__month' => '9',
),
)
Array
(
"sm__state_name__",
"om__order_date__year",
"om__order_date__quarter",
"om__order_date__month",
)
ABOBE ARRAY INCLUDES CAN BE ANY NUMBER OF FIELDS
Below is sample output of What I want in return
{
"data": [
{
"key": "Luanda",
"items": [
{
"key": 2017,
"items": [
{
"key": 3,
"items": [
{
"key": 8,
"items": null,
"count": 4,
"summary": [
438380.9935
]
},
{
"key": 9,
"items": null,
"count": 1606,
"summary": [
438380.9935
]
},
],
"summary": [
1285085.9636
]
}
],
"summary": [
1285085.9636
]
}
],
"summary": [
1285085.9636
]
},
{
"key": "Benguela",
"items": [
{
"key": 2017,
"items": [
{
"key": 3,
"items": [
{
"key": 9,
"items": null,
"count": 679,
"summary": [
4781987.8575
]
},
],
"summary": [
15017212.0305
]
}
],
"summary": [
15017212.0305
]
}
],
"summary": [
15017212.0305
]
},
{...},
{...},
{...},
],
"totalCount": 22854
}
Don't mind the summary value. I just put dummy values there.
Is this kind of process even possible? Because I think of many different things, recursion, multiple loops, triple loop but couldn't think of way this could work.
I know it's not an issue or bug. Sorry for that. But it would be great if someone could point me to right direction.
With a variable array of columns you need to group in hierarchical order, you'll certainly want a recursive solution to this problem. For each step in your recursive calls, check to see if a particular grouping level exists yet, and if not, then initialize it. Group using associative arrays for your items, then convert to flat arrays after. It's very simple conceptually, although perhaps a little confusing to look at:
function aggregateData($data, $db_row, $columns, $first_column = true) {
// Base case: with no more columns left, we just take the sum and return.
if(empty($columns)) {
$data['summary'] += $db_row['sum1'];
return $data;
}
$column = array_shift($columns);
$value = $db_row[$column];
if($first_column) {
// First column is a special case. We don't add anything here because every level's summary is the sum of its nested items.
if(!array_key_exists($value, $data)) {
$data[$value] = [
'key'=>$value,
'items'=>empty($columns) ? null : [],
'summary'=>0
];
}
$data[$value] = aggregateData($data[$value], $db_row, $columns, false);
} else {
// For all other columns, we add the sum to each nested level.
if(!array_key_exists($value, $data['items'])) {
$data['items'][$value] = [
'key'=>$value,
'items'=>empty($columns) ? null : [],
'summary'=>0
];
}
$data['summary'] += $db_row['sum1'];
$data['items'][$value] = aggregateData($data['items'][$value], $db_row, $columns, false);
}
return $data;
}
function flattenData($data) {
foreach($data as $key=>$value) {
if(is_null($value['items'])) {
break;
}
$data[$key]['items'] = flattenData($value['items']);
}
return array_values($data);
}
$db_rows = /* your DB retrieval code here */;
$columns = /* columns to group by in hierarchical order */;
$data = [];
foreach($db_rows as $db_row) {
$data = aggregateData($data, $db_row, $columns);
}
$data = flattenData($data);
To help understand what's going on, consider the top-most level, grouping by state. After the aggregateData() calls, before flattening the arrays, it will produce a structure that looks like the following:
{
"Luanda": {
"key": "Luanda",
"items": {...},
"summary": ...,
},
"Benguela": {
"key": "Benguela",
"items": {...},
"summary": ...,
}
}
Notice that because each entry is associated with its key in an object, instead of an index in an array, this allows for easy lookups so we can aggregate information at each level. After flattening, we instead get this:
[
{
"key": "Luanda",
"items": [...],
"summary": ...,
},
{
"key": "Benguela",
"items": [...],
"summary": ...,
}
]
Each entry is no longer associated with its key, instead being the desired flat array. We lose the ability to do simple lookups, but we no longer need that capability at the end of our calculations.
The above doesn't solve the entirety of your problem as there are points of data not being included in this result, but as stackoverflow is not a free coding service and you have not provided any of your own code, I will be leaving the necessary modifications as an exercise. This should, however, remove the bulk of the work required and serve as a strong starting point for your solution.
Ok, so basically if I understand this correctly, you have a database with a bunch of records. Then you want to create some massive JSON based off the database that will have 4 dimensions based on the columns for the state, year, quarter, and month. Then the outer-most array will contain the records of the database grouped by the state, then the items for any specific state will further constrict into a set of items based on the key for the year, etc.
I'm thinking the most efficient way to do this is a single loop where you'd pull all the records in the database then loop through it in PHP and construct some new arrays based on the current record iteration which can be referred to later for look-up purposes.
Begin by setting:
$dataItems = [];
This would be used for storing and structuring data for easily look up and calculations.
Iterate through the list. The first item would be:
sum1
count1
state_name
order_year
order_quarter
order_month
5645000
4
Luanda
2017
3
8
Then you'd run code for the iteration that would look something like this to help populate the array or arrays you'd be constructing:
if (empty($dataItems['Luanda'])) {
$dataItems['Luanda'] = [];
}
if (empty($dataItems['Luanda'][2017])) {
$dataItems['Luanda'][2017] = [];
}
if (empty($dataItems['Luanda'][2017][3])) {
$dataItems['Luanda'][2017][3] = [];
}
if (empty($dataItems['Luanda'][2017][3][8])) {
$dataItems['Luanda'][2017][3][8] = [];
}
$dataItems['Luanda'][2017][3][8][] = ['sum1' => 5645000, 'count1' => 4];
The second item would be:
sum1
count1
state_name
order_year
order_quarter
order_month
213985939.8600001
1606
Luanda
2017
3
9
The PHP code for this iteration would look like this:
if (empty($dataItems['Luanda'])) {
$dataItems['Luanda'] = [];
}
if (empty($dataItems['Luanda'][2017])) {
$dataItems['Luanda'][2017] = [];
}
if (empty($dataItems['Luanda'][2017][3])) {
$dataItems['Luanda'][2017][3] = [];
}
if (empty($dataItems['Luanda'][2017][3][9])) {
$dataItems['Luanda'][2017][3][9] = [];
}
$dataItems['Luanda'][2017][3][9][] = ['sum1' => 213985939.8600001, 'count1' => 1606];
Etc.
Then you'd loop through the $dataItems structure once it's built and do stylizing logic like having a key called "items", etc and create your desired output structure, then finally output in JSON format by using json_encode.
If you need something like the count for the entire year, in your initial loop, you can write to a separate array to help keep track of it, adding it together along the way, then refer to it later when creating an array with the desired output structure. Things like summary you can easily add into the loop and keep track of that along the way.
Sounds like some fun, but yeah you'd only need one loop to create a lookUp or multiple lookUp sort of arrays then a second loop to refer to the loopUp array(s) to get your data in the desired output format.

How to merge two different array PHP

I would need to combine two different fields.
In the first field I generate days of the month. I want to list all days of the month.
I would like to add a second field to them, where there are items for each day. But, for example, there are no items on weekends or on other days. Ie. that field two will always have fewer items.
The second field is tightened from the DB.
I would need to do a JOIN like in MySQL for the first field.
It occurred to me that in MySQL it would be possible to make a temporary table with a given month and link it here, but I don't think it's right.
$arrayDate = [0 => '20210401',1 => '20210402',2 => '20210403',3 => '20210404',4 => '20210405',5 => '20210406',6 => '20210407',7 => '20210408',8 => '20210409',9 => '20210410',10 => '20210411',11 => '20210412',12 => '20210413',13 => '20210414',14 => '20210415',15 => '20210416',16 => '20210417',17 => '20210418',18 => '20210419',19 => '20210420',20 => '20210421',21 => '20210422',22 => '20210423',23 => '20210424',24 => '20210425',25 => '20210426',26 => '20210427',27 => '20210428',28 => '20210429',29 => '20210430'];
$arrayItem[35] = ['id' => 35, 'date' => '20210401', 'item' => 'aaaa'];
$arrayItem[36] = ['id' => 36, 'date' => '20210402', 'item' => 'bbbb'];
$arrayItem[37] = ['id' => 36, 'date' => '20210430', 'item' => 'cccc'];
// i need output
20210401 - aaaa
20210402 - bbbb
20210403 - empty
20210404 - empty
...
20210430 - cccc
EDIT: I use nested loops, but I still can't get the right output
foreach ($arrayDate as $date) {
foreach ($arrayItem as $item) {
if ($date == $item['date']) {
bdump($item['date']);
} else {
bdump($date);
}
}
}
bdump($item['date']) = '20210401', '20210402', '20210430'
bdump($date) = '20210401', '20210401', '20210402', '20210402', '20210403', '20210403', '20210403', '20210404', '20210404', '20210404', '20210405', '20210405', '20210405' ....
With array_column you create a array from $arrayItem with date as key.
$dateItem is an array like
array (
20210401 => "aaaa",
20210402 => "bbbb",
20210430 => "cccc",
)
The output you can do with a simple foreach.
$dateItem = array_column($arrayItem,'item','date');
foreach($arrayDate as $date){
echo $date.' '.($dateItem[$date] ?? 'empty')."<br>\n";
}
Note:
With
array_column($arrayItem,null,'date')
you get a two-dimensional array with a date as a key that can be used.
array (
20210401 =>
array (
'id' => 35,
'date' => "20210401",
'item' => "aaaa",
),
20210402 =>
array (
'id' => 36,
'date' => "20210402",
'item' => "bbbb",
),
20210430 =>
array (
'id' => 36,
'date' => "20210430",
'item' => "cccc",
),
)

How to partition combinations from three categories?

I have an array with recipes from 3 categories, breakfast, lunch and dinner. Each of these categories, have 10 unique recipes.
$recipes = [
'breakfast' => [
0 => [
'title' => 'eggless waffles',
'calorie' => 210,
],
1 => [
'title' => 'blueberry oatmeal',
'calorie' => 161,
],
...
],
'lunch' => [9],
'dinner' => [9]
];
I'd like to sort and create a combination of 3 recipes for each day
$days = array_fill(0, 6, [1 => [], 2 => [], 3 => []]);
Each recipe has a calorie amount, and each final day should have a combination (consists of 1 breakfast, 1 lunch and 1 dinner) with recipes that was ordered by whichever 3 recipe combo hit closest to 500
For example, if day 1 combined recipes (breakfast, lunch and dinner) calorie totaled 660, and day 2 was 400. It's possible that switching breakfast from day 2, to day 1 might make both of them hit closest to 500, however it's possible that switching day 3 breakfast to day 1, and day 2 to day 3 might make all 3 hit closer to 500 as well.
So day 1, 2, 3, 4, 5, 6, and 7 should have 3 recipes (breakfast, lunch and dinner)
$final = [
0 => [
'breakfast' => [...],
'lunch' => [...],
'dinner' => [...],
],
1 => [
'breakfast' => [...],
'lunch' => [...],
'dinner' => [...],
],
2 => [
'breakfast' => [...],
'lunch' => [...],
'dinner' => [...],
],
...
];
It's been days since I've reached an impasse, and I cannot figure out how to go about sorting these arrays into a combination of 3 for each day. (I know I'm not providing a lot of code to go off of)
Edit 1:
This is what I've got so far:
class Combinations {
private $days;
public function __construct(){
$this->days = array_fill(1, 7, [1 => [], 2 => [], 3 => []]);
}
public function create(){
$median = 600;
foreach($this->days as $day => $categories){
while($this->dayIsIncomplete($day)){
$recipes = [];
foreach($categories as $category => $value){
$recipes[$category] = $this->getRandomRecipe($category);
}
// add random meals to first day
if($day === 1){
$this->days[$day] = $recipes;
continue;
}
foreach($recipes as $category => $recipe){
foreach($this->days as $dayKey => $mealsArray){
$originalMacros = $this->totalMacros($mealsArray);
// remove $recipe category from mealsArray, and merge it ($recipe)
$filteredMacros = $this->totalMacros(array_merge([$recipe], array_filter($mealsArray, function($key) use($category){
return $key !== $category;
}, ARRAY_FILTER_USE_KEY)));
// if original is not closer to median
if(($originalMacros - $median) * ($originalMacros - $median) < ($filteredMacros - $median) * ($filteredMacros - $median)){
// flip current recipes
// switch D2B ($recipe) with D1B
}
}
}
}
}
}
public function getRandomRecipe(int $category){
$recipes = []
if($category === 1){
$recipes[] = ['id' => 1, 'calorie' => 310];
$recipes[] = ['id' => 2, 'calorie' => 360];
$recipes[] = ['id' => 3, 'calorie' => 450];
$recipes[] = ['id' => 4, 'calorie' => 330];
$recipes[] = ['id' => 5, 'calorie' => 220];
$recipes[] = ['id' => 6, 'calorie' => 390];
$recipes[] = ['id' => 7, 'calorie' => 400];
$recipes[] = ['id' => 8, 'calorie' => 320];
$recipes[] = ['id' => 9, 'calorie' => 460];
}
if($category === 2){
$recipes[] = ['id' => 10, 'calorie' => 420];
$recipes[] = ['id' => 11, 'calorie' => 360];
$recipes[] = ['id' => 12, 'calorie' => 450];
$recipes[] = ['id' => 13, 'calorie' => 310];
$recipes[] = ['id' => 14, 'calorie' => 320];
$recipes[] = ['id' => 15, 'calorie' => 490];
$recipes[] = ['id' => 16, 'calorie' => 440];
$recipes[] = ['id' => 17, 'calorie' => 520];
$recipes[] = ['id' => 18, 'calorie' => 560];
}
if($category === 3){
$recipes[] = ['id' => 19, 'calorie' => 510];
$recipes[] = ['id' => 20, 'calorie' => 660];
$recipes[] = ['id' => 21, 'calorie' => 750];
$recipes[] = ['id' => 22, 'calorie' => 610];
$recipes[] = ['id' => 23, 'calorie' => 580];
$recipes[] = ['id' => 24, 'calorie' => 690];
$recipes[] = ['id' => 25, 'calorie' => 710];
$recipes[] = ['id' => 26, 'calorie' => 620];
$recipes[] = ['id' => 27, 'calorie' => 730];
}
return $recipes[array_rand($recipes)];
}
public function dayIsIncomplete($day){
return !empty($this->days[$day][1]) && !empty($this->days[$day][2]) && !empty($this->days[$day][3]);
}
public function totalMacros($array){
$total = 0;
foreach ($array as $key => $value) {
$total += $value['calorie'];
}
return $total / 2;
}
}
Edit 2:
I'm trying to figure out what algorithm best fits to sort this issue out. I think using a bipartite matching (maximum) algorithm might be what I need.
Edit 3:
Thank you all for taking the time to help, I haven't forgotten about the answers. I had to put this aside for the time being, however soon enough I'll get to it, and the accepted answer will get my remaining 300 bounty.
So I tested a genetic algorithm and it works. I used Jenetics, a Java library (it's not PHP, sorry, but PHP is not suited to heavy computations anyway).
I took 1400 calories as the daily target.
The function to be minimized is the mean squared error.
Here's the code:
import java.util.ArrayList;
import io.jenetics.*;
import io.jenetics.engine.*;
import io.jenetics.util.*;
public class Recipes
{
private static final int TARGET = 1400;
private static final int DAYS = 7;
private static class Recipe
{
public int id;
public int calories;
public Recipe(int id, int calories)
{
this.id = id;
this.calories = calories;
}
}
private static ISeq<Recipe> getSeq(int[] ids, int[] calories)
{
ArrayList<Recipe> list = new ArrayList<>();
for(int i=0;i<ids.length;i++)
list.add(new Recipe(ids[i], calories[i]));
return ISeq.of(list);
}
private static double meanSquareError(Genotype<EnumGene<Recipe>> gt)
{
int err = 0;
for(int d=0;d<DAYS;d++)
{
int calories = 0;
for(int m=0;m<3;m++)
calories += gt.get(m).get(d).allele().calories;
err += (calories-TARGET)*(calories-TARGET);
}
return err / (double)DAYS;
}
public static void main(String[] args)
{
ISeq<Recipe> recipes1 = getSeq(new int[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9}, new int[]{310, 360, 450, 330, 220, 390, 400, 320, 460});
ISeq<Recipe> recipes2 = getSeq(new int[]{10, 11, 12, 13, 14, 15, 16, 17, 18}, new int[]{420, 360, 450, 310, 320, 490, 440, 520, 560});
ISeq<Recipe> recipes3 = getSeq(new int[]{19, 20, 21, 22, 23, 24, 25, 26, 27}, new int[]{510, 660, 750, 610, 580, 690, 710, 620, 730});
Factory<Genotype<EnumGene<Recipe>>> gtf = Genotype.of(
PermutationChromosome.of(recipes1, DAYS),
PermutationChromosome.of(recipes2, DAYS),
PermutationChromosome.of(recipes3, DAYS)
);
Engine<EnumGene<Recipe>, Double> engine = Engine
.builder(Recipes::meanSquareError, gtf)
.optimize(Optimize.MINIMUM)
.populationSize(50)
.alterers(new SwapMutator<>(0.2), new PartiallyMatchedCrossover<>(0.2), new Mutator<>(0.01))
.build();
Phenotype<EnumGene<Recipe>, Double> result = engine.stream()
.limit(20000)
.collect(EvolutionResult.toBestPhenotype());
for(int m=0;m<3;m++)
{
for(int d=0;d<DAYS;d++)
{
Recipe r = result.genotype().get(m).get(d).allele();
System.out.print(String.format("%2d (%d) ", r.id, r.calories));
}
System.out.println();
}
System.out.println("MSE = " + result.fitness());
}
}
A genetic algorithm is non-deterministic so it gives a different result each time. The best solution I could get is:
3 (450) 4 (330) 5 (220) 2 (360) 7 (400) 1 (310) 8 (320)
16 (440) 15 (490) 17 (520) 10 (420) 13 (310) 11 (360) 14 (320)
19 (510) 23 (580) 20 (660) 26 (620) 24 (690) 27 (730) 21 (750)
MSE = 14.285714
It's almost perfect (all days are at 1400 calories except Sunday which has 1390).
You have:
10 breakfasts
10 lunches
10 dinners
The combinations of these are 10x10x10 = 1000 recipes.
Part 1
Calculate these recipes and their total calories each.
From each recipes `s total calories, calculate the absolute difference from the daily calories goal:
AbsoluteDifference = Abs(calories - 500)
and which breakfast, lunch and dinner it consists of.
So now e.g. you have a list:
| Recipes | AbsDiff | Breakfast | Lunch | Dinner |
| recipe 1 | 140 | 1 | 7 | 4
| recipe 2 | 135 | 4 | 8 | 3
| recipe 3 | 210 | 7 | 9 | 10
...
| recipe 1000 | 170 | 5 | 1 | 9
Part 2
This is the difficult part, by calculating all combinations of 7 recipes is gonna take too long.
The best combination is the one that has total of absDiff of its recipes the minimum:
MIN(AbsDiff(recipe1) + AbsDiff(recipe2) + AbsDiff(recipe7))
Algorithm
The idea is to calculate only a few combinations of 7 different recipes.
Initial Step
Make a guess of "how much the minimum could be about", e.g. say you think it is likely less than 350 calories.
Using this guess, you can try to calculate all the combinations of 7 recipes that have TotalCaloriesDiff < 350.
Based on:
In a collection of n positive numbers that sum up to or less than S, at least one of them will be less than S divided by n (S/n).
In this case S=350 and n=7, then at least one recipe should have AbsDiff < 350/7 = 50.
So, you could try to calculate combinations of 7 recipes with lower total differences to the guess.
Steps
Get the recipes with AbsDiff(recipe1) < 350 / 7
For each recipe1 found above, get the next recipes that have AbsDiff(recipe2) < (350 - AbsDiff(recipe1)) / 6 and they do not share any breakfast, lunch or dinner with recipe1.
continue till you get combinations of 7 recipes.
select the combination with lowest TotalCaloriesDiff
If you do not find any results, based on your guess, you raise the guess, e.g. 350 + 50 = 400.
Here is my answer to a similar problem.
I think first you should make a combination of dishes, where calorie should be near to 500, to make them unice, like:
$arr = [
0 => [
'breakfast' => 160
'lunch' => 160
'dinner' => 180
],
...
You should rebuild array like $breakfast = ['1' => 130, '2' => 150, '3' => 160, '4' => 170, '5' => 120, '6' => 100, '7' => 130] and etc.
Maybe try to compare arrays like
$final = ['1' => 152, '2' => 235, '3' => 521, '4' => 343, ... ];
And then you can grab each value from $arr ->
$final = ['1' => ['breakfast' => '1', 'lunch' => '5', 'dinner' => '2'], ...];
I think you can modify this logic as you want. Best of luck

Keep array rows where a column value is found in a second flat array

** I have edited this to show how I got my code to work using array_search
I have an array, $arr1 with 5 columns as such:
key id name style age whim
0 14 bob big 33 no
1 72 jill big 22 yes
2 39 sue yes 111 yes
3 994 lucy small 23 no
4 15 sis med 24 no
5 16 maj med 87 yes
6 879 Ike larg 56 no
7 286 Jed big 23 yes
This array is in a cache, not a database.
I then have a second array with a list of id values -
$arr2 = array(0=>14, 1=>72, 2=>8790)
How do I filter $arr1 so it returns only the rows with the id values in $arr2?
I got my code to work as follows:
$arr1 = new CachedStuff(); // get cache
$resultingArray = []; // create an empty array to hold rows
$filter_function = function ($row) use ($arr2) {
return (array_search($row['id'], $arr2));
};
$resultingArrayIDs = $arr1->GetIds($filter_function, $resultingArray);
This gives me two outputs: $resultingArray & $resultingArrayIDs both of which represent the intersection of the $arr1 and $arr2.
This whole task can be accomplished with just one slick, native function call -- array_uintersect().
Because the two compared parameters in the custom callback may come either input array, try to access from the id column and if there isn't one declered, then fallback to the parameter's value.
Under the hood, this function performs sorting while evaluating as a means to improve execution time / processing speed. I expect this approach to outperform iterated calls of in_array() purely from a point of minimized function calls.
Code: (Demo)
var_export(
array_uintersect(
$arr1,
$arr2,
fn($a, $b) =>
($a['id'] ?? $a)
<=>
($b['id'] ?? $b)
)
);
Something like this should do it, provided I've understood your question and data structure correctly:
$dataArray = [
[ 'key' => 0, 'id' => 14 , 'name' => 'bob' , 'style' => 'big' , 'age' => 33 , 'whim' => 'no' ],
[ 'key' => 1, 'id' => 72 , 'name' => 'jill' , 'style' => 'big' , 'age' => 22 , 'whim' => 'yes' ],
[ 'key' => 2, 'id' => 39 , 'name' => 'sue' , 'style' => 'yes' , 'age' => 111 , 'whim' => 'yes' ],
[ 'key' => 3, 'id' => 994 , 'name' => 'lucy' , 'style' => 'small' , 'age' => 23 , 'whim' => 'no' ],
[ 'key' => 4, 'id' => 15 , 'name' => 'sis' , 'style' => 'med' , 'age' => 24 , 'whim' => 'no' ],
[ 'key' => 5, 'id' => 16 , 'name' => 'maj' , 'style' => 'med' , 'age' => 87 , 'whim' => 'yes' ],
[ 'key' => 6, 'id' => 879 , 'name' => 'Ike' , 'style' => 'larg' , 'age' => 56 , 'whim' => 'no' ],
[ 'key' => 7, 'id' => 286 , 'name' => 'Jed' , 'style' => 'big' , 'age' => 23 , 'whim' => 'yes' ]
];
$filterArray = [14, 72, 879];
$resultArray = array_filter( $dataArray, function( $row ) use ( $filterArray ) {
return in_array( $row[ 'id' ], $filterArray );
} );
View this example on eval.in
However, your question appears to suggest this data might be coming from a database; is that correct? If so, perhaps it's more efficient to pre-filter the results at the database-level. Either by adding a field in the SELECT query, that represents a boolean value whether a row matched your filter ids, or by simply not returning the other rows at all.
One way is with foreach loop with array_search()
$result = [];
foreach ($arr1 as $value) { // Loop thru $arr1
if (array_search($value['id'], $arr2) !== false) { // Check if id is in $arr2
$result[] = $value; // Push to result if true
}
}
// print result
print_r($result);
As #DecentDabbler mentioned - if the data is coming out of a database, using an IN on your WHERE will allow you to retrieve only the relevant data.
Another way to filter is to use array functions
array_column extracts the value of the id column into an array
array_intersect returns the elements which are in both $arr1['id'] and $arr2
array_flip flips the resulting array such that the indices into $arr1 indicate the elements in both $arr1 and $arr2
$arr1 = [ [ 'id' => 14, 'name' => 'bob'],
['id' => 72, 'name' => 'jill'],
['id' => 39, 'name' => 'sue'],
['id' => 994, 'name' => 'lucy'],
['id' => 879, 'name'=> 'large']];
$arr2 = [ 14,72,879 ];
$intersection = array_flip(array_intersect(array_column($arr1,'id'),$arr2));
foreach ($intersection as $i) {
var_dump($arr1[$i]);;
}

Categories