Масиви: видалення дублікатів по декількох полях

Масиви: видалення дублікатів по декількох полях

У поточному проекті проекті зіштовхнувся з цікавим завданням - потрібно зробити асоціативний масив унікальним по декількох полях. Стандартна функція 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']);

 

Звісно, ніхто не заважає функцію перетворити в метод якогось класу-хелпера.

На цьому все. Успіхів!