Swift-여러 기준으로 객체 배열 정렬


92

Contact개체 배열이 있습니다.

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

연락 클래스 :

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

그리고 일부 연락처가 동일한 경우를 대비 lastName하여 배열을 정렬하고 싶습니다 .firstNamelastName

해당 기준 중 하나를 기준으로 정렬 할 수 있지만 둘다는 아닙니다.

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

이 배열을 정렬하기 위해 더 많은 기준을 추가하려면 어떻게해야합니까?


2
방금 말한 것과 똑같은 방식으로 수행하십시오! 중괄호 안의 코드는 "성이 같으면 이름으로 정렬하고 그렇지 않으면 성을 기준으로 정렬"해야합니다.
matt

4
1) : 나는 몇 가지 코드가 여기에 냄새 참조 Contact아마 상속 안 NSObject)이, Contact아마 구조체해야하고, 3) firstNamelastName아마 암시 적으로 풀어 선택적 항목 안된다.
알렉산더 - 분석 재개 모니카

3
@AMomchilov Contact가 구조체 여야한다고 제안 할 이유가 없습니다. 나머지 코드가 인스턴스를 사용할 때 이미 참조 의미론에 의존하는지 모르기 때문입니다.
Patrick Goley 2016-06-03

3
@AMomchilov "아마"는 나머지 코드베이스에 대해 정확히 알지 못하기 때문에 오해의 소지가 있습니다. 구조체로 변경되면 즉시 인스턴스를 수정하는 대신 vars를 변경할 때 모든 갑작스런 복사본이 생성됩니다. 이것은 행동의 급격한 변화이며, 참조 값 의미론 모두에 대해 모든 것이 적절하게 코드화되지 않았기 때문에 "아마"버그가 발생할 수 있습니다 .
패트릭 Goley

1
@AMomchilov 아직 구조체 여야하는 이유를 들어 보지 못했습니다. 나는 OP가 나머지 프로그램의 의미를 수정하는 제안을 고맙게 생각하지 않는다. 특히 당면한 문제를 해결할 필요조차 없었을 때. 컴파일러 규칙 일부 난해한 법률 용어했다 몰랐어요 ... 어쩌면 내가 잘못 웹 사이트에있어
패트릭 Goley

답변:


120

"여러 기준으로 정렬"이 무엇을 의미하는지 생각해보십시오. 두 개체가 먼저 하나의 기준으로 비교됨을 의미합니다. 그런 다음 해당 기준이 동일하면 원하는 순서를 얻을 때까지 다음 기준에 따라 동점이 끊어집니다.

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

여기서보고있는 것은 요소를 비교하는 방법을 결정하기 위해 제공된 클로저를 참조 하는 Sequence.sorted(by:)method 입니다.

정렬이 여러 곳에서 사용되는 경우 유형이 Comparable 프로토콜을 준수하도록하는 것이 더 나을 수 있습니다 . 이렇게하면 요소를 비교하는 방법을 결정하기 위해 연산자 구현을 참조하는 Sequence.sorted()method 를 사용할 수 있습니다 . 이런 식으로, 당신은 정렬 할 수 의 적 분류 코드를 복제하지 않고도들.Comparable.<(_:_:)SequenceContact


2
else본문은 그 사이에 있어야하며 { ... }그렇지 않으면 코드가 컴파일되지 않습니다.
Luca Angeletti 2016 년

알았다. 나는 그것을 구현하려고 시도했지만 올바른 구문을 얻지 못했습니다. 감사합니다.
sbkl

대한 sortsortInPlace참조 여기에 . 끄트머리 볼 훨씬 더 모듈 식의, 아래

sortInPlaceSwift 3에서는 더 이상 사용할 수 없습니다. 대신 sort(). sort()배열 자체를 변경합니다. 또한 정렬 된 배열 sorted()을 반환하는 이름 이 지정된 새 func 가 있습니다
Honey

2
@AthanasiusOfAlex 사용 ==은 좋은 생각이 아닙니다. 2 개의 속성에서만 작동합니다. 더 이상보다, 당신은 복합 부울 식의 많은 자신을 반복 시작
알렉산더 - 분석 재개 모니카

122

튜플을 사용하여 여러 기준 비교

여러 기준에 의해 정렬을 수행하는 정말 간단한 방법 (하나 개의 비교에 의해 즉, 정렬, 다른 비교하여 다음과 경우에 해당)을 사용하는 것입니다 튜플 은 AS, <그리고 >운영자가 사전 식 비교를 수행 그들에게 과부하가 있습니다.

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

이것은 lastName먼저 요소의 속성 을 비교합니다 . 같지 않으면 정렬 순서는 <비교를 기반으로 합니다. 그들이 경우 이다 동일, 그것은, 튜플의 요소의 다음 쌍에 이동 비교 즉 것이다 firstName속성을.

표준 라이브러리는 2 ~ 6 개 요소의 튜플을 제공 <하고 >오버로드합니다.

다른 속성에 대해 다른 정렬 순서를 원하면 튜플의 요소를 간단히 바꿀 수 있습니다.

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

이제 lastName내림차순, firstName오름차순으로 정렬됩니다 .


sort(by:)여러 술어를 사용 하는 오버로드 정의

에 대한 논의에서 영감 과 정렬 컬렉션 map폐쇄 및 SortDescriptors , 또 다른 옵션은 사용자 지정 과부하 정의하는 것입니다 sort(by:)sorted(by:)각 술어가 요소의 순서를 결정 차례로 간주됩니다 - 여러 술어와 그 거래를.

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

( secondPredicate:매개 변수는 유감 스럽지만 기존 sort(by:)과부하로 인한 모호성을 피하기 위해 필요합니다 )

