Reputation: 121
I need some help with a grouping algorithm. I have some objects in a Laravel collection, here are some sample data in JSON representation:
[
{
"group": "WHITE",
"name": "John Doe",
"sequence": 1
},
{
"group": "WHITE",
"name": "John Doe Jr",
"sequence": 2
},
{
"group": "BLUE",
"name": "John Doe Sr",
"sequence": 3
},
{
"group": "BLUE",
"name": "John Doe Again",
"sequence": 4
},
{
"group": "RED",
"name": "Mr John Doe",
"sequence": 5
},
{
"group": "RED",
"name": "Ms Joahnna Doe",
"sequence": 6
},
{
"group": "BLUE",
"name": "Dr Johnny Doe",
"sequence": 7
},
{
"group": "RED",
"name": "Sir John Doe",
"sequence": 8
},
{
"group": "RED",
"name": "Sir John Doe Senior",
"sequence": 9
},
{
"group": "WHITE",
"name": "Ms John Doe",
"sequence": 10
}
]
I'd like to be able to grouping theese objects by their group name BUT keeping the sequence unaltered, and repeating keys where occurs twice or more, like this:
{
"WHITE": [
{ "name": "John Doe", "sequence": 1 },
{ "name": "John Doe Jr", "sequence": 2 }
],
"BLUE": [
{ "name": "John Doe Sr", "sequence": 3 },
{ "name": "John Doe Again", "sequence": 4 }
],
"RED": [
{ "name": "Mr John Doe", "sequence": 5 },
{ "name": "Ms Joahnna Doe", "sequence": 6 }
],
"BLUE (second group)": [
{ "name": "Dr Johnny Doe", "sequence": 7 }
],
"RED (second group)": [
{ "name": "Sir John Doe", "sequence": 8 },
{ "name": "Sir John Doe Senior", "sequence": 9 }
],
"WHITE (second group)": [
{ "name": "Ms John Doe", "sequence": 10 }
]
}
I've already found a way doing like so:
I iterate every object looking for occurrences in the previous ones where the sequence number is not consecutive, and I add a new group_name
key to the objects alternating this name when the previous condition is satisfied (i.e. adding a count of the splits that I've found). As far as I can remember, pushing every occurrence in the original collection let me use a simple $collection->groupBy('group_name')
foreach ($data as $stop) {
$previous_stops_in_group = $stops
->where('group', $stop->group)
$stop->group_name = $stop->group;
$stop->splitted = false;
if ($previous_stops_in_group->count() >0) {
$last_sequence_in_group = $previous_stops_in_group->last()->sequence;
if (($stop->sequence - $last_sequence_in_group) > 1) {
$splittedroutes++;
$stop->group_name = $stop->group . $splittedroutes;
$stop->splitted = true;
}
}
$last_splitted_group = $stops
->where('splitted',true)
->where('stop_group',$stop->stop_group)
->where('direction', $stop->direction)->last();
if (!empty($last_splitted_group) && isset($last_splitted_group->group_name)) {
$stop->group_name = $last_splitted_group->group_name;
}
$stops->push($stop);
}
Unfortunally I've wrote this masterpiece about 1 year ago, and now I can't figure out what is going on because this algorithm should be applied to hundreds of records and it doesn't work as intended.
I'd like to find a more intuitive solution, using an expressive syntax, maybe with collection's native methods, maybe a combination of partition()
and mergeRecursive()
?
Upvotes: 0
Views: 229
Reputation: 111
collect($data)->reduce(function ($carry, $item) {
$count = count($carry[$item["group"]] ?? []);
$groupName = $item["group"] . ($count > 1 ? " (group {$count})" : "");
$carry[$groupName][] = array_diff_key($item, array_flip(["group"]));
return $carry;
}, []);
Upvotes: 0
Reputation: 48031
Use reduce()
(or cast to an array with ->toArray()
and use PHP's native array_reduce()
) to iterate the objects in your collection. Use a few static
variables to assist with identifying changes in grouping values and to increment each group's counter.
Code: (PHPize Demo)
var_export(
$collection->reduce(
function ($result, $obj) {
static $previousGroup,
$groupKey,
$keyCounters = [];
$group = $obj->group;
unset($obj->group);
$keyCounters[$group] ??= 0;
if (!$previousGroup || $previousGroup !== "$group " . $keyCounters[$group]) {
$groupKey = "$group " . (++$keyCounters[$group]);
$previousGroup = $groupKey;
}
$result[$groupKey][] = $obj;
return $result;
}
)
);
Output:
array (
'WHITE 1' =>
array (
0 =>
(object) array(
'name' => 'John Doe',
'sequence' => 1,
),
1 =>
(object) array(
'name' => 'John Doe Jr',
'sequence' => 2,
),
),
'BLUE 1' =>
array (
0 =>
(object) array(
'name' => 'John Doe Sr',
'sequence' => 3,
),
1 =>
(object) array(
'name' => 'John Doe Again',
'sequence' => 4,
),
),
'RED 1' =>
array (
0 =>
(object) array(
'name' => 'Mr John Doe',
'sequence' => 5,
),
1 =>
(object) array(
'name' => 'Ms Joahnna Doe',
'sequence' => 6,
),
),
'BLUE 2' =>
array (
0 =>
(object) array(
'name' => 'Dr Johnny Doe',
'sequence' => 7,
),
),
'RED 2' =>
array (
0 =>
(object) array(
'name' => 'Sir John Doe',
'sequence' => 8,
),
1 =>
(object) array(
'name' => 'Sir John Doe Senior',
'sequence' => 9,
),
),
'WHITE 2' =>
array (
0 =>
(object) array(
'name' => 'Ms John Doe',
'sequence' => 10,
),
),
)
Upvotes: 0
Reputation: 3421
I don't know if it can be done better with collections, but here is my approach:
$items = [
{
"group": "WHITE",
"name": "John Doe",
"sequence": 1
},
// ...
{
"group": "WHITE",
"name": "Ms John Doe",
"sequence": 10
}
];
$result = [];
foreach ($items as $item) {
if (!isset($result[$item['group']])) $result[$item['group']] = [];
$result[$item['group']][] = [
'name' => $item['item'],
'sequence' => $item['sequence'],
];
}
Upvotes: 0