Swift-複数の基準でオブジェクトの配列をソートする


91

Contactオブジェクトの配列があります:

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

連絡先クラス:

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

そして、いくつかの連絡先が同じになった場合に備えてlastName、その配列を並べ替えたいと思います。firstNamelastName

これらの基準のいずれかでソートできますが、両方でソートすることはできません。

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

この配列を並べ替える基準を追加するにはどうすればよいですか?


2
あなたが今言ったのと全く同じ方法でそれをしてください!中括弧内のコードは、「姓が同じである場合は、名でソートします。それ以外の場合は、姓でソートします」と記述します。
マット

4
私はいくつかのコードはここに臭いを参照:1)Contactおそらくから継承していなければならないNSObject、2)Contactおそらく構造体、および3でなければなりません)firstNamelastName、おそらく暗黙的に開封されたoptionalsすべきではありません。
アレクサンダー-モニカ

3
@AMomchilov彼のコードの残りの部分が参照セマンティクスにそのインスタンスの使用にすでに依存しているかどうかがわからないため、Contactが構造体であることを提案する理由はありません。
Patrick Goley、2016年

3
@AMomchilovは、コードベースの残りの部分について正確に何も知らないため、「おそらく」は誤解を招くものです。構造体に変更すると、手元のインスタンスを変更するのではなく、varを変更するときに突然のコピーがすべて生成されます。これは振る舞いの劇的な変化であり、参照セマンティクス値セマンティクスの両方に対してすべてが適切にコード化されているとは考えにくいため、これを行うと「おそらく」バグが発生ます。
Patrick Goley、2016年

1
@AMomchilovおそらく構造体である必要がある理由の1つをまだ聞いていません。特に当面の問題を解決する必要さえなかった場合、OPが彼のプログラムの残りのセマンティクスを変更する提案を高く評価することはないと思います。コンパイラルールが合法であることに気づかなかった...多分私は間違ったウェブサイトにいる
Patrick Goley

回答:


118

「複数の基準によるソート」の意味を考えてください。つまり、2つのオブジェクトが最初に1つの基準で比較されます。次に、それらの基準が同じである場合、希望の順序が得られるまで、ネクタイは次の基準で分割されます。

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.<(_:_:)オペレーターの実装を調べて、要素の比較方法を決定します。このように、並べ替えコードを複製することなく、任意のSequenceを並べ替えることができContactます。


2
elseボディはの間でなければなりません{ ... }それ以外のコードはコンパイルされません。
Luca Angeletti、2016年

とった。私はそれを実装しようとしましたが、構文を正しく取得できませんでした。どうもありがとう。
sbkl 2016年

以下のためのsortsortInPlaceご覧くださいここに。以下を参照してください。これははるかにモジュール化されています
Honey

sortInPlaceはSwift 3では使用できなくなりましたsort()。代わりにを使用する必要があります。sort()配列自体を変更します。またsorted()ソートされた配列を返すという名前の新しいfuncがあります
Honey

2
@AthanasiusOfAlexを使用するの==は良い考えではありません。2つのプロパティに対してのみ機能します。それ以上なら、あなたはたくさんの複合ブール式で自分自身を繰り返し始めます
アレクサンダー-モニカを復活させます

118

タプルを使用して複数の基準を比較する

複数の条件でソートを実行するのは本当に簡単な方法(すなわち、1つの比較によって、および同等の場合は、別の比較でソート)が使用しているタプルを通り、<そして>事業者は辞書式の比較を行う彼らのために過負荷を持っています。

/// 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これは""、他の文字列と比較する方法のなすがままになります(空でない文字列の前に来ます)。nilsをリストの最後に配置したい場合、それは一種の暗黙的で魔法のようなものであり、柔軟性がありません。私のnilComparator関数stackoverflow.com/a/44808567/3141234をご覧になる
アレクサンダー

18

2つの条件で並べ替える別の簡単な方法を以下に示します。

最初のフィールドを確認します。この場合はlastName、等しくない場合は並べ替え、等しいlastName場合lastNameは2番目のフィールドで並べ替えます(この場合)firstName

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

これはタプルよりも柔軟性があります。
Babac

5

@Hamishで説明されているように辞書式ソートで実行できないことの1つは、さまざまなソート方向を処理することです。たとえば、最初のフィールドを降順で、次のフィールドを昇順でソートします。

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で説明されているように辞書式ソートで実行できないことの1つは、さまざまなソート方向を処理することです」–はい、タプルの要素を交換するだけです))
Hamish

これは興味深い理論的な演習ですが、@ Hamishの回答よりもはるかに複雑です。私の意見では、コードが少ないほどコードは優れています。
Manuel、

5

この質問にはすでに多くの素晴らしい答えがありますが、SwiftのSort Descriptorsの記事を指摘したいと思います。複数の条件で並べ替えを行う方法はいくつかあります。

  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は関数型で、2つの値を取り、ブール値を返します。sortByFirstNameが2つのパラメーター($ 0、$ 1)を取り、それらの名を比較するとします。結合関数は、一連のSortDescriptorsを取得し、それらすべてを比較して、順序を指定します。

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

    構造体とクラスの両方で使用できるため、これを拡張してnilと比較することもできます。

それでも、元の記事を読むことを強くお勧めします。それははるかに詳細であり、よく説明されています。


2

追加のコードを必要としないため、ハミッシュのタプルソリューションを使用することをお勧めします。


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。また、昇順/降順は、多重定義された演算子ではなく、パラメーターによって制御されます。JamieのソリューションはArrayに拡張機能を追加しますが、私のソリューションは組み込みのsort/ sortedメソッドを使用しますが、との2つを定義する必要がcompareありcomparisonsます。

完全を期すために、ここに私のソリューションとハミッシュのタプルソリューションを比較します。デモを行うために、(name, address, profileViews)ハミッシュのソリューションで人を並べ替えるワイルドな例を使用して、比較が始まる前に6つのプロパティ値のそれぞれを1回だけ評価します。これは望ましくない場合もあれば、望ましくない場合もあります。たとえばprofileViews、高価なネットワークコールであると想定すると、profileViews絶対に必要でない限り、コールを避けたい場合があります。私のソリューションは、評価避けることができますprofileViewsまで、$0.name == $1.name$0.address == $1.address。ただし、評価profileViewsする場合は、1回よりもはるかに多く評価される可能性があります。


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]
センスフル

-1

それはSwift 3の私のarray [String]で機能し、Swift 4では問題ないようです

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

答える前に質問を読みましたか?提示したものを1つではなく複数のパラメータで並べ替えます。
Vive
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.