#php #arrays
#php #массивы
Вопрос:
Я потратил часы, пытаясь найти ответ на этот вопрос, но я борюсь. Я достаточно хорошо знаком с PHP и различными встроенными функциями и могу создать для этого сложный цикл foreach(), но я подумал, что попрошу посмотреть, есть ли у кого-нибудь более разумное решение моей проблемы.
У меня есть следующий упрощенный пример массива с тремя «строками» (реальный массив обычно намного больше и сложнее, но проблема та же).
$rows[] = [
"widget_id" => "widget1",
"size" => "large",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint1",
"colour" => "red",
]
]
];
# Exactly the same as above, except the "paint" child array is different
$rows[] = [
"widget_id" => "widget1",
"size" => "large",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint2",
"colour" => "green",
]
]
];
# Same children ("item" and "paint") as the first row, but different parents ("widget_id" is different)
$rows[] = [
"widget_id" => "widget2",
"size" => "medium",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint1",
"colour" => "red",
]
]
];
Я пытаюсь получить следующий результат:
[[
"widget_id" => "widget1",
"size" => "large",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [[
"paint_id" => "paint1",
"colour" => "red",
],[
"paint_id" => "paint2",
"colour" => "green",
]]
]
],[
"widget_id" => "widget2",
"size" => "medium",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint1",
"colour" => "red",
]
]
]]
В принципе, когда две строки имеют один и тот же ключ и значения, объедините их. Если ключ тот же, но значение другое, сохраните оба значения и поместите их в числовой массив под ключом (вроде как, как array_merge_recursive
это делается).
Проблема в том, что значения сами по себе могут быть массивами, и существует неизвестное количество уровней. Есть ли разумный и эффективный способ сделать это, или мне нужно прибегнуть к foreach
циклу с высокой нагрузкой?
Спасибо за просмотр, надеюсь, что есть люди более умные, чем я, читающие это!
Комментарии:
1. Что вы пробовали до сих пор?
2. Если у вас несколько уровней, вам может понадобиться рекурсивная функция
3. @FelippeDuarte Я пишу рекурсивную
foreach()
функцию, потому что подозреваю, что нет никаких «умных» решений (пожалуйста, докажите, что я ошибаюсь!). Я опубликую это, как только закончу с этим. Это будет некрасиво!
Ответ №1:
Я добился получения ожидаемой структуры массива с помощью следующей функции, я надеюсь, что комментарии четко указывают на то, что внутри:
function complex_merge(array $arr): array
{
// Grouped items
$result = [];
$iterationKey = 0;
// Loop through every item
while (($element = array_shift($arr)) !== null) {
// Save scalar values as is
$scalarValues = array_filter($element, 'is_scalar');
// Save array values in an array
$arrayValues = array_map(fn(array $arrVal) => [$arrVal], array_filter($element, 'is_array'));
$arrayValuesKeys = array_keys($arrayValues);
$result[$iterationKey] = array_merge($scalarValues, $arrayValues);
// Compare with remaining items
for ($i = 0; $i < count($arr); $i ) {
$comparisonScalarValues = array_filter($arr[$i], 'is_scalar');
// Scalar values are same, add the array values to the containing arrays
if ($scalarValues === $comparisonScalarValues) {
$comparisonArrayValues = array_filter($arr[$i], 'is_array');
foreach ($arrayValuesKeys as $arrayKey) {
$result[$iterationKey][$arrayKey][] = $comparisonArrayValues[$arrayKey];
}
// Remove matching item
array_splice($arr, $i, 1);
$i--;
}
}
// Merge array values
foreach ($arrayValuesKeys as $arrayKey) {
$result[$iterationKey][$arrayKey] = complex_merge($result[$iterationKey][$arrayKey]);
// array key contains a single item, extract it
if (count($result[$iterationKey][$arrayKey]) === 1) {
$result[$iterationKey][$arrayKey] = $result[$iterationKey][$arrayKey][0];
}
}
// Increment result key
$iterationKey ;
}
return $result;
}
Просто перейдите $rows
к функции, быстрая проверка значений:
echo '<pre>' . print_r(complex_merge($rows), true) . '</pre>';
/*
Displays:
Array
(
[0] => Array
(
[widget_id] => widget1
[size] => large
[item] => Array
(
[item_id] => item1
[shape] => circle
[paint] => Array
(
[0] => Array
(
[paint_id] => paint1
[colour] => red
)
[1] => Array
(
[paint_id] => paint2
[colour] => green
)
)
)
)
[1] => Array
(
[widget_id] => widget2
[size] => medium
[item] => Array
(
[item_id] => item1
[shape] => circle
[paint] => Array
(
[paint_id] => paint1
[colour] => red
)
)
)
)
*/
Комментарии:
1. Он мог бы иметь лучшие имена переменных и, вероятно, мог бы быть оптимизирован, но он работает, и мне было весело это делать 🙂
2. Спасибо за этот отличный пример. Я также опубликовал свою собственную попытку, но я думаю, что ваша лучше.
3. Я провел быстрый тест, мой, похоже, работает быстрее примерно на 0,01 миллисекунды. Но объектно-ориентированный подход может быть проще в использовании 🙂
4. Спасибо. На самом деле я многому научился у вашего подхода, вы использовали некоторые встроенные методы, с которыми я не был знаком (или удобен!). Может быть, я поиграю с гибридной версией, когда у меня будет время, а пока я реализовал вашу версию, поскольку она короче! Еще раз спасибо за вашу помощь.
Ответ №2:
Вот моя собственная попытка. Я думаю, что предпочитаю версию AymDev, хотя она намного более лаконична. Интересно, что быстрее.
class ComplexMerge{
/**
* Checks to see whether an array has sequential numerical keys (only),
* starting from 0 to n, where n is the array count minus one.
*
* @link https://codereview.stackexchange.com/questions/201/is-numeric-array-is-missing/204
*
* @param $arr
*
* @return bool
*/
private static function isNumericArray($arr)
{
if(!is_array($arr)){
return false;
}
return array_keys($arr) === range(0, (count($arr) - 1));
}
/**
* Given an array, separate out
* array values that themselves are arrays
* and those that are not.
*
* @param array $array
*
* @return array[]
*/
private static function separateOutArrayValues(array $array): array
{
$valuesThatAreArrays = [];
$valuesThatAreNotArrays = [];
foreach($array as $key => $val){
if(is_array($val)){
$valuesThatAreArrays[$key] = $val;
} else {
$valuesThatAreNotArrays[$key] = $val;
}
}
return [$valuesThatAreArrays, $valuesThatAreNotArrays];
}
/**
* Groups row keys together that have the same non-array values.
* If every row is already unique, returns NULL.
*
* @param $array
*
* @return array|null
*/
private static function groupRowKeysWithSameNonArrayValues($array): ?array
{
foreach($array as $key => $row){
# Separate out the values that are arrays and those that are not
[$a, $v] = self::separateOutArrayValues($row);
# Serialise the values that are not arrays and create a unique ID from them
$uniqueRowId = md5(serialize($v));
# Store all the original array keys under the unique ID
$deduplicatedArray[$uniqueRowId][] = $key;
}
# If every row is unique, there are no more rows to combine, and our work is done
if(!$a amp;amp; count($array) == count($deduplicatedArray)){
return NULL;
}
return $deduplicatedArray;
}
private static function mergeRows(array $array): array
{
# Get the grouped row keys
if(!$groupedRowKeys = self::groupRowKeysWithSameNonArrayValues($array)){
//If there are no more rows to merge
return $array;
}
foreach($groupedRowKeys as $uniqueRowId => $keys){
foreach($keys as $id => $key){
# Separate out the values that are arrays and those that are not
[$valuesThatAreArrays, $valuesThatAreNotArrays] = self::separateOutArrayValues($array[$key]);
//We're using the key from the grouped row keys array, but using it on the original array
# If this is the first row from the group, throw in the non-array values
if(!$id){
$unique[$uniqueRowId] = $valuesThatAreNotArrays;
}
# For each of the values that are arrays include them back in
foreach($valuesThatAreArrays as $k => $childArray){
$unique[$uniqueRowId][$k][] = $childArray;
//Wrap them in a numerical key array so that only children and siblings are have the same parent-child relationship
}
}
}
# Go deeper
foreach($unique as $key => $val){
foreach($val as $k => $valuesThatAreNotArrays){
if(self::isNumericArray($valuesThatAreNotArrays)){
$unique[$key][$k] = self::mergeRows($unique[$key][$k]);
}
}
}
# No need to include the unique row IDs
return array_values($unique);
}
public static function normalise($array): ?array
{
$array = self::mergeRows($array);
return $array;
}
}
Использование:
$array = ComplexMerge::normalise($array);