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

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

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

 

Естественно, никто не мешает функцию превратить в метод какого-то класса-хелпера. 

На этом всё. Успехов!