2016-06-02 2 views
27

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

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

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

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

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

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

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

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

спасибо.

+2

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

+1

Я вижу, что здесь есть несколько кодов: 1) «Контакт», вероятно, не должен наследовать от «NSObject», 2) «Контакт», вероятно, должен быть структурой, а 3) 'firstName' и' lastName', вероятно, не должен быть неявно развернутые варианты. – Alexander

+0

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

ответ

33

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

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(_:) method, которая зависит от данного закрытия для определения сортировки. Если ваша сортировка будет использоваться во многих местах, лучше использовать вместо нее sortInPlace() method, который работает только с MutableCollectionType экземплярами, которые также соответствуют Comparable protocol. Таким образом, вы можете сортировать коллекцию Contact s без необходимости дублировать код сортировки.

+2

Тело 'else' должно находиться между' {...} 'иначе код не компилируется. –

+0

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

+0

@appzYourlLife старые привычки умереть трудно – Alexander

0

Как насчет:

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

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

21

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

/// 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) 
+2

Это лучший ответ. –

+1

Это должен быть принятый ответ; это читаемое и гибкое решение. – Manuel

+0

Это должен быть принятый ответ. Работает отлично. –

5

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

Я создал ab log post о том, как это сделать в 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 
     ) 
+1

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

+0

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

2

Я бы рекомендовал использовать Hamish's tuple solution, так как он не требует дополнительного кода.


Если вы хотите что-то, что ведет себя как 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), 
] 

Основные отличия от Jamie's solution состоят в том, что доступ к свойствам определен как встроенный, а не как статический/экземплярный метод для класса. Например. $0.family вместо Animal.familyCompare. И восходящий/нисходящий управляется параметром, а не перегруженным оператором. Решение Jamie добавляет расширение на Array, тогда как мое решение использует встроенный метод /sorted, но для этого требуется два дополнительных: compare и comparisons.

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

1

У этого вопроса уже много замечательных ответов, но я хочу указать на статью - Sort Descriptors in 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 - это тип функции, принимает два значения, возвращает 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.

Настоятельно рекомендуется прочитать original article. У этого есть намного больше деталей и хорошо объяснено.

2

Ниже приведен простой простой способ сортировки по 2 критериям.

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

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

Это, imo, легко лучшее решение. Единая строка кода, интуитивно понятная для чтения. –

0

, который работал на моем массиве [String] в Swift 3 и, кажется, в Swift 4 нормально

массив = array.sorted {$ 0.compare ($ 1, опции: .numeric) ==. orderedAscending}

Смежные вопросы