Подтвердить что ты не робот

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

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

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

Класс контактов:

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

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

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

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

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

Спасибо.

4b9b3361

Ответ 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  }