Sorting Arrays of objects by predefined map of values [duplicate]
Asked Answered
C

3

1

I have the following Arrays:

$inputArray = Array(
    [0] => stdClass Object (
        [id] => 8
    )

    [1] => stdClass Object (
        [id] => 7
    )

    [2] => stdClass Object (
        [id] => 5
    )
)

$sortingArray = [5,8,1]

I'm looking an efficient way to sort the input array by the id "map of values".. expected output should be $inputArray reordered so that 3rd item would be first, first item would be 2nd etc.

thanks!

Candler answered 9/10, 2018 at 5:52 Comment(7)
try usort()..Calcite
usort as far as i saw is comparing 2 values in array and not against an external map of values...or at least i couldn't find oneCandler
Can you post what you have done so far?Calcite
so far, i have iterated the array of objects and comparing each one to the list. if found i insert to a new array and unset the value from the $inputArray, but i believe this is sub-optimal, has to be a better way..Candler
@Delacourt it is indeed....but the answer here tops the answers there :)Candler
May I ask the source of this data? Is this coming from a file? a database?Delacourt
the source is partly DB and partly calculationsCandler
V
1

I've used a few things here, the main thing is that I use the sorting array as a target for an array_replace() and I use array_column() to index the objects by (this needs PHP 7.0+ to work with objects)...

$input = array_column($inputArray, null, "id");   
$sort = array_fill_keys($sortingArray, null);
$output = array_filter(array_replace($sort, $input));

The array_filter() will remove any elements which aren't in the input array but in the sorting array. The array_fill_keys() is used instead of array_flip() so that I can set a null value, which allows the filter to work.

Venterea answered 9/10, 2018 at 6:15 Comment(2)
i think this is a more elegant approach. works like a charm! *the reason that i believe its a preferred method is that it filters duplicates in the original input array and can't fail if key doesnt existCandler
I don't expect that 0 will be a valid value for an id, but if someone in the future uses this approach, array_filter() will destroy them with the null values.Delacourt
C
2

You can use array_flip to make a temporary array. This will flip the array means the values will be the key.

Use usort to sort the array.

$tempArr = array_flip( $sortingArray  );
usort($inputArray, function($a, $b) use ( $tempArr ) {
    $tempA = isset( $tempArr[ $a->id ] ) ? $tempArr[ $a->id ] : 999999; //If id does not exist on $sortingArray. Use 999999 as an index
    $tempB = isset( $tempArr[ $b->id ] ) ? $tempArr[ $b->id ] : 999999;
    return $tempA - $tempB;
});

echo "<pre>";
print_r( $inputArray );
echo "</pre>";

This will result to:

Array
(
    [0] => stdClass Object
        (
            [id] => 5
        )

    [1] => stdClass Object
        (
            [id] => 8
        )

    [2] => stdClass Object
        (
            [id] => 7
        )

)
Calcite answered 9/10, 2018 at 6:10 Comment(0)
V
1

I've used a few things here, the main thing is that I use the sorting array as a target for an array_replace() and I use array_column() to index the objects by (this needs PHP 7.0+ to work with objects)...

$input = array_column($inputArray, null, "id");   
$sort = array_fill_keys($sortingArray, null);
$output = array_filter(array_replace($sort, $input));

The array_filter() will remove any elements which aren't in the input array but in the sorting array. The array_fill_keys() is used instead of array_flip() so that I can set a null value, which allows the filter to work.

Venterea answered 9/10, 2018 at 6:15 Comment(2)
i think this is a more elegant approach. works like a charm! *the reason that i believe its a preferred method is that it filters duplicates in the original input array and can't fail if key doesnt existCandler
I don't expect that 0 will be a valid value for an id, but if someone in the future uses this approach, array_filter() will destroy them with the null values.Delacourt
D
0

First and foremost, if your input array is the result set of a MySQL query, you should be sorting the data via an ORDER BY clause. FIELD() is an excellent tool for the job, with slight weirdness that you need to reverse your sorting logic and write DESC after the function.

Resource: https://www.electrictoolbox.com/mysql-order-specific-field-values/ (scroll down to "gotcha")

SQL Fiddle Demo: http://sqlfiddle.com/#!9/6b996f/1


Beyond that, I'll offer two refined versions of Nigel's and Eddie's solutions leveraging some php7+ goodness. Which one you choose to implement will have to do with:

  1. Whether you want to allow elements with duplicate id values to "survive" the process
  2. Which one performs more efficiently for your actual project data (the size of your input array and your custom order array)
  3. Personal preference (both snippets can be sensibly written in 3 to 5 lines, so brevity isn't much of a criteria)

Solution #1: usort() and null coalescing operator

PHP Demo

$array = [
    (object)['id' => 8],
    (object)['id' => 7],
    (object)['id' => 5]
];

$order = [5, 8, 1];  // custom sort order

$order = array_flip($order);  // restructure for easy lookup with isset()
$order[''] = max(array_column($array, 'id')) + 1;  // append value higher than max for outlying ids

usort($array, function($a, $b) use ($order) {
    return ($order[$a->id] ?? $order['']) <=> ($order[$b->id] ?? $order['']);
});
var_export($array);

*note that I could have passed $outlier into the custom function scope as a second use argument and replaced all of the $order[''] variables with $outlier, but I opted to append the outlier data into the lookup array.

*I used parentheses in my custom function not only to improve readability, but to guarantee that the evaluation is performed as intended -- I didn't actually bother to check if the evaluation is the same without the parenthetical grouping.

Solution #2: array_replace() a flipped & filtered sort array with a keyed array

(PHP Demo)

$array = [
    (object)['id' => 8],
    (object)['id' => 7],
    (object)['id' => 5]
];

$order = [5, 8, 1];

$order = array_flip($order);                            // flip for future key comparisons
$keyed = array_column($array, null, 'id');              // declare id values as new keys
$filtered_order = array_intersect_key($order, $keyed);  // remove unwanted order keys
$replaced = array_replace($filtered_order, $keyed);     // apply objects to output array
var_export(array_values($replaced));                    // re-index the output (if desired)

*note this won't generate an oversized array of null values and it will not remove values that are "falsey".

*I will again state that the use of array_column() will damage the data if there are duplicate id values in the input array.

Delacourt answered 11/10, 2018 at 6:58 Comment(2)
thanks, regarding the SQL it isnt unfortunately...regarding the refactored version of array_search the first response was shorter and more efficient....no?Candler
The efficiency is very dependent on the size of the arrays involved. If you mean the usort() - array_search() answers on the duplicate page, I didn't test against them however, array_search() works harder than isset(). The cost to using isset() is a single call of array_flip() to set it up. array_search() actually traverses the array and breaks when it finds a match. isset() doesn't do this -- this is the magic of how php processes a hash map. I am under-qualified to explain this in detail.Delacourt

© 2022 - 2024 — McMap. All rights reserved.