1

I have two arrays, the first one is the sorted version (sorted by the user) of the second one, but the second one can be shorter or longer than the first one. For example:

$books_sorted = array(
     0 => array(
        "title" => "In Search of Lost Time"
     ),
     1 => array(
        "title" => "Don Quixote"
     ),
     2 => array(
        "title" => "The Great Gatsby"
     )
);

$books_available = array(
      0 => array(
         "title" => "Moby Dick"
      ),
      1 => array(
         "title" => "In Search of Lost Time"
      ),
      2 => array(
         "title" => "The Great Gatsby"
      ),
      3 => array(
         "title" => "War and Peace"
      )
);

I need a result array that respects the order set by the user, but removes the missing books from the second array and adds them to the end of all the new books from the second array. Ex.

// "Don Quixote" is not available anymore -> needs to be removed
// "War and Peace" and "Moby Dick" are available -> need to be added both at the end

$books_third_array = array(
     0 => array(
        "title" => "In Search of Lost Time"
     ),
     1 => array(
        "title" => "The Great Gatsby"
     ),
     2 => array(
        "title" => "Moby Dick"
     ),
     3 => array(
        "title" => "War and Peace"
     )    
);

I only put the "title" key because there are others, but I don't think they're useful to this example.

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
  • Why not just keep the original id's in the 2nd array dimension of the sorted array to be way more efficient – Shardj Jul 18 '19 at 11:22

3 Answers3

3

You can find all elements in the first array that are in the second, then find all elements in the second that are not in the first - and combine the two. array_filter will help you there. You'll have something like this:

$sorted_titles = array_column($books_sorted, 'title');
$available_titles = array_column($books_available, 'title');

$third_array = array_merge(
    array_filter($books_sorted, function($e) use ($available_titles) {
        return in_array($e['title'], $available_titles);
    }),
    array_filter($books_available, function($e) use ($sorted_titles) {
        return !in_array($e['title'], $sorted_titles);
    })
);

Live demo: https://3v4l.org/fSpWm

Edit based on comments:

If you need to not just preserve other "fields" in your first array, but also copy non-existing keys from the second array into the first one, the code becomes somewhat more complicated. Something like this may do:

$sorted_titles = array_column($books_sorted, 'title');
$available_titles = array_reduce($books_available, function($result, $e) {
    $result[$e['title']] = $e;
    return $result;
});

$third_array = array_merge(
    array_map(
        function($e) use ($available_titles) {
            return array_merge($available_titles[$e['title']], $e);
        },
        array_filter($books_sorted, function($e) use ($available_titles) {
            return in_array($e['title'], array_keys($available_titles));
        })
    ),
    array_filter($books_available, function($e) use ($sorted_titles) {
        return !in_array($e['title'], $sorted_titles);
    })
);

Live demo: https://3v4l.org/VZGbB

Aleks G
  • 56,435
  • 29
  • 168
  • 265
  • Thank you Aleks G, it works. Can I ask you an edit? If I would like to merge and keep extra array keys other than "title", for example "author", and get all of them in the $third_array, can you give me that other example? https://3v4l.org/kFKMQ – silentheaven Jul 18 '19 at 11:47
  • You shouldn't need to change anything from my original example - this code will preserve the rest of data in sub-arrays - https://3v4l.org/CKvf6 – Aleks G Jul 18 '19 at 12:23
  • Yes, but I meant to merge array keys with their values even if they're not in both arrays. https://3v4l.org/kFKMQ – silentheaven Jul 18 '19 at 13:44
  • @silentheaven Well, that's a completely different problem now. Now you are not only filtering one array based on the content of another - you need to merge the content. See my updated answer. – Aleks G Jul 18 '19 at 14:14
1

Use usort() and define your own sorting-function, use the use keyword to pass in the array of sorted titles, and if its in the array, move it up - otherwise move it down.

$books_sorted_titles = array_column($books_sorted, 'title');
usort($books_available, function($k, $v) use ($books_sorted_titles) {
    return in_array($v['title'], $books_sorted_titles) ? 1 : -1;
});
Qirel
  • 25,449
  • 7
  • 45
  • 62
  • I am not too sure, but may be sorted order issue will come when both array changed to more number of books.+1 for creative solution – Alive to die - Anant Jul 18 '19 at 11:06
  • @AnantSingh---AlivetoDie This solution does not preserve the sort order of the first array. Items from `sorted` array that are in different order in `available` array will be in the wrong order - have a look yourself: https://3v4l.org/JATnl – Aleks G Jul 18 '19 at 11:26
  • Thank you. There is only one problem. If the number of the books are the same in the first two arrays, the third one seems to be not sorted correctly. (it should be equal to the first array) https://3v4l.org/UcMm2 I don't know if I made something wrong. – silentheaven Jul 18 '19 at 11:38
  • I see that the array only puts the sorted values first, but keeps its order in the available-array. The approach from Aleks is superior in that sense, so I suggest you give that one a try (I already upvoted his answer, seems to be the more correct approach.). – Qirel Jul 18 '19 at 11:41
0

You should avoid making iterated calls of in_array() for performance reasons. When this logic is called for, use array_intersect().

To implement a functional-style approach, isolate the intersections between the arrays with the sorted array as the first parameter so that its order is preserved. Then get the diiferences between the arrays with the available array as the first parameter to return its qualifying elements. Then simply merge the arrays so that the sorted values come before the (un-chosen) available values.

Code: (Demo)

var_export(
    array_merge(
        array_uintersect(
            $books_sorted,
            $books_available,
            fn($a, $b) => $a['title'] <=> $b['title']
        ),
        array_udiff(
            $books_available,
            $books_sorted,
            fn($a, $b) => $a['title'] <=> $b['title']
        ),
    )
);

I don't know how this alternative snippet will compare in terms of performance, but you can determine the intersections and differences in one pass. This aproach however consumes/unsets elements from the available array. (Demo)

$available = array_column($books_available, 'title');
$result = [];
foreach ($books_sorted as $row) {
    if (($index = array_search($row['title'], $available)) !== false) {
        $result[] = $row;
        unset($books_available[$index]);
    }
}
array_push($result, ...$books_available);
var_export($result);
mickmackusa
  • 43,625
  • 12
  • 83
  • 136