php sort 2d array by sort order of another 2d array
Asked Answered
J

4

1

I have two multi-dimensional arrays and need to sort the first array in the same order as the second array based on different keys (but their values are the same). In the below example I need $allOptions to be sorted into the same order as $regOptions but based on the values of clID == optID.

However, not all $allOptions sub-arrays (clID) are present in $regOptions sub-arrays (optID)....so any non-matched elements in $allOptions would be thrown to the bottom/end of the array.

How can I do this?

$allOptions = array(
     array("clID"=> 171, ...other values),
     array("clID"=> 191, ...other values),
     array("clID"=> 131, ...other values),
     array("clID"=> 101, ...other values),
     array("clID"=> 201, ...other values),
     array("clID"=> 181, ...other values),
     ...
     array("clID"=> 99, ...other values),  // not in regOptions
     array("clID"=> 129, ...other values)  // not in regOptions
     array("clID"=> 139, ...other values)
    
) ;

$regOptions = array(
    array("order"=>1,"optID"=> 131, ...other values),
    array("order"=>2,"optID"=> 191, ...other values),
    array("order"=>3,"optID"=> 181, ...other values),
    array("order"=>4,"optID"=> 139, ...other values),
    array("order"=>5,"optID"=> 101, ...other values),
    array("order"=>6,"optID"=> 201, ...other values),
    array("order"=>7,"optID"=> 171, ...other values) 
    ...
) ;

So the output would be:

$allOptions = array(
     array("clID"=> 131, ...other values),
     array("clID"=> 191, ...other values),
     array("clID"=> 181, ...other values),
     array("clID"=> 139, ...other values)
     array("clID"=> 101, ...other values),
     array("clID"=> 201, ...other values),
     array("clID"=> 171, ...other values),
     ...
     array("clID"=> 99, ...other values),  // not in regOptions
     array("clID"=> 129, ...other values)  // not in regOptions
) ;
Jaquith answered 19/10, 2023 at 20:0 Comment(3)
C
5

All earlier posted answers are working Waaaaaaaay too hard. Use array_column() to generate a lookup array. Use usort() or array_multisort() to sort by that priority array.

Code: (Demo)

$priority = array_column($regOptions, 'order', 'optID');
usort(
    $allOptions,
    fn($a, $b) => ($priority[$a['clID']] ?? PHP_INT_MAX) <=> ($priority[$b['clID']] ?? PHP_INT_MAX)
);
var_export($allOptions);

If you want to fallback to sorting by clID ASC for all of the unmentioned clId values, then use the elvis operator to break those ties with a simple 3-way comparison on the column value.

$priority = array_column($regOptions, 'order', 'optID');
usort(
    $allOptions,
    fn($a, $b) =>
        ($priority[$a['clID']] ?? PHP_INT_MAX) <=> ($priority[$b['clID']] ?? PHP_INT_MAX)
        ?: $a['clID'] <=> $b['clID']
);

The first script can be performed with array_multisort() in the following way. Note that this approach may perform better than usort() because it makes fewer lookups, but it does sort on $priority, then sort on $allOptions row length, then clId (how it would work if you called sort($allOptions); I imagine that the rows all have the same length though). (Demo)

$priority = array_column($regOptions, 'order', 'optID');
array_multisort(
    array_map(fn($v) => $priority[$v['clID']] ?? PHP_INT_MAX, $allOptions),
    $allOptions
);
var_export($allOptions);
Cessionary answered 20/10, 2023 at 5:9 Comment(4)
Thanks for all your comments and input on all the responses here. very informative. Yet your solution above (1st one) is generating this error: unexpected '=> (T_DOUBLE_ARROW)', expecting ')' on this section of code fn($a, $b) => Jaquith
As you can see from my runnable demo links, these scripts are correct and modern. Are you using a version of PHP below PHP7.4?Cessionary
ah...yes, the server is pinned to 7.3....server upgrade is beyond my control.Jaquith
In that case, you cannot yet enjoy "arrow function" syntax. Must be: usort($array, function ($a, $b) { return $a <=> $b; });Cessionary
M
0

Use php usort()

Example

    function customSort($a, $b, $regOptions) {
      $aOptID = $a['clID'];
      $bOptID = $b['clID'];
      $aOrder = array_search($aOptID, array_column($regOptions, 'optID'));
      $bOrder = array_search($bOptID, array_column($regOptions, 'optID'));

      if ($aOrder === false) $aOrder = PHP_INT_MAX;

      if ($bOrder === false) $bOrder = PHP_INT_MAX;

      return $aOrder - $bOrder;
    } 

    usort($allOptions, function($a, $b) use ($regOptions) {
      return customSort($a, $b, $regOptions);
    });
