3

I have an array that looks like

$array = [
    //...
    'name' => ['value' => 'Raj KB'],
    'street' => ['value' => 'Street ABC'],
    'city' => ['value' => 'Dubai'],
    'country_id' => ['value' => 'UAE'],
    'region' => ['value' => 'DXB'],
    'region_id' => ['value' => 11],
    'zip_code' => ['value' => 12345],
    'city_id' => ['value' => 22],
    //...
];

I would like to sort the array so that the keys country_id, region, region_id, city, city_id occur serially while preserving the position of others.

Expected Output

 $array = [
    //...
    'name' => ['value' => 'Raj KB'],
    'street' => ['value' => 'Street ABC'],
    'country_id' => ['value' => 'UAE'],
    'region' => ['value' => 'DXB'],
    'region_id' => ['value' => 11],
    'city' => ['value' => 'Dubai'],
    'city_id' => ['value' => 22],
    'zip_code' => ['value' => 12345],
    //...
];

I have tried as:

Trial #1

uksort($array, function ($a, $b) {

  $order = ['country_id' => 0, 'region' => 1, 'region_id' => 2, 'city' => 3, 'city_id' => 4];
  if (isset($order[$a]) && isset($order[$b])) {
    return $order[$a] - $order[$b];
  } else {
    return 0;
  }
});

var_dump($array);

Trial #2

uksort($array, function ($a, $b) {

  $order = ['country_id' => 0, 'region' => 1, 'region_id' => 2, 'city' => 3, 'city_id' => 4];
  if (!isset($order[$a]) && !isset($order[$b])) {
    return 0;
  } elseif (!isset($order[$a])) {
    return 1;
  } elseif (!isset($order[$b])) {
    return -1;
  } else {
    return $order[$a] - $order[$b];

  }
});

var_dump($array);

But the rest of the orders are not maintained anymore. So I want those custom fields to appear in the same order without breaking the positions of others. For example, name should appear first, etc.

6
  • ksort(). Commented May 30, 2020 at 9:43
  • uksort() will do the job I guess Commented Jun 1, 2020 at 11:14
  • Why do you expect name to occur early, and zip_code late? Are you expecting the original order to somehow play a role? I don't see the logic. Commented Jun 1, 2020 at 11:31
  • I only want those to be sorted out serially, other should be as it is. Commented Jun 1, 2020 at 11:47
  • But that is not really well defined. Which element (that also occurs in $orders) should define the anchor for the other matches to assemble around? Why, for instance, are not all matches moved to the slots before city_id, so they are adjacent with it, and so that zip_code would appear in third place? Your example is quite simple, but it could get even less clear if the matches are spread all over a very large array, completely out of order. Where should the matches assemble? Commented Jun 1, 2020 at 13:23

4 Answers 4

4
+25

It looks like what you want is difficult to achieve with one of PHP's sort methods. Moreover, as the relative order of the non-matching keys should not change, we can aim for a better time complexity than with a O(nlogn) sort-method.

So, I would suggest writing a function that makes some iterations over both arrays ($array, $order) for it to collect the key/value pairs in the expected order. That constitutes an O(n+m) time complexity, where n and m are the two sizes of the two arrays.

Here is the function:

function sortadjacent($array, $order) {
    $insertAt = 0;
    foreach($array as $key => $_) {
        if (isset($order[$key])) break;
        $insertAt++;
    }

    $special = [];
    foreach($order as $key => $_) {
        if (isset($array[$key])) $special[$key] = $array[$key];
    }

    $result = [];
    foreach($array as $key => $value) {
        if (!isset($order[$key])) $result[$key] = $value;
        else if (count($result) == $insertAt) $result = array_merge($result, $special);
    }

    return $result;
}

You would call it as follows:

$result = sortadjacent($array, $order);

Note that this function does not make changes to $array, but instead returns the expected result in a new array.

Sign up to request clarification or add additional context in comments.

Comments

3

You're quite close in your implementation, but you have to consider the case in your comparison function where only one of your wanted keys are present and not any of the others. If you return 0 in that case, they'll be mangled within the other keys in your array (since their position in that case is considered to be equal).

Since you also want the sequence of existing keys to be kept, and the other "extracted" keys to be inserted after country_id, you can keep a reference to the original sort order and use that to resolve the sort order in relation to country_id for other fields (and between other fields to keep the current sort order)

By handling those two special cases to explicitly sort the keys you want to occur after each other by themselves, you get a result that satisfies your requirement:

$order = ['country_id' => 1, 'region' => 2, 'region_id' => 3, 'city' => 4, 'city_id' => 5];
$preset_order = array_flip(array_keys($array));

uksort($array, function ($a, $b) use ($order, $preset_order) {
  if (isset($order[$a]) && isset($order[$b])) {
    return $order[$a] - $order[$b];
  } else if (isset($order[$a])) {
    return $preset_order['country_id'] - $preset_order[$b];
  } else if (isset($order[$b])) {
    return $preset_order[$a] - $preset_order['country_id'];
  } else {
    return $preset_order[$a] - $preset_order[$b];
  }
});

Outputs:

