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