Сортировка строк в стиле Powershell с подчеркиванием
Следующий список не сортируется должным образом (IMHO):
$a = @( 'ABCZ', 'ABC_', 'ABCA' )
$a | sort
ABC_
ABCA
ABCZ
У моей удобной диаграммы ASCII и Unicode C0 Controls и Basic Latin есть подчеркивание (нижняя линия) с порядковым номером 95 (U+005F). Это более высокое число, чем заглавные буквы AZ. Сортировка должна была поставить строку, заканчивающуюся подчеркиванием последней.
Get-Culture en-US
Следующий набор команд делает то, что я ожидаю:
$a = @( 'ABCZ', 'ABC_', 'ABCA' )
[System.Collections.ArrayList] $al = $a
$al.Sort( [System.StringComparer]::Ordinal )
$al
ABCA
ABCZ
ABC_
Теперь я создаю файл в кодировке ANSI, содержащий те же 3 строки:
Get-Content -Encoding Byte data.txt
65 66 67 90 13 10 65 66 67 95 13 10 65 66 67 65 13 10
$a = Get-Content data.txt
[System.Collections.ArrayList] $al = $a
$al.Sort( [System.StringComparer]::Ordinal )
$al
ABC_
ABCA
ABCZ
Еще раз строка, содержащая подчеркивание / нижнюю строку, не отсортирована правильно. Что мне не хватает?
Редактировать:
Давайте сослаться на этот пример № 4:
'A' -lt '_'
False
[char] 'A' -lt [char] '_'
True
Похоже, что оба утверждения должны быть ложными или оба должны быть истинными. Я сравниваю строки в первом утверждении, а затем сравниваю тип Char. Строка - это просто набор типов Char, поэтому я думаю, что две операции сравнения должны быть эквивалентны.
А теперь, например, № 5:
Get-Content -Encoding Byte data.txt
65 66 67 90 13 10 65 66 67 95 13 10 65 66 67 65 13 10
$a = Get-Content data.txt
$b = @( 'ABCZ', 'ABC_', 'ABCA' )
$a[0] -eq $b[0]; $a[1] -eq $b[1]; $a[2] -eq $b[2];
True
True
True
[System.Collections.ArrayList] $al = $a
[System.Collections.ArrayList] $bl = $b
$al[0] -eq $bl[0]; $al[1] -eq $bl[1]; $al[2] -eq $bl[2];
True
True
True
$al.Sort( [System.StringComparer]::Ordinal )
$bl.Sort( [System.StringComparer]::Ordinal )
$al
ABC_
ABCA
ABCZ
$bl
ABCA
ABCZ
ABC_
Два ArrayList содержат одинаковые строки, но сортируются по-разному. Зачем?
4 ответа
Во многих случаях PowerShell оборачивает / разворачивает объекты в / из PSObject
, В большинстве случаев это делается прозрачно, и вы даже не замечаете этого, но в вашем случае это является причиной вашей проблемы.
$a='ABCZ', 'ABC_', 'ABCA'
$a|Set-Content data.txt
$b=Get-Content data.txt
[Type]::GetTypeArray($a).FullName
# System.String
# System.String
# System.String
[Type]::GetTypeArray($b).FullName
# System.Management.Automation.PSObject
# System.Management.Automation.PSObject
# System.Management.Automation.PSObject
Как видите, объект, возвращенный из Get-Content
завернуты в PSObject
, что мешает StringComparer
видеть основные строки и сравнивать их должным образом. Строго набранная строка не может хранить PSObject
s, поэтому PowerShell будет разворачивать строки, чтобы хранить их в строго типизированной коллекции, что позволяет StringComparer
чтобы увидеть строки и сравнить их должным образом.
Редактировать:
Прежде всего, когда вы пишете, что $a[1].GetType()
или это $b[1].GetType()
Вы вызываете не методы.NET, а методы PowerShell, которые обычно вызывают методы.NET для обернутого объекта. Таким образом, вы не можете получить реальный тип объектов таким способом. Более того, их можно переопределить, рассмотрите этот код:
$c='String'|Add-Member -Type ScriptMethod -Name GetType -Value {[int]} -Force -PassThru
$c.GetType().FullName
# System.Int32
Давайте назовем методы.NET через отражение:
$GetType=[Object].GetMethod('GetType')
$GetType.Invoke($c,$null).FullName
# System.String
$GetType.Invoke($a[1],$null).FullName
# System.String
$GetType.Invoke($b[1],$null).FullName
# System.String
Теперь мы получаем реальный тип для $c
, но это говорит о том, что тип $b[1]
является String
не PSObject
, Как я уже сказал, в большинстве случаев распаковка выполняется прозрачно, поэтому вы видите завернутый String
и не PSObject
сам. Один конкретный случай, когда этого не происходит, заключается в следующем: когда вы передаете массив, то элементы массива не разворачиваются. Итак, давайте добавим дополнительный уровень косвенности здесь:
$Invoke=[Reflection.MethodInfo].GetMethod('Invoke',[Type[]]([Object],[Object[]]))
$Invoke.Invoke($GetType,($a[1],$null)).FullName
# System.String
$Invoke.Invoke($GetType,($b[1],$null)).FullName
# System.Management.Automation.PSObject
Теперь, когда мы проходим $b[1]
как часть массива, мы можем видеть его реальный тип: PSObject
, Хотя я предпочитаю использовать [Type]::GetTypeArray
вместо.
Около StringComparer
: как видите, когда не оба сравниваемых объекта являются строками, тогда StringComparer
полагаться на IComparable.CompareTo
для сравнения. А также PSObject
воплощать в жизнь IComparable
интерфейс, так что сортировка будет выполняться в соответствии с PSObject
IComparable
реализация.
Windows использует Unicode, а не ASCII, так что вы видите порядок сортировки Unicode для en-US. Общие правила сортировки:
- цифры, затем смешанные строчные и прописные
- Специальные символы появляются перед цифрами.
Продолжая свой пример,
$a = @( 'ABCZ', 'ABC_', 'ABCA', 'ABC4', 'abca' )
$a | sort-object
ABC_
ABC4
abca
ABCA
ABCZ
Если вы действительно хотите сделать это... Я признаю, что это некрасиво, но это работает. Я хотел бы создать функцию, если это то, что вам нужно делать на регулярной основе.
$ a = @ ('ABCZ', 'ABC_', 'ABCA', 'ab1z') $ ascii = @ ()
foreach ($ item in $ a) {$ string = "" for ($ i = 0; $ i -lt $ item.length; $ i ++) {$ char = [int] [char] $ item [$ i] $ string + = "$ char;" }
$ascii += $string
}
$ b = @ ()
foreach ($ item в $ascii | Sort-Object) { $string = "" $array = $item.Split(";") foreach ($char в $ массиве) { $string += [char] [int] $ символ}
$b += $string
}
$ a $ b
ABCA ABCZ ABC_
Я попробовал следующее, и сортировка, как и ожидалось:
[System.Collections.ArrayList] $al = [String[]] $a