Mullinax answered 19/10, 2023 at 20:29 Comment(2)
@Jaquith I edited the code; when I read it, either it wasn't in there or it escaped my attention that the arrays might not matchMullinax
@Jaquith I do not encourage you to use this unexplained answer, it is making performing multiple cycles over the data on each iteration of usort(). Not only is this script verbose, it is not going to be the best performing solution.Cessionary
S
0

Here is a solution not based on array_search():

$allOptions = [["clID" => 1], ["clID" => 2], ["clID" => 3], ["clID" => 4]];
$regOptions = [["optID" => 3], ["optID" => 2]];

$order = [];
foreach($regOptions as $key => $option)
    $order[$option['optID']] = $key;

usort($allOptions, function($o1, $o2) use($order)
{
    $id1 = $o1['clID'];
    $id2 = $o2['clID'];
    if(isset($order[$id1]) && isset($order[$id2]))
        return $order[$id1] <=> $order[$id2];
    if(isset($order[$id1]) && !isset($order[$id2]))
        return -1;
    if(!isset($order[$id1]) && isset($order[$id2]))
        return 1;
    return $id1 <=> $id2;
});

var_export($allOptions);

Result:

array (
  0 => 
  array (
    'clID' => 3,
  ),
  1 => 
  array (
    'clID' => 2,
  ),
  2 => 
  array (
    'clID' => 1,
  ),
  3 => 
  array (
    'clID' => 4,
  ),
)
Speckle answered 19/10, 2023 at 20:46 Comment(1)
Please see my educational links under the question.Cessionary
D
0

Another option to sorting would be iterating over $regOptions and building a new array from $indexedOptions, and then appending any items not found to the end. Live example at https://3v4l.org/jd5Rq

<?php

$allOptions = array(
    array("clID"=> 171),
    array("clID"=> 191),
    array("clID"=> 131),
    array("clID"=> 101),
    array("clID"=> 201),
    array("clID"=> 181),
    array("clID"=> 99),  // not in regOptions
    array("clID"=> 129),  // not in regOptions
    array("clID"=> 139),
) ;

$regOptions = array(
    array("order"=>1,"optID"=> 131),
    array("order"=>2,"optID"=> 191),
    array("order"=>3,"optID"=> 181),
    array("order"=>4,"optID"=> 139),
    array("order"=>5,"optID"=> 101),
    array("order"=>6,"optID"=> 201),
    array("order"=>7,"optID"=> 171) 
);

// build new array from original array with indexes for faster lookups
$indexedOptions = [];
foreach($allOptions as $val) {
    $indexedOptions[$val['clID']] = $val;
}

$newArray = [];
foreach ($regOptions as $order) {
    $id = $order['optID'];
    
    if (isset($indexedOptions[$id])) {
        $newArray[] = $indexedOptions[$id];
        unset($indexedOptions[$id]);
    }
}

// at this point, $newArray has all of the found values in order,
// and $indexedOptions has any remaining items.
// var_dump($newArray);
// var_dump($indexedOptions);

// put them together for the final array
$final = array_merge($newArray, $indexedOptions);

var_dump($final);

Output:

array(9) {
  [0]=> array(1) {
    ["clID"]=> int(131) 
  }
  [1]=> array(1) {
    ["clID"]=> int(191) 
  }
  [2]=> array(1) {
    ["clID"]=> int(181) 
  }
  [3]=> array(1) {
    ["clID"]=> int(139) 
  }
  [4]=> array(1) {
    ["clID"]=> int(101) 
  }
  [5]=> array(1) {
    ["clID"]=> int(201) 
  }
  [6]=> array(1) {
    ["clID"]=> int(171) 
  }
  [7]=> array(1) {
    ["clID"]=> int(99) 
  }
  [8]=> array(1) {
    ["clID"]=> int(129) 
  } 
}
Diminuendo answered 19/10, 2023 at 20:58 Comment(2)
This brute force script that merges the whitelisted values with the whole input array doesn't seem very elegant to me. For each value not mentioned in the lookup array, there will be N number of fruitless cycles.Cessionary
Agreed, and it could easily be improved by first indexing the array by clID for faster lookups. But other early answers were missing the "add missing items to the end of the array" and I wanted to offer an alternate solution.Diminuendo

© 2022 - 2024 — McMap. All rights reserved.