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
Робіть це точно так само, як ви щойно сказали! У коді всередині фігурних дужок має бути написано: "Якщо прізвища однакові, сортуйте за іменем, інакше сортуйте за прізвищем".
matt

4
Я бачу тут кілька запахів коду: 1), Contactмабуть, не слід успадковувати від NSObject, 2), Contactмабуть, має бути структура, і 3) firstNameі, lastNameмабуть, не слід неявно розгортати необов’язкові.
Олександр - Відновити Моніку

3
@AMomchilov Немає підстав вважати, що Contact повинен бути структурою, оскільки ви не знаєте, чи решта його коду вже покладається на посилальну семантику при використанні його екземплярів.
Patrick Goley

3
@AMomchilov "Напевно" вводить в оману, оскільки ви точно нічого не знаєте про решту кодової бази. Якщо його змінити на структуру, усі раптові копії генеруються при мутації vars, замість того, щоб модифікувати відповідний екземпляр. Це кардинальна зміна поведінки та здійснення, яка "ймовірно" призведе до помилок, оскільки навряд чи все було правильно кодовано як для посилальної, так і для семантики значень.
Патрік Голі,

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

Відповіді:


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зContact s, не дублюючи код сортування.


2
Текст elseповинен бути між, { ... }інакше код не компілюється.
Luca Angeletti

Зрозумів. Я намагався реалізувати це, але не зміг правильно зрозуміти синтаксис. Дуже дякую.
sbkl

для sortпроти sortInPlaceдив. тут . Як бачимо це нижче, це набагато модульніше
Мед

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

2
@AthanasiusOfAlex Використання ==- це не гарна ідея. Він працює лише для 2 властивостей. Більше того, і ти починаєш повторювати себе з великою кількістю складених булевих виразів
Олександр - Відновити Моніку

122

Використання кортежів для порівняння кількох критеріїв

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

/// 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:)перевантаження, яке приймає кілька предикатів

Натхненний дискусією про сортування колекцій із 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

Холлі Моллі! Такий простий, але такий ефективний ... чому я ніколи про це не чув ?! Дуже дякую!
Етеніл

@BobCowe Це залишає на вас милість щодо ""порівняння з іншими рядками (це перед непустими рядками). Це якось неявно, своєрідно магічно і негнучко, якщо ви хочете, щоб nils замість цього з’явилися в кінці списку. Рекомендую поглянути на мою nilComparatorфункцію stackoverflow.com/a/44808567/3141234
Олександр - Поновити Моніку

19

Інший простий підхід до сортування за двома критеріями показано нижче.

Перевірте перше поле, у цьому випадку це так 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. Використання швидкого способу сортування з прізвищем / ім’ям. Цей спосіб повинен працювати як з класом, так і зі структурою. Однак ми тут не сортуємо за 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)]
    
  3. Швидкий спосіб наслідування 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)]
    

    Це добре, тому що ви можете використовувати його як зі структурою, так і з класом, ви навіть можете розширити його для порівняння з nils.

І все-таки настійно рекомендується прочитати оригінальну статтю . У ньому набагато більше деталей і добре пояснено.


2

Я б рекомендував використовувати рішення кортежу Hamish, оскільки воно не вимагає додаткового коду.


Якщо ви хочете щось, що поводиться як 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. А зростаючий / спадний керується параметром, а не перевантаженим оператором. Рішення Джеймі додає розширення для Array, тоді як моє рішення використовує вбудований 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]. Що, мабуть, хоче ОП, це поєднання типів і поєднання: [String, Int, Bool]щоб вони могли це зробити [$0.first, $0.age, $0.isActive].
Почутливий

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