Swift - Sort array of objects with multiple criteria

I have an array of Contact objects:

var contacts:[Contact] = [Contact]()

Contact class:

Class Contact:NSOBject {
var firstName:String!
var lastName:String!
}

And I would like to sort that array by lastName and then by firstName in case some contacts got the same lastName.

I'm able to sort by one of those criteria, but not both.

contacts.sortInPlace({$0.lastName < $1.lastName})

How could I add more criteria to sort this array?

74916 次浏览

想想“按多个标准排序”是什么意思。这意味着两个对象首先按照一个标准进行比较。然后,如果这些条件是相同的,那么下一个条件将打破关系,依此类推,直到您得到所需的顺序。

let sortedContacts = contacts.sort {
if $0.lastName != $1.lastName { // first, compare by last names
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
}
}

您在这里看到的是 Sequence.sorted(by:),它参考提供的闭包来确定元素的比较方式。

如果您的排序将在许多地方使用,最好使您的类型符合 Comparable协议。这样,您可以使用 Sequence.sorted()方法,它将参考 Comparable.<(_:_:)接线员的实现来确定元素的比较方式。这样,您就可以对 Contact的任何 Sequence进行排序,而无需复制排序代码。

这个怎么样:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }

使用元组对多个条件进行比较

按多个标准执行排序的一个非常简单的方法(即按一个比较进行排序,如果等价,则按另一个比较进行排序)是使用 元组,因为 <>操作符有执行字典比较的重载。

/// 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

For example:

struct Contact {
var firstName: String
var lastName: String
}


var contacts = [
Contact(firstName: "Leonard", lastName: "Charleson"),
Contact(firstName: "Michael", lastName: "Webb"),
Contact(firstName: "Charles", lastName: "Alexson"),
Contact(firstName: "Michael", lastName: "Elexson"),
Contact(firstName: "Alex", lastName: "Elexson"),
]


contacts.sort {
($0.lastName, $0.firstName) <
($1.lastName, $1.firstName)
}


print(contacts)


// [
//   Contact(firstName: "Charles", lastName: "Alexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Webb")
// ]

这将首先比较元素的 lastName属性。如果它们不相等,那么排序顺序将基于与它们的 <比较。如果它们 相等,那么它将移动到元组中的下一对元素,即比较 firstName属性。

标准库为2到6个元素的元组提供 <>重载。

If you want different sorting orders for different properties, you can simply swap the elements in the tuples:

contacts.sort {
($1.lastName, $0.firstName) <
($0.lastName, $1.firstName)
}


// [
//   Contact(firstName: "Michael", lastName: "Webb")
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Charles", lastName: "Alexson"),
// ]

This will now sort by lastName descending, then firstName ascending.


定义采用多个谓词的 sort(by:)重载

受到关于 使用 map闭包和 SortDescriptor 对集合排序的讨论的启发,另一种选择是定义一个 sort(by:)sorted(by:)的自定义重载,用于处理多个谓词——依次考虑每个谓词以决定元素的顺序。

extension MutableCollection where Self : RandomAccessCollection {
mutating func sort(
by firstPredicate: (Element, Element) -> Bool,
_ secondPredicate: (Element, Element) -> Bool,
_ otherPredicates: ((Element, Element) -> Bool)...
) {
sort(by:) { lhs, rhs in
if firstPredicate(lhs, rhs) { return true }
if firstPredicate(rhs, lhs) { return false }
if secondPredicate(lhs, rhs) { return true }
if secondPredicate(rhs, lhs) { return false }
for predicate in otherPredicates {
if predicate(lhs, rhs) { return true }
if predicate(rhs, lhs) { return false }
}
return false
}
}
}

extension Sequence {
func sorted(
by firstPredicate: (Element, Element) -> Bool,
_ secondPredicate: (Element, Element) -> Bool,
_ otherPredicates: ((Element, Element) -> Bool)...
) -> [Element] {
return sorted(by:) { lhs, rhs in
if firstPredicate(lhs, rhs) { return true }
if firstPredicate(rhs, lhs) { return false }
if secondPredicate(lhs, rhs) { return true }
if secondPredicate(rhs, lhs) { return false }
for predicate in otherPredicates {
if predicate(lhs, rhs) { return true }
if predicate(rhs, lhs) { return false }
}
return false
}
}
}

(secondPredicate:参数很不幸,但是为了避免与现有的 sort(by:)重载一起产生歧义,需要使用该参数)

这样我们就可以说(使用前面的 contacts数组) :

contacts.sort(by:
{ $0.lastName > $1.lastName },  // first sort by lastName descending
{ $0.firstName < $1.firstName } // ... then firstName ascending
// ...
)


print(contacts)


// [
//   Contact(firstName: "Michael", lastName: "Webb")
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Charles", lastName: "Alexson"),
// ]


// or with sorted(by:)...
let sortedContacts = contacts.sorted(by:
{ $0.lastName > $1.lastName },  // first sort by lastName descending
{ $0.firstName < $1.firstName } // ... then firstName ascending
// ...
)

尽管调用站点不像元组变体那样简洁,但是您可以更清楚地了解比较的内容和顺序。


符合 Comparable

如果你打算定期进行这些比较,那么,就像 @ 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()获得升序顺序:

contacts.sort()

sort(by: >)表示降序:

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)

词典排序不能像@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
)

我建议使用 哈米什的元组解,因为它不需要额外的代码。


如果您希望某些东西的行为类似于 if statements,但是要简化分支逻辑,您可以使用这个解决方案,它允许您执行以下操作:

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),
]

杰米的解决方案的主要区别在于,对属性的访问是内联定义的,而不是作为类上的静态/实例方法。例如 $0.family而不是 Animal.familyCompare。升序/降序由参数控制,而不是由重载运算符控制。Jamie 的解决方案在 Array 上添加了一个扩展,而我的解决方案使用内置的 sort/sorted方法,但是需要另外两个方法来定义: comparecomparisons

为了完整起见,下面是我的解决方案与 哈米什的元组解的比较。为了演示,我将使用一个野生的例子,其中我们希望根据 (name, address, profileViews)对人进行排序,Hamish 的解决方案将在比较开始前精确地计算6个属性值中的每一个。这可能是不希望的,也可能是不希望的。例如,假设 profileViews是一个昂贵的网络调用,我们可能希望避免调用 profileViews,除非它是绝对必要的。我的解决方案将避免评估 profileViews直到 $0.name == $1.name$0.address == $1.address。然而,当它评估 profileViews时,它可能会评估多次。

这个问题已经有很多很好的答案,但我想指出一篇文章-在 Swift 中排序描述符。我们有几种方法来进行多准则排序。

  1. 使用 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)]
    
  2. Using Swift way of sorting with last name/first name . This way should work with both class/struct. However, we don't sort by yearOfBirth here.

    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)]
    
  3. Swift way to inmitate NSSortDescriptor. This uses the concept that 'functions are a first-class type'. SortDescriptor is a function type, takes two values, returns a bool. Say sortByFirstName we take two parameters($0,$1) and compare their first names. The combine functions takes a bunch of SortDescriptors, compare all of them and give orders.

    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 和 class 一起使用,甚至可以扩展它来与 nils 进行比较。

尽管如此,强烈建议阅读 original article。它有更多的细节和很好的解释。

下面显示了使用2个条件进行排序的另一种简单方法。

检查第一个字段,在本例中是 lastName,如果它们不相等,按 lastName排序,如果 lastName相等,那么按第二个字段排序,在本例中是 firstName

contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }

我的数组[字符串]在 Swift 3和它似乎在 Swift 4是确定的

array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}