그러면 다음과 같이 말할 수 있습니다 ( contacts이전 배열 사용 ).

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

호출 사이트가 튜플 변형만큼 간결하지는 않지만 비교되는 항목과 순서에 대한 추가 명확성을 얻을 수 있습니다.


준수 Comparable

당신이 같이 정기적으로 다음 비교 이러한 종류의 일을 할 거라면 @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()오름차순을 요청하십시오.

contacts.sort()

또는 sort(by: >)내림차순 :

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)

contacts.sort { ($0.lastName, $0.firstName) < ($1.lastName, $1.firstName) } 도움이되었습니다. 감사합니다
Prabhakar Kasi

나처럼 정렬 할 속성이 선택 사항 인 경우 다음과 같이 할 수 contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }있습니다..
BobCowe

홀리 몰리! 너무 간단하지만 효율적입니다 ... 왜 내가 들어 본 적이 없습니까?! 감사합니다!
Ethenyl

@BobCowe 그것은 당신 ""이 다른 문자열과 비교 하는 방법의 자비에 남습니다 (비어 있지 않은 문자열보다 먼저옵니다). nils가 대신 목록의 끝에 오기 를 원한다면 일종의 암시 적, 일종의 마술 및 융통성이 없습니다 . 내 nilComparator함수를 살펴 보는 것이 좋습니다. stackoverflow.com/a/44808567/3141234
Alexander-Reinstate Monica

19

두 가지 기준으로 정렬하는 또 다른 간단한 방법은 다음과 같습니다.

첫 번째 필드를 확인합니다.이 경우 lastName같지 않은 경우 정렬 기준 lastName, 동일한 경우 lastName두 번째 필드 기준 정렬 (이 경우) firstName입니다.

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

이것은 튜플보다 더 많은 유연성을 제공합니다.
Babac

5

사전 식 정렬이 @Hamish에서 설명한대로 수행 할 수없는 한 가지는 다른 정렬 방향을 처리하는 것입니다 (예 : 내림차순, 다음 필드 오름차순 등).

저는 Swift 3에서이 작업을 수행하는 방법에 대한 블로그 게시물을 작성하고 코드를 간단하고 읽기 쉽게 유지했습니다.

여기에서 찾을 수 있습니다.

http://master-method.com/index.php/2016/11/23/sort-a-sequence-ie-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

나는 이것이 흥미로운 이론적 연습이라고 생각하지만 @Hamish의 대답보다 훨씬 더 복잡합니다. 적은 코드가 더 나은 코드라고 생각합니다.
마누엘

5

이 질문에는 이미 많은 훌륭한 답변이 있지만 기사 -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. 성 / 이름으로 Swift 정렬 방식 사용. 이 방법은 클래스 / 구조체 모두에서 작동합니다. 그러나 여기서는 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. NSSortDescriptor를 유도하는 신속한 방법. 이것은 '함수는 일류 유형'이라는 개념을 사용합니다. SortDescriptor는 함수 유형이며 두 값을 취하고 bool을 반환합니다. sortByFirstName이라고하면 두 개의 매개 변수 ($ 0, $ 1)를 가져 와서 이름을 비교합니다. 결합 함수는 여러 SortDescriptor를 가져 와서 모두 비교하고 주문합니다.

    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와 class 모두에서 사용할 수 있기 때문에 좋습니다. 심지어 nil과 비교하기 위해 확장 할 수도 있습니다.

그래도 원본 기사를 읽는 것이 좋습니다. 훨씬 더 자세한 내용과 잘 설명되어 있습니다.


2

추가 코드가 필요하지 않기 때문에 Hamish의 튜플 솔루션을 사용하는 것이 좋습니다 .


if명령문 처럼 작동 하지만 분기 논리를 단순화하는 것을 원할 경우이 솔루션을 사용하여 다음을 수행 할 수 있습니다.

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. 그리고 오름차순 / 내림차순은 오버로드 된 연산자 대신 매개 변수에 의해 제어됩니다. 제이미의 솔루션은 내 솔루션이 내장 사용하는 반면 배열에 확장자를 추가 sort/ sorted방법 만 정의 할 수있는 두 개의 추가 것들을 필요 compare하고 comparisons.

완전성을 위해 내 솔루션이 Hamish의 튜플 솔루션 과 비교되는 방법은 다음과 같습니다 . 설명하기 위해 (name, address, profileViews)Hamish의 솔루션으로 사람을 정렬하려는 야생 예제를 사용 하여 비교가 시작되기 전에 6 개의 속성 값 각각을 정확히 한 번 평가합니다. 이것은 원하지 않을 수도 있고 원하지 않을 수도 있습니다. 예를 들어, profileViews비용이 많이 드는 네트워크 호출 이라고 가정하면 profileViews꼭 필요한 경우가 아니면 호출을 피하고 싶을 수 있습니다 . 내 솔루션은 및 profileViews까지 평가하지 않습니다 . 그러나 평가할 때 한 번 이상 평가할 가능성이 높습니다.$0.name == $1.name$0.address == $1.addressprofileViews


1

어때 :

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

lexicographicallyPrecedes배열의 모든 유형이 동일해야합니다. 예를 들면 [String, String]. OP가 원하는 것은 유형을 혼합하고 일치시키는 [String, Int, Bool][$0.first, $0.age, $0.isActive]입니다.
Senseful

-1

Swift 3에서 내 array [String]에서 작동했고 Swift 4에서도 괜찮은 것 같습니다.

array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}

대답하기 전에 질문을 읽었습니까? 하나가 아닌 여러 매개 변수를 기준으로 정렬합니다.
Vive
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.