Swift: ordena la matriz de objetos con varios criterios

92

Tengo una variedad de Contactobjetos:

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

Clase de contacto:

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

Y me gustaría ordenar esa matriz una lastNamey otra vez firstNameen caso de que algunos contactos obtengan lo mismo lastName.

Puedo ordenar por uno de esos criterios, pero no por ambos.

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

¿Cómo podría agregar más criterios para ordenar esta matriz?

sbkl
fuente
2
¡Hágalo exactamente de la misma manera que acaba de decir! Su código dentro de las llaves debería decir: "Si los apellidos son iguales, entonces ordene por nombre; de ​​lo contrario, ordene por apellido".
Matt
4
Veo algunos olores de código aquí: 1) Contactprobablemente no debería heredar de NSObject, 2) Contactprobablemente debería ser una estructura, y 3) firstNamey lastNameprobablemente no deberían ser opcionales implícitamente desenvueltos.
Alexander - Reincorpora a Monica
3
@AMomchilov No hay razón para sugerir que Contact debería ser una estructura porque no sabes si el resto de su código ya se basa en la semántica de referencia al usar instancias de él.
Patrick Goley
3
@AMomchilov "Probablemente" es engañoso porque no sabe exactamente nada sobre el resto del código base. Si se cambia a una estructura, todas las copias repentinas se generan al mutar las vars, en lugar de modificar la instancia en cuestión. Este es un cambio drástico en el comportamiento y hacerlo "probablemente" resultaría en errores porque es poco probable que todo haya sido codificado correctamente tanto para la semántica de referencia como de valor.
Patrick Goley
1
@AMomchilov Todavía tengo que escuchar una razón por la que probablemente debería ser una estructura. No creo que OP agradecería sugerencias que modifiquen la semántica del resto de su programa, especialmente cuando ni siquiera era necesario resolver el problema en cuestión. No me di cuenta de que las reglas del compilador eran legales para algunos ... quizás estoy en el sitio web equivocado
Patrick Goley

Respuestas:

120

Piense en lo que significa "ordenar según varios criterios". Significa que dos objetos se comparan primero por un criterio. Luego, si esos criterios son los mismos, los siguientes criterios romperán los lazos, y así sucesivamente hasta que obtenga el orden deseado.

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

Lo que estás viendo aquí es el Sequence.sorted(by:)método , que consulta el cierre proporcionado para determinar cómo se comparan los elementos.

Si su clasificación se utilizará en muchos lugares, puede ser mejor hacer que su tipo se ajuste al Comparable protocolo . De esa manera, puede usar el Sequence.sorted()método , que consulta su implementación del Comparable.<(_:_:)operador para determinar cómo se comparan los elementos. De esta manera, puede clasificar cualquiera Sequencede Contactlos correos electrónicos sin tener que duplicar el código de clasificación.

Alexander - Reincorporar a Monica
fuente
2
El elsecuerpo debe estar entre, de lo { ... }contrario, el código no se compilará.
Luca Angeletti
Entendido. Traté de implementarlo pero no pude obtener la sintaxis correcta. Muchas gracias.
sbkl
para sortvs. sortInPlacever aquí . Además, vea esto a continuación, es mucho más modular
cariño,
sortInPlaceYa no está disponible en Swift 3, en lugar de eso tienes que usarlo sort(). sort()mutará la propia matriz. También hay una nueva función nombrada sorted()que devolverá una matriz ordenada
cariño,
2
@AthanasiusOfAlex Usar ==no es una buena idea. Solo funciona para 2 propiedades. Más que eso, y comienzas a repetirte con muchas expresiones booleanas compuestas
Alexander - Reincorpora a Monica
123

Usar tuplas para hacer una comparación de varios criterios

Una forma realmente simple de realizar una ordenación por múltiples criterios (es decir, ordenando por una comparación, y si es equivalente, luego por otra comparación) es usando tuplas , ya que los operadores <y >tienen sobrecargas para ellos que realizan comparaciones 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 ejemplo:

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

Esto comparará las lastNamepropiedades de los elementos primero. Si no son iguales, el orden de clasificación se basará en una <comparación con ellos. Si son iguales, se moverá al siguiente par de elementos de la tupla, es decir, compararáfirstName propiedades.

La biblioteca estándar proporciona <y> sobrecarga para tuplas de 2 a 6 elementos.

Si desea diferentes órdenes de clasificación para diferentes propiedades, simplemente puede intercambiar los elementos en las 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"),
// ]

Esto ahora se ordenará lastNamedescendiendo y luego firstNameascendiendo.


Definiendo un sort(by:) sobrecarga que toma múltiples predicados

Inspirado por la discusión sobre la clasificación de colecciones con mapcierres y SortDescriptors , otra opción sería definir una sobrecarga personalizada de sort(by:)y sorted(by:)que trata con múltiples predicados, donde cada predicado se considera a su vez para decidir el orden de los 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
    }
  }
}

(El secondPredicate:parámetro es desafortunado, pero es necesario para evitar crear ambigüedades con elsort(by:) sobrecarga )

Esto entonces nos permite decir (usando la contactsmatriz de antes):

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

Aunque el sitio de llamadas no es tan conciso como la variante de tupla, obtiene una claridad adicional con lo que se está comparando y en qué orden.


De acuerdo a Comparable

Si va a hacer este tipo de comparaciones con regularidad, como sugieren @AMomchilov y @appzYourLife , puede cumplir Contactcon 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)
  }
}

Y ahora solo pide sort()un orden ascendente:

contacts.sort()

o sort(by: >)por orden descendente:

contacts.sort(by: >)

Definición de órdenes de clasificación personalizadas en un tipo anidado

Si tiene otros órdenes de clasificación que desea utilizar, puede definirlos en un tipo anidado:

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

