Как отсортировать многоуровневый список

Мне нужно отсортировать список с многоуровневыми индексами

      (@('1', '2', '3', '1.1.1', '1.99', '2.5', '5.5', "10") | Sort-Object) -join ", "

1, 1.1.1, 1.99, 10, 2, 2.5, 3, 5.5

Я придумал такое решение, но оно не работает с индексами выше десяти

      (@('1', '2', '3', '1.1.1', '1.99', '2.5', '5.5', "10") | Sort-Object {
    $chanks = $_.Split('.')
    $sum = 0
    for ($i = 0; $i -lt $chanks.Count; $i++) {
        $sum += [int]$chanks[$i] / [math]::Pow(10, $i)
    }
    $sum
}) -join ", "

1, 1.1.1, 2, 2.5, 3, 5.5, 10, 1.99

4 ответа

Основано на комментарии Лэнса У. Мэтьюза к вашему вопросу. ... | Sort-Object { [int] $_.Split('.')[0] }, { [int] $_.Split('.')[1] }, { [int] $_.Split('.')[2] },

вот способ сделать то же самое, не зная количества вложенных уровней.

Это делается путем проверки максимального уровня вложенности и сохранения его в переменной, а затем динамического построения массива блоков сценария для сортировки.

      $Arr = @('1', '2', '3', '1.1.1', '1.99', '2.5', '5.5', "10", '12.9.3.1.5', '176', '12.9.9', '2.1') 
$MaxDots = ($Arr | % { $_.Split('.').count } | Measure-Object  -Maximum).Maximum

$sbl = for ($i = 0; $i -lt $MaxDots; $i++) {
  { [int] $_.Split('.')[$i] }.GetNewClosure()
}

$Arr | Sort-Object $sbl

Вот несколько решений с некоторыми оговорками.

The -Propertyпараметр Sort-Objectпринимает массив, поэтому вы можете указать "сортировать по... затем по...". Если вы знаете, что максимальное количество «субиндексов» равно 2 (т.е. x.y.z), то вы можете разбить строку на компоненты, разделенные .а затем последовательно сортировать по каждому компоненту как целое число . Повторяется, но работает...

      (
    @('1', '2', '3', '1.1.1', '1.99', '2.5', '5.5', "10") |
        Sort-Object -Property {
            # Sort by the first component
            return [Int32] $_.Split('.')[0]
        }, {
            # Sort by the second component
            return [Int32] $_.Split('.')[1]
        }, {
            # Sort by the third component
            return [Int32] $_.Split('.')[2]
        }
) -join ', '

Если компонент не указан (например, '1.2'.Split('.')[2]), то, кстати, становится 0при литье на [Int32].

Вот альтернатива, которая использует только один [ScriptBlock]но это также требует, чтобы была известна максимальная длина субиндекса в цифрах...

      $maxComponentCount = 3
$maxComponentDigits = 2

# Create a string of repeated '0's $maxComponentDigits long
$emptyComponentText = [String]::new([Char] '0', $maxComponentDigits)

(
    @('1', '2', '3', '1.1.1', '1.99', '2.5', '5.5', "10") |
        Sort-Object -Property {
            $components = [String[]] (
                $_.Split('.').ForEach(
                    # Pad each component to $maxComponentDigits digits
                    { $_.PadLeft($maxComponentDigits, '0') }
                )
            );
            
            if ($components.Length -lt $maxComponentCount)
            {
                # Pad $components up to $maxComponentCount with $emptyComponentText elements
                $components += ,$emptyComponentText * ($maxComponentCount - $components.Length)
            }

            # Join components - now $maxComponentCount elements of $maxComponentDigits digits - back into an index string
            return $components -join '.'
        }
) -join ', '

Это заполнение каждого входного индекса, чтобы иметь одинаковое количество субиндексов и каждый субиндекс, чтобы иметь одинаковое количество цифр, а затем полагаться на лексическую сортировку, чтобы расположить их в правильном порядке, так что это...

      @('1', '2', '3', '1.1.1', '1.99', '2.5', '5.5', "10")

