Swift - сортировка массива объектов с несколькими критериями
У меня есть массив объектов Contact
:
var contacts:[Contact] = [Contact]()
Класс контактов:
Class Contact:NSOBject {
var firstName:String!
var lastName:String!
}
И я хотел бы отсортировать этот массив lastName
, а затем firstName
, если некоторые контакты получили тот же lastName
.
Я могу сортировать один из этих критериев, но не оба.
contacts.sortInPlace({$0.lastName < $1.lastName})
Как я могу добавить дополнительные критерии для сортировки этого массива?
Спасибо.
Ответы
Ответ 1
Подумайте, что означает "сортировка по нескольким критериям". Это означает, что два объекта сначала сравниваются по одному критерию. Затем, если эти критерии одинаковы, связь будет нарушена следующими критериями и так далее, пока вы не получите желаемый порядок.
contacts.sortInPlace{ //sort(_:) in Swift 3
if $0.lastName != $1.lastName {
return $0.lastName < $1.lastName
}
/* last names are the same, break ties by foo
else if $0.foo != $1.foo {
return $0.foo < $1.foo
}
... repeat for all other fields in the sorting
*/
else { // All other fields are tied, break ties by last name
return $0.firstName < $1.firstName
}
}
Здесь вы используете метод sortInPlace(_:)
, который зависит от данного закрытия, чтобы определить сортировку. Если ваша сортировка будет использоваться во многих местах, возможно, лучше использовать метод sortInPlace()
, который работает только с экземплярами MutableCollectionType
, которые также соответствуют протоколу Comparable
. Таким образом, вы можете сортировать коллекцию Contact
, не повторяя код сортировки.
Ответ 2
Действительно простой способ выполнения сортировки по нескольким критериям (т.е. сортировка по одному сравнению и, если это эквивалентно, затем путем другого сравнения) заключается в использовании кортежей, поскольку операторы <
и >
имеют перегрузки для них, что выполнять лексикографические сравнения.
/// Returns a Boolean value indicating whether the first tuple is ordered
/// before the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).
public func < <A : Comparable, B : Comparable>(lhs: (A, B), rhs: (A, B)) -> Bool
Например:
struct Contact {
var firstName: String
var lastName: String
}
var contacts = [
Contact(firstName: "Charlie", lastName: "Webb"),
Contact(firstName: "Alex", lastName: "Elexson"),
Contact(firstName: "Charles", lastName: "Webb"),
Contact(firstName: "Alex", lastName: "Alexson")
]
// in Swift 2.x, sortInPlace(_:)
contacts.sort {
($0.lastName, $0.firstName) <
($1.lastName, $1.firstName)
}
print(contacts)
// [
// Contact(firstName: "Alex", lastName: "Alexson"),
// Contact(firstName: "Alex", lastName: "Elexson"),
// Contact(firstName: "Charles", lastName: "Webb"),
// Contact(firstName: "Charlie", lastName: "Webb")
// ]
Это сначала сравнит свойства элементов lastName
. Если они не равны, порядок сортировки будет основан на сравнении <
с ними. Если они равны, то он переместится на следующую пару элементов в кортеже, сравнивая свойства firstName
.
Стандартная библиотека обеспечивает перегрузки <
и >
для кортежей от 2 до 6 элементов.
Если вам нужны разные порядки сортировки для разных свойств, вы можете просто поменять элементы в кортежах:
contacts.sort {
($1.lastName, $0.firstName) <
($0.lastName, $1.firstName)
}
// [
// Contact(firstName: "Charles", lastName: "Webb"),
// Contact(firstName: "Charlie", lastName: "Webb"),
// Contact(firstName: "Alex", lastName: "Elexson"),
// Contact(firstName: "Alex", lastName: "Alexson")
// ]
Теперь будет сортироваться по lastName
по убыванию, затем firstName
по возрастанию.
Если вы собираетесь регулярно проводить подобные сравнения, то @AMomchilov и @appzYourLife, вы можете сопоставить Contact
с Comparable
:
extension Contact : Comparable {
static func == (lhs: Contact, rhs: Contact) -> Bool {
return (lhs.firstName, lhs.lastName) ==
(rhs.firstName, rhs.lastName)
}
static func < (lhs: Contact, rhs: Contact) -> Bool {
return (lhs.lastName, lhs.firstName) <
(rhs.lastName, rhs.firstName)
}
}
А теперь просто вызовите sort()
для возрастающего порядка:
// ascending
contacts.sort()
или sort(by: >)
для убывающего порядка:
// descending
contacts.sort(by: >)
Если у вас есть другие порядки сортировки, которые вы хотите использовать, вы можете определить их по вложенному типу:
extension Contact {
enum Comparison {
static let firstLastAscending: (Contact, Contact) -> Bool = {
return ($0.firstName, $0.lastName) <
($1.firstName, $1.lastName)
}
}
}
а затем просто вызывается как:
contacts.sort(by: Contact.Comparison.firstLastAscending)
Ответ 3
Единственное, что лексикографические сорта не могут сделать, как описано в @Hamish, - это обрабатывать различные направления сортировки, например сортировать по первому полю, нисходящему, следующему полю по возрастанию и т.д.
Я создал сообщение в блоге о том, как это сделать в Swift 3 и сохранить код простым и читаемым.
Вы можете найти его здесь:
http://master-method.com/index.php/2016/11/23/sort-a-sequence-i-e-arrays-of-objects-by-multiple-properties-in-swift-3/
Вы также можете найти репозиторий GitHub с кодом здесь:
https://github.com/jallauca/SortByMultipleFieldsSwift.playground
Суть всего этого, скажем, если у вас есть список мест, вы сможете это сделать:
struct Location {
var city: String
var county: String
var state: String
}
var locations: [Location] {
return [
Location(city: "Dania Beach", county: "Broward", state: "Florida"),
Location(city: "Fort Lauderdale", county: "Broward", state: "Florida"),
Location(city: "Hallandale Beach", county: "Broward", state: "Florida"),
Location(city: "Delray Beach", county: "Palm Beach", state: "Florida"),
Location(city: "West Palm Beach", county: "Palm Beach", state: "Florida"),
Location(city: "Savannah", county: "Chatham", state: "Georgia"),
Location(city: "Richmond Hill", county: "Bryan", state: "Georgia"),
Location(city: "St. Marys", county: "Camden", state: "Georgia"),
Location(city: "Kingsland", county: "Camden", state: "Georgia"),
]
}
let sortedLocations =
locations
.sorted(by:
ComparisonResult.flip <<< Location.stateCompare,
Location.countyCompare,
Location.cityCompare
)
Ответ 4
Я бы рекомендовал использовать решение хэмиш-кортежа, поскольку он не требует дополнительного кода.
Если вы хотите что-то, что ведет себя как if
statement, но упрощает логику ветвления, вы можете использовать это решение, которое позволяет вам сделать следующее:
animals.sort {
return comparisons(
compare($0.family, $1.family, ascending: false),
compare($0.name, $1.name))
}
Вот функции, которые позволяют вам сделать это:
func compare<C: Comparable>(_ value1Closure: @autoclosure @escaping () -> C, _ value2Closure: @autoclosure @escaping () -> C, ascending: Bool = true) -> () -> ComparisonResult {
return {
let value1 = value1Closure()
let value2 = value2Closure()
if value1 == value2 {
return .orderedSame
} else if ascending {
return value1 < value2 ? .orderedAscending : .orderedDescending
} else {
return value1 > value2 ? .orderedAscending : .orderedDescending
}
}
}
func comparisons(_ comparisons: (() -> ComparisonResult)...) -> Bool {
for comparison in comparisons {
switch comparison() {
case .orderedSame:
continue // go on to the next property
case .orderedAscending:
return true
case .orderedDescending:
return false
}
}
return false // all of them were equal
}
Если вы хотите проверить это, вы можете использовать этот дополнительный код:
enum Family: Int, Comparable {
case bird
case cat
case dog
var short: String {
switch self {
case .bird: return "B"
case .cat: return "C"
case .dog: return "D"
}
}
public static func <(lhs: Family, rhs: Family) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
struct Animal: CustomDebugStringConvertible {
let name: String
let family: Family
public var debugDescription: String {
return "\(name) (\(family.short))"
}
}
let animals = [
Animal(name: "Leopard", family: .cat),
Animal(name: "Wolf", family: .dog),
Animal(name: "Tiger", family: .cat),
Animal(name: "Eagle", family: .bird),
Animal(name: "Cheetah", family: .cat),
Animal(name: "Hawk", family: .bird),
Animal(name: "Puma", family: .cat),
Animal(name: "Dalmatian", family: .dog),
Animal(name: "Lion", family: .cat),
]
Основные отличия от решения Jamie заключаются в том, что доступ к свойствам определен как встроенный, а не как статический/экземплярный метод в классе. Например. $0.family
вместо Animal.familyCompare
. И восходящий/нисходящий управляется параметром, а не перегруженным оператором. Решение Jamie добавляет расширение на Array, тогда как мое решение использует встроенный метод sort
/sorted
, но требует двух дополнительных: compare
и comparisons
.
Для полноты, вот как мое решение сравнивается с решением хэмиш-кортежа. Чтобы продемонстрировать, я буду использовать дикий пример, где мы хотим сортировать людей с помощью (name, address, profileViews)
Решение Hamish будет оценивать каждое из 6 значений свойств ровно один раз до начала сравнения. Это может быть нежелательным или нежелательным. Например, если предположить, что profileViews
является дорогостоящим сетевым вызовом, мы можем избежать вызова profileViews
, если это абсолютно необходимо. Мое решение избежит оценки profileViews
до $0.name == $1.name
и $0.address == $1.address
. Однако, когда он оценивает profileViews
, он, скорее всего, будет оценивать еще много раз, чем один раз.
Ответ 5
Как насчет:
contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
Ответ 6
У этого вопроса уже много отличных ответов, но я хочу указать на статью - Сортировка дескрипторов в Swift. У нас есть несколько способов сортировки нескольких критериев.
-
Используя NSSortDescriptor, этот способ имеет некоторые ограничения, объект должен быть классом и наследуется от NSObject.
class Person: NSObject {
var first: String
var last: String
var yearOfBirth: Int
init(first: String, last: String, yearOfBirth: Int) {
self.first = first
self.last = last
self.yearOfBirth = yearOfBirth
}
override var description: String {
get {
return "\(self.last) \(self.first) (\(self.yearOfBirth))"
}
}
}
let people = [
Person(first: "Jo", last: "Smith", yearOfBirth: 1970),
Person(first: "Joe", last: "Smith", yearOfBirth: 1970),
Person(first: "Joe", last: "Smyth", yearOfBirth: 1970),
Person(first: "Joanne", last: "smith", yearOfBirth: 1985),
Person(first: "Joanne", last: "smith", yearOfBirth: 1970),
Person(first: "Robert", last: "Jones", yearOfBirth: 1970),
]
Здесь, например, мы хотим отсортировать фамилию, затем сначала имя, наконец, год рождения. И мы хотим сделать это без учета регистра и использовать локали пользователей.
let lastDescriptor = NSSortDescriptor(key: "last", ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
let firstDescriptor = NSSortDescriptor(key: "first", ascending: true,
selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
let yearDescriptor = NSSortDescriptor(key: "yearOfBirth", ascending: true)
(people as NSArray).sortedArray(using: [lastDescriptor, firstDescriptor, yearDescriptor])
// [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
-
Использование метода быстрой сортировки с именем/именем.
Этот способ должен работать как с классом/структурой. Тем не менее, мы не сортируем их по yearOfBirth здесь.
let sortedPeople = people.sorted { p0, p1 in
let left = [p0.last, p0.first]
let right = [p1.last, p1.first]
return left.lexicographicallyPrecedes(right) {
$0.localizedCaseInsensitiveCompare($1) == .orderedAscending
}
}
sortedPeople // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1985), Joanne smith (1970), Joe Smith (1970), Joe Smyth (1970)]
-
Быстрый способ ввода данных NSSortDescriptor. Это использует концепцию, что "функции являются первоклассным типом". SortDescriptor - это тип функции, принимает два значения, возвращает bool. Скажем sortByFirstName, мы берем два параметра ($ 0, $1) и сравниваем их имена. Комбинированные функции берут кучу SortDescriptors, сравнивают их все и дают заказы.
typealias SortDescriptor<Value> = (Value, Value) -> Bool
let sortByFirstName: SortDescriptor<Person> = {
$0.first.localizedCaseInsensitiveCompare($1.first) == .orderedAscending
}
let sortByYear: SortDescriptor<Person> = { $0.yearOfBirth < $1.yearOfBirth }
let sortByLastName: SortDescriptor<Person> = {
$0.last.localizedCaseInsensitiveCompare($1.last) == .orderedAscending
}
func combine<Value>
(sortDescriptors: [SortDescriptor<Value>]) -> SortDescriptor<Value> {
return { lhs, rhs in
for isOrderedBefore in sortDescriptors {
if isOrderedBefore(lhs,rhs) { return true }
if isOrderedBefore(rhs,lhs) { return false }
}
return false
}
}
let combined: SortDescriptor<Person> = combine(
sortDescriptors: [sortByLastName,sortByFirstName,sortByYear]
)
people.sorted(by: combined)
// [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
Это хорошо, потому что вы можете использовать его как с struct, так и с классом, вы можете даже расширить его для сравнения с nils.
Тем не менее, настоятельно рекомендуется прочитать оригинальную статью. Он имеет гораздо больше деталей и хорошо объяснил.
Ответ 7
Ниже приведен еще один простой подход для сортировки по 2 критериям.
Проверьте первое поле, в этом случае оно lastName
, если они не равны по типу lastName
, если lastName
равны, затем сортируйте по второму полю, в этом случае firstName
.
contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName }