Swift - Classifica a matriz de objetos com vários critérios

91

Eu tenho uma série de Contactobjetos:

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

Classe de contato:

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

E eu gostaria de classificar essa matriz por lastNamee então por firstNamecaso alguns contatos tenham o mesmo lastName.

Posso classificar por um desses critérios, mas não ambos.

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

Como posso adicionar mais critérios para classificar essa matriz?

sbkl
fonte
2
Faça exatamente da mesma maneira que você acabou de dizer! Seu código entre as chaves deve dizer: "Se os sobrenomes forem iguais, classifique pelo nome; caso contrário, classifique pelo sobrenome".
matt
4
Vejo alguns odores de código aqui: 1) Contactprovavelmente não deve herdar de NSObject, 2) Contactprovavelmente deve ser uma estrutura e 3) firstNamee lastNameprovavelmente não deve ser opcional implicitamente desembrulhado.
Alexander - Reintegrar Monica em
3
@AMomchilov Não há razão para sugerir que Contact deve ser uma estrutura porque você não sabe se o resto de seu código já depende da semântica de referência ao usar instâncias dele.
Patrick Goley
3
@AMomchilov "Provavelmente" é enganoso porque você não sabe exatamente nada sobre o resto da base de código. Se for alterado para uma estrutura, todas as cópias repentinas são geradas ao transformar o vars, em vez de modificar a instância em questão. Esta é uma mudança drástica no comportamento e fazer isso "provavelmente" resultaria em bugs, porque é improvável que tudo tenha sido codificado corretamente para a semântica de referência e de valor.
Patrick Goley
1
@AMomchilov Ainda estou para ouvir uma razão pela qual provavelmente deveria ser uma estrutura. Não acho que OP apreciaria sugestões que modificassem a semântica do resto de seu programa, especialmente quando nem era necessário resolver o problema em questão. Não sabia que as regras do compilador eram legais para alguns ... talvez eu esteja no site errado
Patrick Goley

Respostas:

118

Pense no que significa "classificação por vários critérios". Isso significa que dois objetos são comparados primeiro por um critério. Então, se esses critérios forem iguais, os empates serão desfeitos pelo próximo critério e assim por diante até obter a ordem desejada.

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

O que você está vendo aqui é o Sequence.sorted(by:)método , que consulta o fechamento fornecido para determinar como os elementos se comparam.

Se a sua classificação for usada em muitos lugares, pode ser melhor ajustar o seu tipo ao Comparable protocolo . Dessa forma, você pode usar o Sequence.sorted()método , que consulta sua implementação do Comparable.<(_:_:)operador para determinar como os elementos se comparam. Dessa forma, você pode classificar qualquer um Sequencedos Contacts sem nunca ter que duplicar o código de classificação.

Alexander - Reintegrar Monica
fonte
2
O elsecorpo deve estar entre, { ... }caso contrário, o código não compila.
Luca Angeletti
Entendi. Tentei implementá-lo, mas não consegui acertar a sintaxe. Muito obrigado.
sbkl
para sortvs. sortInPlaceveja aqui . Veja isso abaixo, é muito mais modular
Honey
sortInPlaceNÃO está mais disponível no Swift 3, em vez dele você tem que usar sort(). sort()irá transformar o próprio array. Além disso, há uma nova função chamada sorted()que retornará uma matriz classificada
Honey
2
@AthanasiusOfAlex Usar ==não é uma boa ideia. Funciona apenas para 2 propriedades. Mais do que isso, e você começa a se repetir com um monte de expressões booleanas compostas
Alexander - Reintegrar Monica
118

Usando tuplas para fazer uma comparação de vários critérios

Uma maneira realmente simples de realizar uma classificação por vários critérios (ou seja, classificar por uma comparação e, se equivalente, por outra comparação) é usando tuplas , já que os operadores <e >têm sobrecargas para eles que realizam comparações lexicográficas.

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

Por exemplo:

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

