
Масиви: видалення дублікатів по декількох полях
06.05.2019 22:01 | PHP
У поточному проекті проекті зіштовхнувся з цікавим завданням - потрібно зробити асоціативний масив унікальним по декількох полях. Стандартна функція array_unique
в даному випадку не допоможе, подивимося на одне з можливих рішень.
Припустимо, є вихідний масив:
$data = [
['id' => 1, 'title' => 'AAA', 'section' => 1, 'tags' => 'a, b'],
['id' => 2, 'title' => 'AAA', 'section' => 2, 'tags' => 'a, c'],
['id' => 3, 'title' => 'AAA', 'section' => 1, 'tags' => 'a, d'],
['id' => 4, 'title' => 'AAA', 'section' => 1, 'tags' => 'a, b'],
];
Завдання: отримати унікальний масив по комбінації полів title
і section
, при цьому значення інших полів не важливі. Тобто результатом повинен бути:
[
['id' => 1, 'title' => 'AAA', 'section' => 1, 'tags' => 'a, b'],
['id' => 2, 'title' => 'AAA', 'section' => 2, 'tags' => 'a, c'],
];
// або
[
['id' => 2, 'title' => 'AAA', 'section' => 2, 'tags' => 'a, c'],
['id' => 4, 'title' => 'AAA', 'section' => 1, 'tags' => 'a, b'],
];
// і т.д.
В кінцевому підсумку прийшов до досить простого рішення: робимо новий масив, в якому ключем кожного масиву-елемента буде рядок складений зі значень необхідних полів:
'AAA1' => ...,
'AAA2' => ...,
а значенням - сам елемент-масив. Таким чином, масиви, в яких значення зазначених полів дублюються, будуть просто перезаписувати один одного. Потім же, щоб позбутися від складових ключів, скористаємося функцією array_values
:
$filtered = [];
foreach ($data as $item) {
$filtered[$item['title'].$item['section']] = $item;
}
$result = array_values($filtered);
Вуаля! Необхідний результат отримано - і ніяких вкладених циклів, складність алгоритму лінійна (2N). Але давайте підемо далі і перепишемо код з використанням вбудованих функцій. array_map
також не підійде, оскільки не працює з ключами, але зате є array_reduce
. Хоча в документації і написано "... ітеративно зменшує масив до єдиного значення, використовуючи callback-функцію" - та це не зовсім так і експеримент показав, що array_reduce
можна використовувати так само, як і array.reduce()
в javascript:
$filtered = array_reduce($data, function ($filtered, $item) {
$filtered[$item['title'].$item['section']] = $item;
return $filtered;
});
$result = array_values($filtered);
І все таки є момент, який не подобається - у нас хардкодом прописані поля title
та section
, що само по собі не кошерно і виключає можливість повторного використання. А якщо нам знадобиться видаляти дублікати за іншими полями, скажімо section
і tags
? Або потрібно буде працювати з іншим масивом?
Рішення не повинно залежати ні від якогось конкретного масиву, ні від якихось конкретних полів і їх кількості. Правда, при такому розкладі вкладеного циклу не уникнути. З іншого боку, отримаємо рішення "на всі випадки життя":
function uniqueByFields(array $source, array $fields)
{
$filtered = array_reduce($source, function ($filtered, $item) use ($fields) {
$key = array_reduce($fields, function ($key, $field) use ($item) {
return $key . $item[$field];
});
$filtered[$key] = $item;
return $filtered;
});
return $filtered;
}
Єдине, що змінилося - ми створюємо ключ за переданими полями (той самий вкладений цикл). Приклад використання:
$result = uniqueByFields($data, ['title', 'section']);
Звісно, ніхто не заважає функцію перетворити в метод якогось класу-хелпера.
На цьому все. Успіхів!