【问题标题】:Sorting an array of strings by arbitrary numbers of substrings按任意数量的子字符串对字符串数组进行排序
【发布时间】:2021-02-04 20:10:56
【问题描述】:

我正在尝试在 php-cs-fixer 中修改 the OrderedImportsFixer class,以便按照我想要的方式清理我的文件。我想要的是以类似于您在文件系统列表中看到的方式订购我的导入,在“文件”之前列出“目录”。

所以,给定这个数组:

$indexes = [
    26 => ["namespace" => "X\\Y\\Zed"],
    9 =>  ["namespace" => "A\\B\\See"],
    3 =>  ["namespace" => "A\\B\\Bee"],
    38 => ["namespace" => "A\\B\\C\\Dee"],
    51 => ["namespace" => "X\\Wye"],
    16 => ["namespace" => "A\\Sea"],
    12 => ["namespace" => "A\\Bees"],
    31 => ["namespace" => "M"],
];

我想要这个输出:

$sorted = [
    38 => ["namespace" => "A\\B\\C\\Dee"],
    3 =>  ["namespace" => "A\\B\\Bee"],
    9 =>  ["namespace" => "A\\B\\See"],
    12 => ["namespace" => "A\\Bees"],
    16 => ["namespace" => "A\\Sea"],
    26 => ["namespace" => "X\\Y\\Zed"],
    51 => ["namespace" => "X\\Wye"],
    31 => ["namespace" => "M"],
];

在典型的文件系统列表中:

我在uasort 已经有一段时间了(必须保持密钥关联)并且已经接近了。诚然,这更多是由于拼命挥霍,而不是任何严格的方法。不太了解uasort 的工作原理有点限制我。

// get the maximum number of namespace components in the list
$ns_counts = array_map(function($val){
    return count(explode("\\", $val["namespace"]));
}, $indexes);
$limit = max($ns_counts);

for ($depth = 0; $depth <= $limit; $depth++) {
    uasort($indexes, function($first, $second) use ($depth, $limit) {
        $fexp = explode("\\", $first["namespace"]);
        $sexp = explode("\\", $second["namespace"]);
        if ($depth === $limit) {
            // why does this help?
            array_pop($fexp);
            array_pop($sexp);
        }
        $fexp = array_slice($fexp, 0, $depth + 1, true);
        $sexp = array_slice($sexp, 0, $depth + 1, true);
        $fimp = implode(" ", $fexp);
        $simp = implode(" ", $sexp);
        //echo "$depth: $fimp <-> $simp\n";
        return strnatcmp($fimp, $simp);
    });
}
echo json_encode($indexes, JSON_PRETTY_PRINT);

这给了我正确排序的输出,但在底部而不是顶部有更深的命名空间:

{
    "31": {
        "namespace": "M"
    },
    "12": {
        "namespace": "A\\Bees"
    },
    "16": {
        "namespace": "A\\Sea"
    },
    "3": {
        "namespace": "A\\B\\Bee"
    },
    "9": {
        "namespace": "A\\B\\See"
    },
    "38": {
        "namespace": "A\\B\\C\\Dee"
    },
    "51": {
        "namespace": "X\\Wye"
    },
    "26": {
        "namespace": "X\\Y\\Zed"
    }
}

我在想我可能必须为每个级别的命名空间构建一个单独的数组并单独对其进行排序,但我对如何做到这一点却一无所知。有什么建议可以让这个工作的最后一步,或者完全不同的不涉及这么多循环的东西?

