salvob
salvob

Reputation: 121

Group objects in Laravel collection by consecutive identical column values

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

Answers (3)

afcyy
afcyy

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

mickmackusa
mickmackusa

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

Gordon Freeman
Gordon Freeman

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

Related Questions