... сортируется, как если бы это выглядело так...

      @('01.00.00', '02.00.00', '03.00.00', '01.01.01', '01.99.00', '02.05.00', '05.05.00', "10.00.00")

Я полагаю, вы могли бы установить оба $maxComponentCountа также $maxComponentDigitsсказать, 100если ни одно значение не известно, но это похоже на неуклюжий обходной путь (также с последствиями для производительности). Я постараюсь придумать что-нибудь получше.

      '1', '2', '3', '1.1.1', '1.99', '2.5', '5.5', "10" |Sort-Object { '{0:000}{1:000}{2:000}{3:000}' -f $([int[]]("$_.0.0.0".Split('.'))) }

Абсолютные ключи сортировки и относительное направление сортировки

Проблема, с которой мы сталкиваемся, заключается в том, что каждый [ScriptBlock]перешел к Sort-Objectполучает только одно входное значение за раз и возвращает ключевое значение для сортировки, но поскольку каждый индекс имеет переменное количество субиндексов, мы не можем предсказать, сколько уровней нам нужно сравнить, когда у нас есть значения. т еще не видел. Что нам нужно, так это способ определить, как два значения должны быть отсортированы относительно друг друга .

К счастью, .NET, на котором основана оболочка (Windows) PowerShell, определяет , используемый различными методами для определения порядка сортировки значений. Его единственный метод,Compare(), передаются два значения и возвращается, равны ли они или, в противном случае, какое из них предшествует другому. Все, что нам нужно сделать, это предоставить собственную (простую) реализациюа затем мы можем передать его встроенному методу сортировки.

реализация, использование и вывод теста

: .NET для индексных строк

      # Source: https://stackoverflow.com/a/71237565/150605
class HierarchicalIndexComparer : System.Collections.Generic.Comparer[String]
{
    <#
        Implements Comparer[String].Compare()
        See https://docs.microsoft.com/dotnet/api/system.collections.generic.comparer-1.compare
            When $x -lt $y, returns negative integer
            When $x -eq $y, returns 0
            When $x -gt $y, returns positive integer
    #>
    [Int32] Compare([String] $x, [String] $y)
    {
        # Split each index into components converted to integers with validation
        [Int32[]] $xValues = [HierarchicalIndexComparer]::GetComponentValues($x)
        [Int32[]] $yValues = [HierarchicalIndexComparer]::GetComponentValues($y)
        [Int32] $componentsToCompare = [Math]::Min($xValues.Length, $yValues.Length)

        for ($i = 0; $i -lt $componentsToCompare; $i++)
        {
            [Int32] $componentCompareResult = $xValues[$i].CompareTo($yValues[$i])

            # Sort $x and $y by the current component values if they are not equal
            if ($componentCompareResult -ne 0)
            {
                return $componentCompareResult
            }

            # Otherwise, continue with the next component
        }

        # The first $componentsToCompare elements of $x and $y are equal
        # Sort $x and $y by their component count
        return $xValues.Length.CompareTo($yValues.Length)
    }

    hidden static [Int32[]] GetComponentValues([String] $index)
    {
        return [Int32[]] (
            $index.Split('.').ForEach(
                {
                    if ($_.Length -lt 1)
                    {
                        throw "Index string ""$index"" contains an empty sub-index."
                    }

                    [Int32] $value = -1

                    # Leading zeroes will be removed by parsing and not considered when comparing components
                    if (-not [Int32]::TryParse($_, [System.Globalization.NumberStyles]::None, [System.Globalization.CultureInfo]::InvariantCulture, [Ref] $value))
                    {
                        throw "Sub-index ""$_"" of string ""$index"" contains non-digit characters."
                    }

                    return $value
                }
            )
        )
    }
}

Как видите, я использовал класс PowerShell для реализации для сортировки строк индекса; это вытекает из рекомендованного[Comparer[]]класс . См. последний раздел этого ответа для объяснения логики, используемой .

