Swift - Сортировка массива объектов по нескольким критериям


92

У меня есть массив Contactобъектов:

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

Контактный класс:

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

И я хотел бы отсортировать этот массив lastNameпостепенно, firstNameесли у некоторых контактов будет то же самое lastName.

Я могу сортировать по одному из этих критериев, но не по обоим.

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

Как я могу добавить дополнительные критерии для сортировки этого массива?


2
Сделайте это точно так же, как вы только что сказали! В вашем коде внутри фигурных скобок должно быть указано: «Если фамилии совпадают, то сортируйте по имени; в противном случае сортируйте по фамилии».
Мэтт

4
Я вижу здесь несколько запахов кода: 1) Contactвероятно, не должно наследовать NSObject, 2) Contactвероятно, должно быть структурой и 3) firstNameи, lastNameвероятно, не должно быть неявно развернутыми опциями.
Александр - Восстановить Монику

3
@AMomchilov Нет никаких оснований предлагать, чтобы Contact был структурой, потому что вы не знаете, полагается ли остальная часть его кода на ссылочную семантику при использовании ее экземпляров.
Патрик Гоули

3
@AMomchilov "Вероятно" вводит в заблуждение, потому что вы точно ничего не знаете об остальной кодовой базе. Если он изменен на структуру, все внезапные копии создаются при изменении переменных вместо изменения экземпляра под рукой. Это радикальное изменение в поведении, и выполнение этого "вероятно" приведет к ошибкам, поскольку маловероятно, что все было правильно запрограммировано как для семантики ссылок, так и для значений.
Патрик Гоули

1
@AMomchilov Еще не слышал ни одной причины, почему это, вероятно, должна быть структура. Я не думаю, что OP был бы признателен за предложения, которые изменяют семантику остальной части его программы, особенно когда в этом даже не было необходимости решать возникшую проблему. Не осознавал, что правила компилятора для некоторых легальны ... может быть, я не на том веб-сайте
Патрик Гоули

Ответы:


120

Подумайте, что означает «сортировка по нескольким критериям». Это означает, что сначала сравниваются два объекта по одному критерию. Затем, если эти критерии совпадают, связи будут разорваны по следующим критериям, и так далее, пока вы не получите желаемый порядок.

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.<(_:_:)оператора, чтобы определить, как сравниваются элементы. Таким образом, вы можете сортировать любой Sequenceиз Contacts без необходимости дублировать код сортировки.


2
elseТело должно быть в пределах , { ... }иначе код не компилируется.
Лука Анджелетти

Понял. Я попытался реализовать это, но не смог понять синтаксис. Большое спасибо.
sbkl 03

для sortvs. sortInPlaceсм. здесь . АОЛО увидеть это ниже, это гораздо более модульным
Мед

sortInPlaceбольше НЕ доступен в Swift 3, вместо него вы должны использовать sort().sort()изменит сам массив. Также есть новая функция с именем, sorted()которая будет возвращать отсортированный массив
Honey

2
@AthanasiusOfAlex Использование ==- не лучшая идея. Работает только для 2 объектов. Более того, и вы начинаете повторяться с множеством составных логических выражений
Александр - Восстановите Монику

123

Использование кортежей для сравнения нескольких критериев

Действительно простой способ выполнения сортировать по нескольким критериям (например , сортировка по одному сравнения, и если эквивалентны, то другим сравнения) является использование кортежей , как <и >операторы перегрузки для них , которые выполняют лексикографические сравнения.

/// 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: "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 элементов.

Если вам нужны разные порядки сортировки для разных свойств, вы можете просто поменять местами элементы в кортежах:

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

Теперь он будет отсортирован по lastNameубыванию, а затем по firstNameвозрастанию.


Определение sort(by:)перегрузки, которая принимает несколько предикатов

Вдохновленный обсуждение SORTING коллекций с mapзатворами и SortDescriptors , еще одним вариантом было бы определить пользовательскую перегрузку 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 {
  mutating 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)

contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Помогло. Спасибо
Прабхакар Каши

Если , как я, свойства должны быть отсортированы являются опциями, то вы могли бы сделать что - то вроде этого: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
BobCowe

