Сортировка строк в стиле 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 интерфейс, так что сортировка будет выполняться в соответствии с PSObjectIComparable реализация.

Windows использует Unicode, а не ASCII, так что вы видите порядок сортировки Unicode для en-US. Общие правила сортировки:

  1. цифры, затем смешанные строчные и прописные
  2. Специальные символы появляются перед цифрами.

Продолжая свой пример,

$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
Другие вопросы по тегам