Tengo una variedad de Contact
objetos:
var contacts:[Contact] = [Contact]()
Clase de contacto:
Class Contact:NSOBject {
var firstName:String!
var lastName:String!
}
Y me gustaría ordenar esa matriz una lastName
y otra vez firstName
en 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?
Contact
probablemente no debería heredar deNSObject
, 2)Contact
probablemente debería ser una estructura, y 3)firstName
ylastName
probablemente no deberían ser opcionales implícitamente desenvueltos.Respuestas:
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 elSequence.sorted()
método , que consulta su implementación delComparable.<(_:_:)
operador para determinar cómo se comparan los elementos. De esta manera, puede clasificar cualquieraSequence
deContact
los correos electrónicos sin tener que duplicar el código de clasificación.fuente
else
cuerpo debe estar entre, de lo{ ... }
contrario, el código no se compilará.sort
vs.sortInPlace
ver aquí . Además, vea esto a continuación, es mucho más modularsortInPlace
Ya no está disponible en Swift 3, en lugar de eso tienes que usarlosort()
.sort()
mutará la propia matriz. También hay una nueva función nombradasorted()
que devolverá una matriz ordenada==
no es una buena idea. Solo funciona para 2 propiedades. Más que eso, y comienzas a repetirte con muchas expresiones booleanas compuestasUsar 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
lastName
propiedades 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á
lastName
descendiendo y luegofirstName
ascendiendo.Definiendo un
sort(by:)
sobrecarga que toma múltiples predicadosInspirado por la discusión sobre la clasificación de colecciones con
map
cierres y SortDescriptors , otra opción sería definir una sobrecarga personalizada desort(by:)
ysorted(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
contacts
matriz 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
Contact
conComparable
: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)
fuente
contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) }
Ayudado. Graciascontacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }
.""
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 lanil
s aparezca al final de la lista. Te recomiendo que eches un vistazo a minilComparator
función stackoverflow.com/a/44808567/3141234A 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 porlastName
, silastName
son iguales, ordene por el segundo campo, en este casofirstName
.contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName }
fuente
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 )
fuente
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.
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)]
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)]
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.
fuente
Recomendaría usar la solución de tuplas de Hamish ya que no requiere código adicional.
Si desea algo que se comporte como
if
declaraciones 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.family
lugar deAnimal.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étodosort
/ integradosorted
, pero requiere que se definan dos adicionales:compare
ycomparisons
.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 queprofileViews
es una llamada de red costosa, es posible que deseemos evitar llamar aprofileViews
menos que sea absolutamente necesario. Mi solución evitará evaluarprofileViews
hasta$0.name == $1.name
y$0.address == $1.address
. Sin embargo, cuando evalúeprofileViews
, probablemente evaluará muchas más veces de una vez.fuente
Qué tal si:
contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
fuente
lexicographicallyPrecedes
requiere 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]
.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}
fuente