Powershell Сортировка строк с подчеркиванием
Следующий список не сортируется должным образом (IMHO):
$a = @( 'ABCZ', 'ABC_', 'ABCA' )
$a | sort
ABC_
ABCA
ABCZ
Моя удобная диаграмма ASCII и элементы управления Unicode C0 и базовая латинская диаграмма
имеют нижнюю границу (нижняя строка) с порядковым номером 95 (U + 005F). Это больше, чем заглавные буквы A-Z. Сортировка должна была бы поместить строку, заканчивающуюся последним символом подчеркивания.
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
Еще раз строка, содержащая символ подчеркивания/нижней линии, не отсортирована правильно. Что мне не хватает?
Edit:
Позвольте ссылаться на этот пример # 4:
'A' -lt '_'
False
[char] 'A' -lt [char] '_'
True
Кажется, что оба утверждения должны быть False или оба должны быть 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 содержат одни и те же строки, но отсортированы по-разному. Почему?
Ответы
Ответ 1
Во многих случаях 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
not 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
.
Ответ 2
Windows использует Unicode, а не ASCII, поэтому то, что вы видите, - это порядок сортировки Unicode для en-US. Общие правила сортировки:
- затем строчные и прописные слова с надписью
- Специальные символы встречаются перед номерами.
Продолжая ваш пример,
$a = @( 'ABCZ', 'ABC_', 'ABCA', 'ABC4', 'abca' )
$a | sort-object
ABC_
ABC4
abca
ABCA
ABCZ
Ответ 3
Если вы действительно хотите это сделать... Я признаю это уродливым, но оно работает. Я бы создал функцию, если это то, что вам нужно делать на регулярной основе.
$a = @('ABCZ', 'ABC_', 'ABCA', 'ab1z')
$ ascii = @()
foreach ($ item в $a) { $ string = " for ($ я = 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 в $array) { $ string + = [char] [int] $char }
$b += $string
}
$а
$ Ьp >
ABCA
ABCZ
ABC _
Ответ 4
Я попробовал следующее, и сортировка будет такой, как ожидалось:
[System.Collections.ArrayList] $al = [String[]] $a