【问题讨论】:

    标签: php arrays sorting usort


    【解决方案1】:

    我们将其分为 4 个步骤。

    第 1 步:从数据集创建层次结构。

    function createHierarchicalStructure($indexes){
        $data = [];
        foreach($indexes as $d){
            $temp = &$data;
            foreach(explode("\\",$d['namespace']) as $namespace){
                if(!isset($temp[$namespace])){
                    $temp[$namespace] = [];
                }
                $temp = &$temp[$namespace];
            }
        }
        
        return $data;
    }
    

    \\ 拆分命名空间并维护$data 变量。使用&amp; 地址引用继续编辑数组的同一副本。

    第 2 步: 对第一个文件夹中的层次结构进行排序,然后按文件方式排序。

    function fileSystemSorting(&$indexes){
        foreach($indexes as $key => &$value){
            fileSystemSorting($value);
        }
        
        uksort($indexes,function($key1,$key2) use ($indexes){
            if(count($indexes[$key1]) == 0 && count($indexes[$key2]) > 0) return 1;
            if(count($indexes[$key2]) == 0 && count($indexes[$key1]) > 0) return -1;
            return strnatcmp($key1,$key2);
        });
    }
    

    对从属文件夹进行排序并使用uksort 作为当前级别的文件夹。反之亦然。如果比较的两个文件夹都有子文件夹,则将它们作为字符串进行比较,否则如果一个是文件夹而另一个是文件,则将文件夹放在上面。

    第 3 步:现在按顺序展开层次结构。

    function flattenFileSystemResults($hierarchical_data){
        $result = [];
        foreach($hierarchical_data as $key => $value){
            if(count($value) > 0){
                $sub_result = flattenFileSystemResults($value);
                foreach($sub_result as $r){
                    $result[] = $key . "\\" . $r;
                }   
            }else{
                $result[] = $key;
            }
        }
        
        return $result;
    }
    

    第四步:恢复初始数据键并返回结果。

    function associateKeys($data,$indexes){
        $map = array_combine(array_column($indexes,'namespace'),array_keys($indexes));
        $result = [];
        foreach($data as $val){
            $result[ $map[$val] ] = ['namespace' => $val];
        }
        return $result;
    }
    

    驱动代码:

    function foldersBeforeFiles($indexes){
       $hierarchical_data = createHierarchicalStructure($indexes);
       fileSystemSorting($hierarchical_data);
       return associateKeys(flattenFileSystemResults($hierarchical_data),$indexes);
    }
    
    print_r(foldersBeforeFiles($indexes));
    

    演示: https://3v4l.org/cvoB2

    【讨论】:

    • 取决于项目。我知道一些核心 Laravel 文件有超过 20 个导入语句,命名空间深度可达 6 或 7 级。
    • 当然还有there are always outliers。 ;)
    • 用引用构建层次结构的好技巧,这就是我最初试图做的,但画了一个空白。我感谢您的努力,但不幸的是,这并不能保留数组的键。 php-cs-fixer 在修复文档后需要密钥来重建文档。
    • 看来,是的
    • 感谢您的努力,我将 Jeto 的答案标记为已接受,因为它更容易集成到现有代码中。
    【解决方案2】:

    我相信以下应该有效:

    uasort($indexes, static function (array $entry1, array $entry2): int {  
        $ns1Parts = explode('\\', $entry1['namespace']);
        $ns2Parts = explode('\\', $entry2['namespace']);
    
        $ns1Length = count($ns1Parts);
        $ns2Length = count($ns2Parts);
    
        for ($i = 0; $i < $ns1Length && isset($ns2Parts[$i]); $i++) {
            $isLastPartForNs1 = $i === $ns1Length - 1;
            $isLastPartForNs2 = $i === $ns2Length - 1;
    
            if ($isLastPartForNs1 !== $isLastPartForNs2) {
                return $isLastPartForNs1 <=> $isLastPartForNs2;
            }
    
            $nsComparison = $ns1Parts[$i] <=> $ns2Parts[$i];
    
            if ($nsComparison !== 0) {
                return $nsComparison;
            }
        }
    
        return 0;
    });
    

    它的作用是:

    • 将命名空间拆分为多个部分,
    • 从第一个开始比较每个部分,然后:
      • 如果我们处于最后一部分而不是另一部分,则优先考虑具有最多部分的部分,
      • 否则,如果各个部分不同,则按字母顺序优先考虑在另一个之前的部分。

    Demo

    【讨论】:

    • 当我现场试用这个时,它遇到了单元素命名空间的问题(例如Throwable)。一直把它们放在列表的底部。
    • 是的,我刚刚意识到您可能无法通过单一/基本的排序传递来做到这一点,因为两个条目的排序可能会因其他条目而异。例如,如果 AX\Y\Zed 是仅有的两个条目,那么 X\Y\Zed 应该排在第一位(文件夹在前),对吗?而在您的示例中,情况正好相反。
    • 实际上是的,你是对的,我应该期望这些条目位于列表的底部。将使用更多实时数据进行测试,看看效果如何。
    【解决方案3】:

    这是另一个进一步分解步骤的版本,虽然它可能不是最优化的,但绝对可以帮助我的大脑思考它。有关正在发生的事情的更多详细信息,请参阅 cmets:

    uasort(
        $indexes,
        static function (array $a, array $b) {
    
            $aPath = $a['namespace'];
            $bPath = $b['namespace'];
    
            // Just in case there are duplicates
            if ($aPath === $bPath) {
                return 0;
            }
    
            // Break into parts
            $aParts = explode('\\', $aPath);
            $bParts = explode('\\', $bPath);
    
            // If we only have a single thing then it is a root-level, just compare the item
            if (1 === count($aParts) && 1 === count($bParts)) {
                return $aPath <=> $bPath;
            }
    
            // Get the class and namespace (file and folder) parts
            $aClass = array_pop($aParts);
            $bClass = array_pop($bParts);
    
            $aNamespace = implode('\\', $aParts);
            $bNamespace = implode('\\', $bParts);
    
            // If the namespaces are the same, sort by class name
            if ($aNamespace === $bNamespace) {
                return $aClass <=> $bClass;
            }
    
            // If the first namespace _starts_ with the second namespace, sort it first
            if (0 === mb_strpos($aNamespace, $bNamespace)) {
                return -1;
            }
    
            // Same as above but the other way
            if (0 === mb_strpos($bNamespace, $aNamespace)) {
                return 1;
            }
    
            // Just only by namespace
            return $aNamespace <=> $bNamespace;
        }
    );
    

    Online demo

    【讨论】:

    • 我应该注意,这使用了宇宙飞船运算符&lt;=&gt;,但您的代码使用了strnatcmp,它处理字符串中的数字,可能更适合。
    • 数字在命名空间中非常罕见,尽管我确实有一些从 PEAR2\\Net 导入的旧代码。我会看看它是如何工作的......
    • 我最喜欢这种方法,因为它没有循环,但是一旦我向它提供了一些真实数据,它就不起作用了。在我的示例数据中,将A\B\BeeA\B\See 替换为Am\B\BeeAm\B\See 会导致这些项目在A\Bees 之前订购。
    • @miken32,感谢您提供更多示例数据,我会看看是否可以改进!
    【解决方案4】:

    我觉得 Jeto 的算法设计没有错,但我决定更简洁地实现它。我的 sn-p 避免了 for() 循环中的迭代函数调用和算术运算,使用单个宇宙飞船运算符和单个返回。我的 sn-p 短了 50% 以上,而且我通常觉得它更容易阅读,但是每个人都认为自己的宝宝很可爱,对吧?

    代码:(Demo)

    uasort($indexes, function($a, $b) {  
        $aParts = explode('\\', $a['namespace']);
        $bParts = explode('\\', $b['namespace']);
        $aLast = count($aParts) - 1;
        $bLast = count($bParts) - 1;
    
        for ($cmp = 0, $i = 0; $i <= $aLast && !$cmp; ++$i) {
            $cmp = [$i === $aLast, $aParts[$i]] <=> [$i === $bLast, $bParts[$i]];
        }
        return $cmp;
    });
    

    像 Jeto 的回答一样,它同时迭代每个数组并且

    • 检查任一元素是否是数组的最后一个元素,如果是,则将其从列表中移出(因为我们想要更长的路径来赢得决胜局);
    • 如果两个元素都不是其数组中的最后一个元素,则按字母顺序比较元素的当前字符串。
    • 该过程一直重复,直到生成非零评估。
    • 由于预计不会出现重复条目​​,因此返回值应始终为-11(绝不是0

    注意,我在两个条件下停止 for() 循环。

    1. 如果$i 大于$aParts 中的元素数量(仅限)——因为如果$bParts 的元素数量少于$aParts,那么$cmp 将在通知之前生成一个非零值被触发。
    2. 如果$cmp 是一个非零值。

    最后,解释一下 spaceship 运算符两侧的数组语法...
    spaceship 运算符将从左到右比较数组,因此它的行为类似于:

    leftside[0] <=> rightside[0] then leftside[1] <=> rightside[1]
    

    以这种方式进行多重比较不会影响性能,因为&lt;=&gt; 的任何一侧都没有函数调用。如果涉及函数调用,则以回退方式进行单独比较会更高效,例如:

    fun($a1) <=> fun($b1) ?: fun($a2) <=> fun($b2)
    

    这样,后续的函数调用只有在需要决胜局时才会真正进行。

    【讨论】:

      猜你喜欢
      • 2012-10-14
      • 2014-01-05
      • 2014-04-23
      • 2014-10-28
      • 1970-01-01
      • 2016-04-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多