Skip to content

array_unique() with SORT_REGULAR returns duplicate values #20262

Description

@jmarble

Description

The following code:

<?php
$units = ['5', '10', '5', '3A', '5', '5'];
$unique = array_unique($units, SORT_REGULAR);
print_r($unique);

Resulted in this output:

Array
(
    [0] => 5
    [1] => 10
    [3] => 3A
    [4] => 5
)

But I expected this output instead:

Array
(
    [0] => 5
    [1] => 10
    [3] => 3A
)

Demonstrations:


Root Cause

The algorithm:

  1. Sort array using comparison function from php_get_data_compare_func_unstable()
  2. Walk through sorted array comparing only adjacent elements
  3. Delete duplicates when adjacent elements compare equal

The bug:

SORT_REGULAR uses zend_compare() which calls zendi_smart_strcmp() for string comparisons. This function has non-transitive behavior when mixing numeric and non-numeric strings:

  • "5" < "10" → true (numeric comparison: 5 < 10)
  • "10" < "3A" → true (lexicographic: "1" < "3")
  • "3A" < "5" → true (lexicographic: "3" < "5") Creates a cycle!

Because the comparison is non-transitive, sorting algorithms (which require transitive comparisons) produce inconsistent results depending on input order.

The deduplication walks through comparing adjacent elements:

lastkept = position_0;  // "5"
position_1 "10" != "5" → keep, lastkept = position_1
position_2 "10" == "10" → delete
position_3 "3A" != "10" → keep, lastkept = position_3
position_4 "5" != "3A" → keep  ← Bug! Never compared to position_0
position_5 "5" == "5" → delete

The root issue: Non-transitive comparisons break the sorting algorithm's guarantee that equal values will be grouped together. The adjacent-only comparison is correct - but it requires the array to be properly sorted first, which requires transitive comparisons.


Comparison with SORT_STRING

<?php
$units = ['5', '10', '5', '3A', '5', '5'];
echo count(array_unique($units, SORT_REGULAR)) . "\n"; // 4 ✗ Wrong
echo count(array_unique($units, SORT_STRING)) . "\n";  // 3 ✓ Correct

SORT_STRING uses lexical comparison without numeric extraction, so duplicates stay grouped.


Workaround

For simple arrays of scalar values, you can use array_unique with default SORT_STRING flag.

<?php
$unique = array_unique($array, SORT_STRING);

For arrays or objects.

$uniqueAddr = [];
foreach ($addresses as $addr) {
    if (! in_array($addr, $uniqueAddr)) {
        $uniqueAddr[] = $addr;
    }
}

PHP Version

PHP 8.4.13 (cli) (built: Sep 26 2025 00:45:36) (NTS clang 15.0.0)
Copyright (c) The PHP Group
Built by Laravel Herd
Zend Engine v4.4.13, Copyright (c) Zend Technologies
    with Zend OPcache v8.4.13, Copyright (c), by Zend Technologies

Activity

  1. devnexen commented on Oct 23, 2025

    @devnexen
    Member

    It is borderline feature request though, it behaves as expected that would be a BC change IMHO. cc @nielsdos just in case.

  2. jmarble commented on Oct 23, 2025

    @jmarble
    Author

    I think you mean borderline user error ;-)

    I really can't imagine a world where anyone would call array_unique($units, SORT_REGULAR) and expect to get duplicates in the returned array. It's definitely a bug unfortunately. And I fear many users out there are just blissfully unaware.

  3. jmarble commented on Oct 23, 2025

    @jmarble
    Author

    Actually, @devnexen perhaps now is the time we consider killing two birds with one stone? Provide a proper type-strict implementation of array_unique(), without resorting to an O(n²) style implementation of in_array(). My thought is if the SORT_REGULAR flag can properly find uniqueness within an array of mixed types, a flag like SORT_STRICT should be possible -- but I digress.

  4. devnexen commented on Oct 23, 2025

    @devnexen
    Member

    I think you mean borderline user error ;-)

    I really can't imagine a world where anyone would call array_unique($units, SORT_REGULAR) and expect to get duplicates in the returned array. It's definitely a bug unfortunately. And I fear many users out there are just blissfully unaware.

    Precisely why I am calling in someone who knows a lot better array internals :) I have the feeling it might be performance costly to fix it with the actual implementation or would need a good rewrite. But I might be wrong.

  5. macropay-solutions commented on Oct 23, 2025

    @macropay-solutions

    The algorithm:
    Sort array using zendi_smart_strcmp() which calls is_numeric_string_ex()

    The flaw: The algorithm only compares with lastkept (last unique value), not with all previous values. Position 4's "5" is never compared back to position 0's "5".

    @devnexen @jmarble the issue is the sort of mixed types not the way it compares the sorted result.
    Basically sort acts as a groupBy same values which then is walked through to eliminate the duplicates.

  6. devnexen commented on Oct 23, 2025

    @devnexen
    Member

    either way I kept the bug label, someone will answer to your request shortly.

  7. jmarble commented on Oct 23, 2025

    @jmarble
    Author

    @macropay-solutions you're correct that sorting is the primary issue, but the adjacent-only comparison is what allows the bug to manifest.

  8. 45 remaining items

  9. marius-ciclistu commented on Dec 28, 2025

    @marius-ciclistu

    Is this issue going to be buried?

  10. jmarble commented on Dec 28, 2025

    @jmarble
    Author

    Is this issue going to be buried?

    I've published two PRs that fix the issue with SORT_REGULAR.

    I also have an RFC for a new SORT_STRICT flag that provides a way to work around the issue. I will be moving the RFC to discussion maybe next week.

  11. shaedrich commented on Dec 28, 2025

    @shaedrich
  12. marius-ciclistu commented on Dec 28, 2025

    @marius-ciclistu

    @shaedrich I know. But they are not merged.

  13. shaedrich commented on Dec 28, 2025

    @shaedrich

    They've been worked on last in November. Keep your hopes up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions