Различные способы сортировки массива строк в Swift

Как отсортировать массив строк

В Swift есть два способа сортировки: тот, который изменяет исходный массив, и тот, который этого не делает. У них обоих было одно и то же требование: элемент в коллекции должен соответствовать протоколу Comparable.

Типы, соответствующие протоколу Comparable, можно сравнивать с помощью операторов отношения <, <=, >= и >. Многие типы в стандартной библиотеке, включая String, соответствуют протоколу Comparable.

Изменяемая сортировка с sort и sort(by:)

sort() и sort(by:) — это функции сортировки, которые изменяют исходный массив. Чтобы использовать его, вызовите sort() для изменяемого значения массива.

var students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"] // <1>
students.sort()
print(students)
// Prints "["Abena", "Akosua", "Kofi", "Kweku", "Peter"]"

<1> Так как sort() является изменяемой функцией, вы должны объявить массив как var. В противном случае вы получите следующую ошибку.

Cannot use mutating member on immutable value: 'students' is a 'let' constant

Change 'let' to 'var' to make it mutable

sort() будет использовать оператор «меньше» (<) при сравнении элементов, что приведет к сортировке по возрастанию. Чтобы отсортировать в порядке убывания, вам нужно использовать метод sort(by:), который позволяет указать замыкание сравнения, которое возвращает true, если его первый аргумент должен быть упорядочен перед вторым аргументом.

var students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
students.sort { (lhs: String, rhs: String) -> Bool in
    return lhs > rhs
}
// Prints "["Peter", "Kweku", "Kofi", "Akosua", "Abena"]"

Неизменяемая сортировка с sort и sort(by:)

sorted() и sorted(by:) имеют ту же функциональность, что и sort() и sort(by:). Единственное отличие состоит в том, что они возвращают новые отсортированные элементы последовательности вместо модификации исходного массива.

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"] // <1>
let sortedStudents = students.sorted() // <2>
print(students) 
// Prints "["Kofi", "Abena", "Peter", "Kweku", "Akosua"]" <3>

print(sortedStudents) 
// Prints "["Abena", "Akosua", "Kofi", "Kweku", "Peter"]" <4>

<1> Здесь можно использовать let, так как sorted() и sorted(by:) не изменяют исходный массив.

<2> Нам нужно объявить новую переменную для чтения отсортированного результата.

<3> Исходный массив не изменяется.

<4> sorted() возвращает новый отсортированный массив.

Как и sort(), sorted() использует оператор «меньше» (<) при сравнении элементов, что приводит к сортировке по возрастанию. Вам нужно использовать sort(by:), если вы хотите изменить порядок элементов.

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
let sortedStudents = students.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs > rhs
}
print(sortedStudents) 
// Prints "["Peter", "Kweku", "Kofi", "Akosua", "Abena"]"

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

Как отсортировать массив по возрастанию

Вы можете использовать sort() или sorted() для сортировки строк в порядке возрастания.

var students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
let sortedStudents = students.sorted()
students.sort()
print(students) 
// Prints "["Abena", "Akosua", "Kofi", "Kweku", "Peter"]"

print(sortedStudents)
// Prints "["Abena", "Akosua", "Kofi", "Kweku", "Peter"]"

Как отсортировать массив по убыванию

Вы можете сортировать в порядке убывания, указав замыкание сортировки либо для sort(by:), либо для sorted(by:).

var students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
let sortedStudents = students.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs > rhs
}
students.sort { (lhs: String, rhs: String) -> Bool in
    return lhs > rhs
}
print(students) 
// Prints "["Peter", "Kweku", "Kofi", "Akosua", "Abena"]"

print(sortedStudents) 
// Prints "["Peter", "Kweku", "Kofi", "Akosua", "Abena"]"

Как изменить порядок сортировки в Swift

Вы можете инвертировать элементы массива, используя reverse(), который изменяет исходный массив или неизменяемый вариант, reversed().

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
let ascStudents = students.sorted()
let dscStudents = Array(ascStudents.reversed())
print(ascStudents) 
// Prints "["Abena", "Akosua", "Kofi", "Kweku", "Peter"]"

print(dscStudents) 
// Prints "["Peter", "Kweku", "Kofi", "Akosua", "Abena"]"

Как отсортировать массив по алфавиту

Порядок возрастания может быть не тем порядком, который вы ищете при сортировке массива строк.

Вот пример массива имен, которые содержат как прописные, так и строчные буквы.

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua", "abena", "bee"]
let sortedStudents = students.sorted()

print(sortedStudents) 
// Prints "["Abena", "Akosua", "Kofi", "Kweku", "Peter", "abena", "bee"]"

Как видите, отсортированный результат чувствителен к регистру, где «Abena» и «abena» разделены. В этом случае вам нужно больше контроля над методами сравнения. К счастью, в Swift есть много встроенных методов сравнения.

caseInsensitiveCompare(_:) сравнивает две строки без учета регистра. Это краткая форма вызова compare(_:options:) с .caseInsensitive в качестве единственной опции.

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua", "abena", "bee"]
let sortedStudents = students.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.caseInsensitiveCompare(rhs) == .orderedAscending
}
print(sortedStudents) 
// Prints "["Abena", "abena", "Akosua", "bee", "Kofi", "Kweku", "Peter"]"

Вы также можете использовать compare(_:options:) с .caseInsensitive как единственную опцию, которая дает тот же результат.

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua", "abena", "bee"]
let sortedStudents = students.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.compare(rhs, options: .caseInsensitive) == .orderedAscending
}
print(sortedStudents) 
// Prints "["Abena", "abena", "Akosua", "bee", "Kofi", "Kweku", "Peter"]"

caseInsensitiveCompare(_:) дает результат, который нам нужен для этого небольшого выборочного массива, но может быть неправильным методом для того, что нам нужно. На самом деле сортировка по имени или строке намного сложнее. В некоторых странах может использоваться диакритический знак, усложняющий сортировку.

Давайте посмотрим, что произойдет после того, как мы добавим «abenā» в массив наших учеников. Я использую «abenā» в качестве примера, чтобы показать случай с диакритическим знаком. Возможно, это неверное имя.

let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua", "abena", "bee", "ábenā"]
let sortedStudents = students.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.caseInsensitiveCompare(rhs) == .orderedAscending
}
print(sortedStudents) 
// Prints "["Abena", "abena", "Akosua", "ábenā", "bee", "Kofi", "Kweku", "Peter"]"

Как видите, «abenā» отделяется от «abena» и «abena». Вы можете указать параметр .diacriticInsensitive, чтобы игнорировать диакритический знак, но вы можете видеть, что сложность увеличивается и увеличивается по мере того, как мы пытаемся обработать больше случаев.

let sortedStudents = files.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.compare(rhs, options: [.diacriticInsensitive, .caseInsensitive]) == .orderedAscending
}

Нужно ли нам знать все особенности языка, чтобы уметь их сортировать?

К счастью, нет. Что нам действительно нужно здесь, так это метод сравнения с учетом локали. Apple предоставила вам три метода сравнения с учетом региональных настроек. localizedCompare, localizedCaseInsensitiveCompare и localizedStandardCompare.

let files = ["Kofi", "Abena", "Peter", "Kweku", "Akosua", "abena", "bee", "ábenā"]
let sorted = files.sorted()
let compare = files.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.compare(rhs) == .orderedAscending
}
let caseInsensitiveCompare = files.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.caseInsensitiveCompare(rhs) == .orderedAscending
}

let localizedCompare = files.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.localizedCompare(rhs) == .orderedAscending
}
let localizedCaseInsensitiveCompare = files.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.localizedCaseInsensitiveCompare(rhs) == .orderedAscending
}
let localizedStandardCompare = files.sorted { (lhs: String, rhs: String) -> Bool in
    return lhs.localizedStandardCompare(rhs) == .orderedAscending
}

результат

sortedcomparecaseInsensitiveComparelocalizedComparelocalizedCaseInsensitiveComparelocalizedStandardCompare
AbenaAbenaAbenaabenaAbenaabena
AkosuaAkosuaabenaAbenaabenaAbena
KofiKofiAkosuaábenāábenāábenā
KwekuKwekuábenāAkosuaAkosuaAkosua
PeterPeterbeebeebeebee
abenaabenaKofiKofiKofiKofi
beeábenāKwekuKwekuKwekuKweku
ábenābeePeterPeterPeterPeter

Как видите, все три метода сравнения с учетом локали, localizedCompare, localizedCaseInsensitiveCompare и localizedStandardCompare, дают приемлемый результат сортировки.

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

sortedcomparecaseInsensitiveComparelocalizedComparelocalizedCaseInsensitiveComparelocalizedStandardCompare
มามามามามามา
มีมีมีมีมีมี
มี่มี่มี่มี่มี่มี่
มี้มี้มี้มี้มี้มี้
มี๊มี๊มี๊มี๊มี๊มี๊
มี๋มี๋มี๋มี๋มี๋มี๋
หมกหมกหมกเมเมเม
หมีหมีหมีแมแมแม
หมี่หมี่หมี่หมกหมกหมก
หมึกหมึกหมึกหมีหมีหมี
เมเมเมหมี่หมี่หมี่
แมแมแมหมึกหมึกหมึก

Вы можете увидеть их как тарабарщину, но вы можете видеть в строках 7, 8 и 9, что существует разница в порядке между сравнением с учетом локали и без нее.

Существует три метода с учетом локали. Какой из них я должен использовать?

localizedStandardCompare — это то, что вы должны использовать. Причина в следующем разделе.

Как отсортировать имя файла или строку с номерами

Если ваша строка содержит числа, такие как Name2.txt, Name7.txt и Name25.txt, вы хотите, чтобы они сортировались следующим образом:

Name2.txt
Name7.txt
Name25.txt

а нет так:

Name2.txt
Name25.txt
Name7.txt

Здесь на помощь приходит localizedStandardCompare. localizedStandardCompare сравнивает строки так же, как это делает Finder. Который уже обрабатывает такого рода числовую сортировку.

let files = ["Design final.psd", "untitled01.txt", "untitled1.txt", "design final.psd", "design final(2).psd", "design final final.psd", "design final2.psd", "design final12.psd", "untitled2.txt", "untitled21.txt", "untitled11.txt", "untitled012.txt", "untitled.txt", "Design Final2.psd", "DESIGN final final.psd", "untitled12.psd"]

let sortedFiles = files.sorted { (lhs: String, rhs: String) -> Bool in    
    return lhs.localizedStandardCompare(rhs) == .orderedAscending
}

Вы можете добиться аналогичного эффекта с помощью параметра compare(_:options:range:locale:) .numeric.

let custom = files.sorted { (lhs: String, rhs: String) -> Bool in    
    return lhs.compare(rhs, options: [.numeric], locale: .current) == .orderedAscending
}

Существует небольшая разница между localizedStandardCompare и compare(_:options:range:locale:) с опцией .numeric, как вы можете видеть в следующем результате (untitled01.txt и untitled1.txt).

Результат:

caseInsensitiveComparelocalizedComparelocalizedCaseInsensitiveComparelocalizedStandardComparecustom
design final final.psddesign final final.psddesign final final.psddesign final final.psddesign final final.psd
DESIGN final final.psdDESIGN final final.psdDESIGN final final.psdDESIGN final final.psdDESIGN final final.psd
design final(2).psddesign final.psdDesign final.psddesign final.psddesign final.psd
Design final.psdDesign final.psddesign final.psdDesign final.psdDesign final.psd
design final.psddesign final(2).psddesign final(2).psddesign final(2).psddesign final(2).psd
design final12.psddesign final12.psddesign final12.psddesign final2.psddesign final2.psd
design final2.psddesign final2.psddesign final2.psdDesign Final2.psdDesign Final2.psd
Design Final2.psdDesign Final2.psdDesign Final2.psddesign final12.psddesign final12.psd
untitled.txtuntitled.txtuntitled.txtuntitled.txtuntitled.txt
untitled01.txtuntitled01.txtuntitled01.txtuntitled1.txtuntitled01.txt
untitled012.txtuntitled012.txtuntitled012.txtuntitled01.txtuntitled1.txt
untitled1.txtuntitled1.txtuntitled1.txtuntitled2.txtuntitled2.txt
untitled11.txtuntitled11.txtuntitled11.txtuntitled11.txtuntitled11.txt
untitled12.psduntitled12.psduntitled12.psduntitled12.psduntitled12.psd
untitled2.txtuntitled2.txtuntitled2.txtuntitled012.txtuntitled012.txt
untitled21.txtuntitled21.txtuntitled21.txtuntitled21.txtuntitled21.txt

Вывод

Итак, вот правило большого пальца. Когда вам нужно отсортировать или работать с текстом, представленным пользователю, используйте localizedStandardCompare(_:). Он сравнивает ваши строки таким образом, который имеет смысл и является ожидаемым пользователями с учетом их региональных настроек. localizedStandardCompare(_:) даст вам тот же результат, что и System Finder. Так что его использование даст результат, к которому уже привыкли пользователи Apple.

Если у вас есть конкретная потребность, которая не соответствует тому, что предоставляет localizedStandardCompare(_:), вы можете использовать compare(_:options:range:locale:). Но, пожалуйста, убедитесь, что вы передаете языковой стандарт пользователя в качестве аргумента, чтобы получить функцию с учетом языкового стандарта.

let custom = files.sorted { (lhs: String, rhs: String) -> Bool in
    let options: String.CompareOptions = [] // customize options
    return lhs.compare(rhs, options: options, locale: Locale.current) == .orderedAscending
}