Isso irá comparar as lastNamepropriedades dos elementos primeiro. Se eles não forem iguais, a ordem de classificação será baseada em uma <comparação com eles. Se eles forem iguais, ele se moverá para o próximo par de elementos na tupla, ou seja, comparando as firstNamepropriedades.

A biblioteca padrão fornece <e >sobrecarrega tuplas de 2 a 6 elementos.

Se você quiser diferentes ordens de classificação para diferentes propriedades, pode simplesmente trocar os elementos nas tuplas:

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

Agora, isso será classificado por ordem lastNamedecrescente e depois firstNamecrescente.


Definindo uma sort(by:)sobrecarga que leva vários predicados

Inspirado na discussão sobre Sorting Collections with mapclosures e SortDescriptors , outra opção seria definir uma sobrecarga customizada de sort(by:)e sorted(by:)que lida com vários predicados - onde cada predicado é considerado por sua vez para decidir a ordem dos elementos.

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

(O secondPredicate:parâmetro é lamentável, mas é necessário para evitar a criação de ambigüidades com a sort(by:)sobrecarga existente )

Isso então nos permite dizer (usando a contactsmatriz anterior):

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

Embora o call-site não seja tão conciso quanto a variante de tupla, você ganha clareza adicional com o que está sendo comparado e em que ordem.


De acordo com Comparable

Se você vai fazer esse tipo de comparação regularmente, como @AMomchilov e @appzYourLife sugerem, você pode se conformar Contacta 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)
  }
}

E agora é só pedir sort()uma ordem crescente:

contacts.sort()

ou sort(by: >)para uma ordem decrescente:

contacts.sort(by: >)

Definição de ordens de classificação personalizadas em um tipo aninhado

Se você tiver outras ordens de classificação que deseja usar, poderá defini-las em um tipo aninhado:

extension Contact {
  enum Comparison {
    static let firstLastAscending: (Contact, Contact) -> Bool = {
      return ($0.firstName, $0.lastName) <
               ($1.firstName, $1.lastName)
    }
  }
}

e simplesmente chame como:

contacts.sort(by: Contact.Comparison.firstLastAscending)
Hamish
fonte
contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Ajudou. Obrigado
Prabhakar Kasi
Se gosta de mim, as propriedades a serem ordenados são opcionais, então você poderia fazer algo como isto: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
BobCowe
Holly Molly! Tão simples, mas tão eficiente ... por que nunca ouvi falar disso ?! Muito obrigado!
Etenil
@BobCowe Isso deixa você à mercê de como se ""compara a outras strings (vem antes das strings não vazias). É meio implícito, meio mágico e inflexível se você quiser que os nils apareçam no final da lista. Eu recomendo que você dê uma olhada na minha nilComparatorfunção stackoverflow.com/a/44808567/3141234
Alexander - Reintegrar Monica
18

Outra abordagem simples para classificação com 2 critérios é mostrada abaixo.

Verifique o primeiro campo, neste caso é lastName, se eles não são iguais classifique por lastName, se lastName's são iguais, então classifique pelo segundo campo, neste caso firstName.

contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }
oyalhi
fonte
Isso dá mais flexibilidade do que tuplas.
Babac
5

A única coisa que as classificações lexicográficas não podem fazer conforme descrito por @Hamish é lidar com diferentes direções de classificação, digamos classificar pelo primeiro campo descendente, o próximo campo ascendente, etc.

Criei uma postagem no blog sobre como fazer isso no Swift 3 e manter o código simples e legível.

Você pode encontrá-lo aqui:

http://master-method.com/index.php/2016/11/23/sort-a-sequence-ie-arrays-of-objects-by-multiple-properties-in-swift-3/

Você também pode encontrar um repositório GitHub com o código aqui:

https://github.com/jallauca/SortByMultipleFieldsSwift.playground

A essência de tudo, digamos, se você tiver uma lista de locais, será capaz de fazer isso:

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
        )