Холли Молли! Так просто, но так эффективно ... почему я никогда об этом не слышал ?! Большое спасибо!
Ethenyl

@BobCowe Это оставляет вас во власти ""сравнения с другими строками (это предшествует непустым строкам). Это своего рода неявное, своего рода магия и негибкость, если вы хотите, nilчтобы вместо этого s располагались в конце списка. Я рекомендую вам взглянуть на мою nilComparatorфункцию stackoverflow.com/a/44808567/3141234
Александр - Восстановить Монику

19

Еще один простой подход для сортировки по 2 критериям показан ниже.

Проверьте первое поле, в данном случае оно есть lastName, если они не равны, сортировать по lastName, если lastNameравны, то в данном случае сортировать по второму полю firstName.

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

Это дает больше гибкости, чем кортежи.
Бабач,

5

Единственное, что лексикографическая сортировка не может сделать, как описано в @Hamish, - это обрабатывать разные направления сортировки, скажем, сортировку по первому полю по убыванию, по следующему полю по возрастанию и т. Д.

Я создал сообщение в блоге о том, как это сделать в Swift 3, и сохраняю код простым и читаемым.

Вы можете найти это здесь:

http://master-method.com/index.php/2016/11/23/sort-a-sequence-ie-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
        )

1
«Единственное, чего лексикографические сортировки не могут сделать, как описано в @Hamish, - это обрабатывать разные направления сортировки» - да, они могут, просто поменяйте местами элементы в кортежах;)
Хэмиш

Я считаю, что это интересное теоретическое упражнение, но намного сложнее, чем ответ @ Hamish. На мой взгляд, чем меньше кода, тем лучше.
Мануэль

5

На этот вопрос уже есть много отличных ответов, но я хочу указать на статью - Дескрипторы сортировки в 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. Использование быстрого способа сортировки по фамилии / имени. Этот способ должен работать как с классом / структурой. Однако здесь мы не сортируем по году рождения.

    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. Быстрый способ установки NSSortDescriptor. Здесь используется концепция, согласно которой «функции - это первоклассный тип». SortDescriptor - это тип функции, принимает два значения, возвращает логическое значение. Скажем, 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)]
    

    Это хорошо, потому что вы можете использовать его как со структурой, так и с классом, вы даже можете расширить его для сравнения с nils.

Тем не менее, настоятельно рекомендуется прочитать оригинальную статью . Там гораздо больше деталей и хорошо объяснено.


2

Я бы рекомендовал использовать решение для кортежей Хэмиша, поскольку оно не требует дополнительного кода.


Если вам нужно что-то, что ведет себя как ifоператоры, но упрощает логику ветвления, вы можете использовать это решение, которое позволяет вам делать следующее:

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. И возрастание / убывание контролируется параметром, а не перегруженным оператором. Решение Джейми добавляет расширение к массиву, тогда как мое решение использует встроенный метод sort/, sortedно требует определения двух дополнительных: compareи comparisons.

Для полноты картины вот как мое решение сравнивается с решением кортежа Хэмиша . Чтобы продемонстрировать, я буду использовать дикий пример, в котором мы хотим отсортировать людей по (name, address, profileViews)решению Хэмиша, которое оценит каждое из 6 значений свойств ровно один раз перед началом сравнения. Это может быть нежелательно или нежелательно. Например, предполагая, что profileViewsсетевой вызов является дорогостоящим, мы можем избежать profileViewsего, если это не является абсолютно необходимым. Мое решение позволит избежать оценки profileViewsдо $0.name == $1.nameи $0.address == $1.address. Однако, когда он действительно оценивает, profileViewsон, вероятно, будет оценивать гораздо больше раз, чем один раз.


1

Как насчет:

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

lexicographicallyPrecedesтребует, чтобы все типы в массиве были одинаковыми. Например [String, String]. OP, вероятно, хочет смешивать и сопоставлять типы: [String, Int, Bool]чтобы они могли это сделать [$0.first, $0.age, $0.isActive].
Senseful

-1

это сработало для моего массива [String] в Swift 3, и кажется, что в Swift 4 все в порядке

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

Вы прочитали вопрос перед тем, как ответить? Сортируйте по нескольким параметрам, а не по одному, что вы представляете.
Vive
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.