programing

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

nasanasas 2020. 10. 31. 09:47
반응형

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


Contact개체 배열이 있습니다.

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

연락 클래스 :

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

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

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

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

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


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

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


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

여러 기준에 의해 정렬을 수행하는 정말 간단한 방법 (하나 개의 비교에 의해 즉, 정렬, 다른 비교하여 다음과 경우에 해당)을 사용하는 것입니다 튜플 은 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)

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

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

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

사전 식 정렬이 @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
        )

이 질문에는 이미 많은 훌륭한 답변이 있지만 기사 -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)]
    

    This is good because you can use it with both struct and class, you can even extend it to compare with nils.

Still, reading the original article is strongly suggested. It has much more details and well explained.


I'd recommend using Hamish's tuple solution since it doesn't require extra code.


If you want something that behaves like if statements but simplifies the branching logic, you can use this solution, which allows you to do the following:

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

Here are the functions that allow you to do this:

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
}

If you want to test it out, you can use this extra code:

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

The main differences from Jamie's solution is that the access to the properties are defined inline rather than as static/instance methods on the class. E.g. $0.family instead of Animal.familyCompare. And ascending/descending is controlled by a parameter instead of an overloaded operator. Jamie's solution adds an extension on Array whereas my solution uses the built in sort/sorted method but requires two additional ones to be defined: compare and comparisons.

For completeness sake, here's how my solution compares to the Hamish's tuple solution. To demonstrate I'll use a wild example where we want to sort people by (name, address, profileViews) Hamish's solution will evaluate each of the 6 property values exactly once before the comparison begins. This may not or may not be desired. For example, assuming profileViews is an expensive network call we may want to avoid calling profileViews unless it's absolutely necessary. My solution will avoid evaluating profileViews until $0.name == $1.name and $0.address == $1.address. However, when it does evaluate profileViews it'll likely evaluate many more times than once.


How about:

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

that worked for my array[String] in Swift 3 and it seems in Swift 4 is ok

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

참고URL : https://stackoverflow.com/questions/37603960/swift-sort-array-of-objects-with-multiple-criteria

반응형