Jaime allauca
fonte
1
"A única coisa que os tipos lexicográficos não podem fazer conforme descrito por @Hamish é lidar com diferentes direções de classificação" - sim, eles podem, basta trocar os elementos nas tuplas;)
Hamish
Acho este um exercício teórico interessante, mas muito mais complicado do que a resposta de @Hamish. Na minha opinião, menos código é melhor código.
Manuel,
5

Essa pergunta já tem muitas respostas excelentes, mas quero apontar para um artigo - Classificar Descritores em Swift . Temos várias maneiras de fazer a classificação de vários critérios.

  1. Usando NSSortDescriptor, desta forma tem algumas limitações, o objeto deve ser uma classe e herda de 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),
    ]

    Aqui, por exemplo, queremos classificar por sobrenome, depois nome e finalmente por ano de nascimento. E queremos fazer isso de forma insensível e usando a localidade do usuário.

    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. Usando o método Swift de classificação com sobrenome / nome. Dessa forma, deve funcionar com classe / estrutura. No entanto, não classificamos por yearOfBirth aqui.

    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. Maneira rápida de iniciar NSSortDescriptor. Isso usa o conceito de que 'funções são um tipo de primeira classe'. SortDescriptor é um tipo de função, recebe dois valores e retorna um bool. Diga sortByFirstName, pegamos dois parâmetros ($ 0, $ 1) e comparamos seus primeiros nomes. As funções de combinação pegam vários SortDescriptors, comparam todos eles e dão ordens.

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

    Isso é bom porque você pode usá-lo com struct e classe, você pode até mesmo estendê-lo para comparar com nils.

Ainda assim, a leitura do artigo original é altamente recomendável. Tem muito mais detalhes e bem explicados.

XueYu
fonte
2

Eu recomendaria usar a solução de tupla de Hamish, pois não requer código extra.


Se você quiser algo que se comporte como ifinstruções, mas simplifique a lógica de ramificação, pode usar esta solução, que permite fazer o seguinte:

animals.sort {
  return comparisons(
    compare($0.family, $1.family, ascending: false),
    compare($0.name, $1.name))
}

Aqui estão as funções que permitem que você faça isso:

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
}

Se quiser testá-lo, você pode usar este código extra:

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

As principais diferenças da solução de Jamie é que o acesso às propriedades são definidos inline, em vez de métodos estáticos / de instância na classe. Por exemplo$0.family vez de Animal.familyCompare. E a ascensão / descida é controlada por um parâmetro em vez de um operador sobrecarregado. A solução de Jamie adiciona uma extensão em Array, enquanto minha solução usa o método sort/ embutido sorted, mas requer dois adicionais a serem definidos: comparee comparisons.

Para completar, veja como minha solução se compara à solução de tupla de Hamish . Para demonstrar, usarei um exemplo selvagem em que queremos classificar as pessoas pela (name, address, profileViews)solução de Hamish para avaliar cada um dos 6 valores de propriedade exatamente uma vez antes do início da comparação. Isso pode não ser ou não desejado. Por exemplo, supondo que profileViewsseja uma chamada de rede cara, podemos evitar fazer chamadas, a profileViewsmenos que seja absolutamente necessário. Minha solução evitará avaliar profileViewsaté $0.name == $1.namee $0.address == $1.address. No entanto, quando avaliaprofileViews , provavelmente irá avaliar muito mais vezes de uma vez.

Sensível
fonte
1

E se:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
Lou Zell
fonte
lexicographicallyPrecedesrequer que todos os tipos na matriz sejam iguais. Por exemplo [String, String]. O que o OP provavelmente deseja é misturar e combinar tipos: [String, Int, Bool]para que eles possam fazer [$0.first, $0.age, $0.isActive].
Sensato de
-1

que funcionou para meu array [String] no Swift 3 e parece que no Swift 4 está ok

array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}
mamaz
fonte
Você leu a pergunta antes de responder? Classifique por vários parâmetros, não um, o que você apresenta.
Vive