После того, как определил classто можно ссылаться как...

      [HierarchicalIndexComparer]

...и создается с помощью...

      New-Object -TypeName 'HierarchicalIndexComparer'

...или же...

      [HierarchicalIndexComparer]::new()

Сортировка строк тестового индекса с помощью

Теперь, когда у нас есть собственная реализация, мы можем передать ее экземпляр методу сортировки, чтобы он мог сортировать строки индекса. LINQ — это технология .NET, позволяющая выполнять операции с последовательностями почти так же, как Group-Object, Select-Object, а также Where-Objectкомандлеты работают с конвейерами PowerShell. LINQ предоставляет два метода:а такжеOrderByDescending(), для выполнения первичной сортировки последовательностей . После определения некоторых тестовых строк индекса нам просто нужно передать их и наш компаратор одному из этих методов, чтобы получить отсортированный вывод...

      [String[]] $initial = 1, 10, 100 |
    ForEach-Object -Process { "$_", "$_.0", "$_.0.0", "$_.0.0.0", "$_.0.1", "$_.1", "$_.1.0", "$_.1.1" }
# Shuffle the elements into a "random" order that is the same between runs
[String[]] $shuffled = Get-Random -Count $initial.Length -InputObject $initial -SetSeed 12345
[String[]] $sorted = [System.Linq.Enumerable]::OrderBy(
    $shuffled,                                         # The values to be ordered
    [Func[String, String]] {                           # Selects the key by which to order each value
        param($value)

        return $value                                  # Return the value as its own key
    },
    (New-Object -TypeName 'HierarchicalIndexComparer') # The comparer to perform the ordering
) | ForEach-Object -Process {
    # Just for demonstration purposes to show that further pipeline elements can be used after sorting
    return $_
}

for ($index = 0; $index -lt $initial.Length; $index++)
{
    [PSCustomObject] @{
        '#'         = $index
        '$initial'  = $initial[$index]
        '$shuffled' = $shuffled[$index]
        '$sorted'   = $sorted[$index]
    }
}

Преимущество использованияс обычаеминтерфейс[IComparer[]]интерфейсазаключается в том, что он обеспечит правильную сортировку за один проход по строкам вашего индекса, плюс вы можете направить вывод в последующие командлеты.

Вкратце, три параметра, передаваемые вOrderBy()[Enumerable]::OrderBy()находятся...

  1. Последовательность индексных строк для сортировки
  2. «Ключ», по которому сортируется каждый индекс
    • Мы хотим, чтобы каждая строка индекса сортировалась по самой строке (поскольку определяет порядок индекса). [String]экземпляры), поэтому мы просто returnто же самое значение, которое было передано нам; это похоже на ... | Sort-Object -Property { $_ }
    • См. раздел Можно ли использовать LINQ в PowerShell?для получения дополнительной информации о том, что здесь происходит
  3. Экземпляр нашего пользовательского компаратора индексов

Вы также можете использоватьArray::Sort()static , хотя он сортирует переданный ему массив на месте и ничего не возвращает для обработки конвейером. Здесь я сначала создам копию массива, чтобы его можно было отсортировать отдельно...

      # Create a copy of the $shuffled array named $sorted
$sorted = New-Object -TypeName 'System.String[]' -ArgumentList $shuffled.Length
[Array]::Copy($shuffled, $sorted, $shuffled.Length)

[Array]::Sort(
    $sorted,                                                      # The array to be sorted
    (New-Object -TypeName 'SO71232189.HierarchicalIndexComparer') # The comparer with which to sort it
)

Отсортированный результат тестовых строк индекса с использованием [HierarchicalIndexComparer]

Приведенный выше скрипт создает три массива, показывающие различные преобразования коллекции индексов...

Так как при сравнении первых 3-х компонентов $xа также $yмы обнаруживаем, что компонент 1 каждого из них не равен, мы возвращаем результат этого сравнения; $x[1] < $y[1]следовательно $x < $y, что правильно: "1.2.3 < 1.20.4".

Другие вопросы по тегам