y luego simplemente llame como:

contacts.sort(by: Contact.Comparison.firstLastAscending)
Hamish
fuente
contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } Ayudado. Gracias
Prabhakar Kasi
Si como yo, las propiedades para ser clasificadas son opcionales, entonces se podría hacer algo como esto: contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }.
BobCowe
¡Madre mía! Tan simple pero tan eficiente ... ¡¿Por qué nunca había oído hablar de eso ?! ¡Muchas gracias!
Ethenyl
@BobCowe Eso te deja a merced de cómo se ""compara con otras cadenas (viene antes de las cadenas no vacías). Es un poco implícito, un poco mágico e inflexible si, en su lugar, desea que la nils aparezca al final de la lista. Te recomiendo que eches un vistazo a mi nilComparatorfunción stackoverflow.com/a/44808567/3141234
Alexander - Reincorpora a Monica
19

A continuación se muestra otro método simple para ordenar con 2 criterios.

Compruebe el primer campo, en este caso lastName, si no son iguales, ordene por lastName, si lastNameson iguales, ordene por el segundo campo, en este caso firstName.

contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }
oyalhi
fuente
Esto le da más flexibilidad que las tuplas.
Babac
5

Lo único que los tipos lexicográficos no pueden hacer como lo describe @Hamish es manejar diferentes direcciones de clasificación, digamos ordenar por el primer campo descendente, el siguiente campo ascendente, etc.

Creé una publicación de blog sobre cómo hacer esto en Swift 3 y mantener el código simple y legible.

Lo puedes encontrar aquí:

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

También puede encontrar un repositorio de GitHub con el código aquí:

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

La esencia de todo, digamos, si tiene una lista de ubicaciones, podrá hacer esto:

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
fuente
1
"La única cosa que los tipos lexicográficas no pueden hacer como se describe por @Hamish es manejar diferentes direcciones de clasificación" - sí que pueden, simplemente intercambiar los elementos en las tuplas;)
Hamish
Encuentro este un ejercicio teórico interesante pero mucho más complicado que la respuesta de @ Hamish. Menos código es mejor código en mi opinión.
Manuel
5

Esta pregunta ya tiene muchas respuestas excelentes, pero quiero señalar un artículo: Ordenar descriptores en Swift . Tenemos varias formas de realizar la clasificación de varios criterios.

  1. Usando NSSortDescriptor, esta forma tiene algunas limitaciones, el objeto debe ser una clase y hereda 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),
    ]
    

    Aquí, por ejemplo, queremos ordenar por apellido, luego nombre y finalmente por año de nacimiento. Y queremos hacerlo sin distinción entre mayúsculas y minúsculas y utilizando la configuración regional del usuario.

    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 la forma rápida de ordenar por apellido / nombre. De esta forma debería funcionar tanto con class / struct. Sin embargo, aquí no ordenamos por 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. Una forma rápida de imitar NSSortDescriptor. Utiliza el concepto de que "las funciones son un tipo de primera clase". SortDescriptor es un tipo de función, toma dos valores, devuelve un bool. Diga sortByFirstName, tomamos dos parámetros ($ 0, $ 1) y comparamos sus nombres. Las funciones de combinación toman un montón de SortDescriptors, compárelos todos y dé órdenes.

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

    Esto es bueno porque puede usarlo tanto con struct como con class, incluso puede extenderlo para compararlo con nils.

Aún así, se recomienda encarecidamente leer el artículo original . Tiene muchos más detalles y está bien explicado.

XueYu
fuente
2

Recomendaría usar la solución de tuplas de Hamish ya que no requiere código adicional.


Si desea algo que se comporte como ifdeclaraciones pero simplifique la lógica de bifurcación, puede usar esta solución, que le permite hacer lo siguiente:

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

Estas son las funciones que le permiten hacer esto:

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
}

Si desea probarlo, puede usar este código adicional:

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

Las principales diferencias con la solución de Jamie es que el acceso a las propiedades se define en línea en lugar de como métodos estáticos / de instancia en la clase. Por ejemplo, en $0.familylugar de Animal.familyCompare. Y el ascenso / descenso se controla mediante un parámetro en lugar de un operador sobrecargado. La solución de Jamie agrega una extensión en Array, mientras que mi solución usa el método sort/ integrado sorted, pero requiere que se definan dos adicionales: comparey comparisons.

En aras de la integridad, así es como mi solución se compara con la solución de tupla de Hamish . Para demostrar, usaré un ejemplo salvaje en el que queremos clasificar a las personas según (name, address, profileViews)la solución de Hamish, evaluaremos cada uno de los 6 valores de propiedad exactamente una vez antes de que comience la comparación. Esto puede no ser deseado o no serlo. Por ejemplo, asumiendo que profileViewses una llamada de red costosa, es posible que deseemos evitar llamar a profileViewsmenos que sea absolutamente necesario. Mi solución evitará evaluar profileViewshasta $0.name == $1.namey $0.address == $1.address. Sin embargo, cuando evalúe profileViews, probablemente evaluará muchas más veces de una vez.

Sentido
fuente
1

Qué tal si:

contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
Lou Zell
fuente
lexicographicallyPrecedesrequiere que todos los tipos de la matriz sean iguales. Por ejemplo [String, String]. Lo que OP probablemente quiere es mezclar y combinar tipos: [String, Int, Bool]para que pudieran hacerlo [$0.first, $0.age, $0.isActive].
Sentido
-1

eso funcionó para mi matriz [String] en Swift 3 y parece que en Swift 4 está bien

array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}
mamaz
fuente
¿Leíste la pregunta antes de responder? Ordene por múltiples parámetros, no uno, lo que presenta.
Vive el