array(8) {
  'name' =>
  array(1) {
    'value' =>
    string(6) "Raj KB"
  }
  'street' =>
  array(1) {
    'value' =>
    string(10) "Street ABC"
  }
  'country_id' =>
  array(1) {
    'value' =>
    string(3) "UAE"
  }
  'region' =>
  array(1) {
    'value' =>
    string(3) "DXB"
  }
  'region_id' =>
  array(1) {
    'value' =>
    int(11)
  }
  'city' =>
  array(1) {
    'value' =>
    string(5) "Dubai"
  }
  'city_id' =>
  array(1) {
    'value' =>
    int(22)
  }
  'zip_code' =>
  array(1) {
    'value' =>
    int(12345)
  }
}

13 Comments

This is not the expected output. Though the country, region, city are appearing serially but rest of the positions are messed up.
Your only stated requirement was that the keys occur serially. In that case, if the positions of the remaining keys are important: where should the "extracted" keys start? Should they be located serially from the first occurrence of either of the keys? Can they be moved to the end while the rest of the elements are kept in their existing order?
For example, if the keys are in the order: name, street, city, country_id, region, region_id, zip_code, city_id. It should occur as: name, street, {country_id, region, region_id, city, city_id}, zip_code. You can see that rest positions are preserved.
Yes, but what if it's name, street, city, region_id, zip_code, city_id, country_id, region? Should it then start at country_id or at region_id?
@trincot I know, that's why I'm asking. I'm not OP.
|
2

PHP is using Quicksort, so you can't just return return senseful values for the elements you want to be sorted. In my opinion it's a bad idea to use uksort here, because you would have to use the current indices of your array as values, but thats impossible because you can't access a copy of the old array from inside your compare-function. Also you would need to know on which index the first of your special values is.

So I would suggest to do sth like this, because I think it's impossible to do what you want with uksort:

function customSort($array)
{
    $order = ['country_id' => 0, 'region' => 1, 'region_id' => 2, 'city' => 3, 'city_id' => 4];
    $keyArray = array();
    $sortedArray = array();
    foreach ($array as $i => $value) {
        $keyArray[] = $i;
    }
    $counter = 0;
    $hasStarted = false;
    $insertLater = array();
    for ($i = 0; $i < count($keyArray); $i++) {
        if ($hasStarted) {
            if ($counter < count($order)) {
                $sortedArray[array_search($counter, $order)] = $array[array_search($counter, $order)];

                $counter++;
                if (!isset($order[$keyArray[$i]])) {
                    array_push($insertLater, ["key" => $keyArray[$i], "value" => $array[$keyArray[$i]]]);
                }
                continue;
            }
        }

        if (count($insertLater) > 0) {
            $itemToInsert = array_shift($insertLater);
            $sortedArray[$itemToInsert["key"]] = $itemToInsert["value"];

            if (!isset($order[$keyArray[$i]])) {
                array_push($insertLater, ["key" => $keyArray[$i], "value" => $array[$keyArray[$i]]]);
            }
            continue;
        }
        if (isset($order[$keyArray[$i]]) && !$hasStarted) {
            $sortedArray[array_search($counter, $order)] = $array[array_search($counter, $order)];
            $hasStarted = true;
            $counter++;
            continue;
        }
        $sortedArray[$keyArray[$i]] = $array[$keyArray[$i]];
    }
    return $sortedArray;
}
It's may

be not the best solution, but it works O(n).

4 Comments

This cannot be right. When in the first iteration you would get into the else block, and in the second iteration in the if, then the second iteration will overwrite the data that the first iteration had added to the sorted array, since the key is exactly the same ($count == 0).
Oh, there was a mistake. I fixed it now
Did you test this? This runs into an exception now.
I had no exceptions, but I misunderstood the occurrence of the orderd elements, so I changed that too now.
1

An idea, building a generator that yields the keys in the order you want (from my understanding of the question, the ordered keys group starts when the first ordered key occurs):

$ordered_keys = ['country_id', 'region', 'region_id', 'city', 'city_id'];

$keys = (function ($array, $ordered_keys) {
    $flag = true;
    foreach ($array as $key => $v) {
        if ( $flag && in_array($key, $ordered_keys) ) {
            yield from $ordered_keys;
            $flag = false;
        }
        yield $key;
    }
})($array, $ordered_keys);

$result = [];

foreach($keys as $key) {
    $result[$key] = $array[$key];
}

print_r($result);

demo

Note that values for some ordered keys will be set twice, but it doesn't matter since only the first setting determines keys order.


You don't even need a generator, you can do it directly in one loop:

$ordered_keys = ['country_id', 'region', 'region_id', 'city', 'city_id'];

$flag = true;
$result = [];

foreach ($array as $k=>$v) {
    if ( $flag && in_array($k, $ordered_keys) ) {
        $flag = false;
        foreach ($ordered_keys as $key) {
            $result[$key] = $array[$key];
        }
    } else {
        $result[$k] = $array[$k];
    }
}

print_r($result);

demo


Other idea: building an array with the same keys but with integer values.

Before the first "ordered key", values are in the 0-255 range, after values are in the 512-inf range. Values for ordered keys are already setting in the 256-511 range. (These ranges are totally arbitrary, you can choose the ranges you want.)

$ordered_keys = ['country_id' => 256, 'region' => 257, 'region_id' => 258, 'city' => 259, 'city_id' => 260];

$index = 0;

$result = [];

foreach ($array as $k => $v) {
    if ( isset($ordered_keys[$k]) ) {
        $result[$k] = $ordered_keys[$k];
        if ( $index < 255 ) $index = 512;
    } else {
        $result[$k] = $index++;
    }
}

asort($result);

foreach($result as $k => $v) {
    $result[$k] = $array[$k];
}

print_r($result);

demo

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.