37

Say I have an array of the custom class [Player], each of which contains a string property called player.position

I also have an arbitrary array of values, called positionOrders, like so:

let positionOrders = ["QB", "WR", "RB", "TE"]

Where my goal is to sort the [Player] to have all the "QB"s first, then "WR"s, "RB"s, and finally "TE"s.

The current way I am doing loops through each element in positionOrders, then inside that loops through all the players to append to a new array. However, I could not figure a simpler (and more efficient) way to do this. Any tips or pointers are much appreciated. Thanks.

Hamish
  • 78,605
  • 19
  • 187
  • 280
daspianist
  • 5,336
  • 8
  • 50
  • 94
  • Convert orders into an enum or other type. Define Comparable on it. Use that comparison to sort players. – Sulthan Mar 27 '17 at 22:09
  • Is the array `positionOrders` constant or does it change at runtime? – Sulthan Mar 28 '17 at 08:05
  • Thanks for the comments. The positionOrders is static (its from a `struct` of constants). @Alexander thanks for the input. The suggestion you made worked great. – daspianist Mar 28 '17 at 14:36
  • Could the person who downvoted leave a comment as to how the question could be improved? – daspianist Mar 28 '17 at 14:37
  • 1
    @daspianist, If the array is static, why don't you define your position as an Enum with Int as rawValue? Just curious. Because that might make your sort function easier to be implemented. – antonio081014 Mar 28 '17 at 16:53
  • That's a good suggestion. It will be more efficient that way as well - thanks for chiming in @antonio081014 – daspianist Mar 28 '17 at 17:24

10 Answers10

39

Edit: My original approach was shit. This post got a lot of traction, so it's time to give it some more attention and improve it.


Fundamentally, the problem is easy. We have two elements, and we have an array (or any ordered Collection) whose relative ordering determines their sort order. For every element, we find its position in the ordered collection, and compare the two indices to determine which is "greater".

However, if we naively do linear searches (e.g. Array.firstIndex(of:)), we'll get really bad performance (O(array.count)), particularly if the fixed ordering is very large. To remedy this, we can construct a Dictionary, that maps elements to their indices. The dictionary provides fast O(1) look-ups, which is perfect for the job.

This is exactly what HardCodedOrdering does. It pre-computes a dictionary of elements to their orderings, and provides an interface to compare 2 elements. Even better, it can be configured to respond differently to encountering elements with an unknown ordering. It could put them first before everything else, last after everything else, or crash entirely (the default behaviour).

HardCodedOrdering

public struct HardCodedOrdering<Element> where Element: Hashable {
    public enum UnspecifiedItemSortingPolicy {
        case first
        case last
        case assertAllItemsHaveDefinedSorting
    }

    private let ordering: [Element: Int]
    private let sortingPolicy: UnspecifiedItemSortingPolicy

    public init(
        ordering: Element...,
        sortUnspecifiedItems sortingPolicy: UnspecifiedItemSortingPolicy = .assertAllItemsHaveDefinedSorting
    ) {
        self.init(ordering: ordering, sortUnspecifiedItems: sortingPolicy)
    }

    public init<S: Sequence>(
        ordering: S,
        sortUnspecifiedItems sortingPolicy: UnspecifiedItemSortingPolicy = .assertAllItemsHaveDefinedSorting
    ) where S.Element == Element {

        self.ordering = Dictionary(uniqueKeysWithValues: zip(ordering, 1...))
        self.sortingPolicy = sortingPolicy
    }

    private func sortKey(for element: Element) -> Int {
        if let definedSortKey = self.ordering[element] { return definedSortKey }

        switch sortingPolicy {
            case .first:    return Int.min
            case .last:     return Int.max

            case .assertAllItemsHaveDefinedSorting:
                fatalError("Found an element that does not have a defined ordering: \(element)")
        }
    }

    public func contains(_ element: Element) -> Bool {
        return self.ordering.keys.contains(element)
    }

    // For use in sorting a collection of `T`s by the value's yielded by `keyDeriver`.
    // A throwing varient could be introduced, if necessary.
    public func areInIncreasingOrder<T>(by keyDeriver: @escaping (T) -> Element) -> (T, T) -> Bool {
        return { lhs, rhs in
            self.sortKey(for: keyDeriver(lhs)) < self.sortKey(for: keyDeriver(rhs))
        }   
    }

    // For use in sorting a collection of `Element`s
    public func areInIncreasingOrder(_ lhs: Element, rhs: Element) -> Bool {        
        return sortKey(for: lhs) < sortKey(for: rhs)
    }
}

Example usage:


let rankOrdering = HardCodedOrdering(ordering: "Private", "Lieutenant", "Captain", "Admiral") // ideally, construct this once, cache it and share it

let someRanks = [
    "Admiral", // Should be last (greatest)
    "Gallactic Overlord", // fake, should be removed
    "Private", // Should be first (least)
]
let realRanks = someRanks.lazy.filter(rankOrdering.contains)
let sortedRealRanks = realRanks.sorted(by: rankOrdering.areInIncreasingOrder) // works with mutating varient, `sort(by:)`, too.

print(sortedRealRanks) // => ["Private", "Admiral"]
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Of course this code needs to safely look at the index results incase a position isn't in the positionOrders array. – rmaddy Mar 27 '17 at 21:40
  • And you need `$0.position` and `$1.position`. – rmaddy Mar 27 '17 at 21:40
  • Maybe ordering[$0.position] rather than positionOrders[$0.position] – antonio081014 Mar 27 '17 at 21:43
  • @antonio081014 Overlooked copy pasting mistakes strike again! Fixed. – Alexander Mar 27 '17 at 21:44
  • This is simply wrong. Hashing the indexes is *less performant in almost all domains* !! There's no worse engineering that unnecessary "performance code". If, incredibly, performance was an issue, an upstream approach would be called for (such as say spatial hashing). – Fattie Feb 06 '23 at 12:24
  • @Fattie it depends on how long as your “ordering” is. I had a case with like a hundred items, and hashing measurably out-performed linear searching. I know what spatial hashing is, but how would you apply it to this problem? – Alexander Feb 06 '23 at 13:09
  • Also, I have to say, the way you conduct yourself on this site, in my opinion, is consistently critical, hysterical, and completely non-constructive. It’s really quite unpleasant. Adding italics and exclamation points on all your comments doesn’t help take your point any better. Downvote if you want, but if you want to actually bring any value here, suggest a better alternative. I don’t stand by this code as perfect by any stretch, but it had a noticeable performance uplift in my case (perhaps I should clarify when the break even point is), that I’ve profiled and tested. – Alexander Feb 06 '23 at 13:15
  • Hi Alexander *"it depends on how long as your “ordering” is"*, yes, that's what I stated as: "Hashing the indexes is less performant in almost all domains". Note that your idea of ordering once, let's call it at "bring up time", say ... so, it's a game with 100 mountains, those never change, your concept is to hash it once when the map is made and that's that): that's a perfectly fine idea, in that circumstance, but it's not really answering the question; you're just offering an upstream improvement which is relevant in a few settings. (Thus, your note to hash once up front is irrelevant if.. – Fattie Feb 06 '23 at 15:47
  • .. the ordering list is different every time.) Note that, for example, a really common case where you simply sort against a different list, is, in a UI when you have, imagine a tab bar or similar with "pills", the user can select a few from a list of ten. You just let the user edit and then sort against the "master list" to keep the list in order. My point simply that the (perfectly sensible) note you offer is only useful in certain milieu. Pls note that this question is nothing more than "how to sort in swift". (the answer is use `.sort` :) :) ) SURE, with ANY question on this site, – Fattie Feb 06 '23 at 15:52
  • one could add an addendum "oh, consider performance issues in unusual situations". As you know in software engineering, you extremely rarely consider performance. The only thing that matters is "how easily can the next guy who works on this use the code" and "is it KISS". One way to look at it is, your answer could have read: "Oh, you use `.sort`. As a curiosity, in certain situations, if the sorter doesn't change, you might want to do this .. which will be more efficient in some domains of array lengths, in the unusual case that this is something which happens every frame or such ".. – Fattie Feb 06 '23 at 15:59
  • Also @Alexander you mention you don't enjoy exclamation points. Sure, I won't use them when I see you. I think there's a sense of "need to emphasize" when a simple question has a number of wrong answers, and a highly voted answer such as yours which is, put it this way, admirable but "misguiding". (It is "misguiding" simply because - put it this way - your answer is missing the preamble "To do that, you .sort. Here's an interesting thing though..." Some twenty thousand people (and chatgpt) have now seen your "misleading" answer. So. – Fattie Feb 06 '23 at 16:07
  • Tone is hard to read over text, so I could be totally off base here, but the original tone just read as pointlessly critical, with no constructive spirit to it. Your implication that it's unnecessary "bad engineering" is just factually wrong. I've got the receipts, I've profiled this to see that it makes a difference in my use case. You could argue that my use-case (lots of items in the `ordering`, being hashed once and reused for the whole app life) is atypical, I would be inclined to agree with you, if only you had posed it in a more constructive way. – Alexander Feb 06 '23 at 20:38
  • There's also a difference in philosophy here. My programming style aims to simplify business-logic call sites (e.g. calling just `realRanks.sorted(by: rankOrdering.areInIncreasingOrder)`, rather than `sort` with an inline-block). Even absent any particular performance concern, I would still prefer this approach in my projects, because it's the kind of thing I can extract/test once, shove in a corner, and use without worrying again. In part, because it's easier to unit test that individual component. I take it that your philosophy is more like what the Go community prefers, and that's fine. – Alexander Feb 06 '23 at 20:42
39

Here's a generic Swift 5.2 solution.

First we need to define a protocol that holds a property that is used to reorder our elements:

protocol Reorderable {
    associatedtype OrderElement: Equatable
    var orderElement: OrderElement { get }
}

Then we need to extend Array with elements conforming to Reorderable and implement our reordering function:

extension Array where Element: Reorderable {

    func reorder(by preferredOrder: [Element.OrderElement]) -> [Element] {
        sorted {
            guard let first = preferredOrder.firstIndex(of: $0.orderElement) else {
                return false
            }

            guard let second = preferredOrder.firstIndex(of: $1.orderElement) else {
                return true
            }

            return first < second
        }
    }
}

Example

Let's assume you have defined:

struct Player {
    let position: String
}

let currentPositions = ["RB", "AA", "BB", "CC", "WR", "TE"]
let players = currentPositions.map { Player(position: $0) }

Now, all you need to do is conform Player to Reorderable:

extension Player: Reorderable {
    typealias OrderElement = String
    var orderElement: OrderElement { position }
}

You can now use:

let sortedPlayers = players.reorder(by: ["QB", "WR", "RB", "TE"])

print(sortedPlayers.map { $0.position })
// ["WR", "RB", "TE", "AA", "BB", "CC"]

Original Solution

This is a generic Swift 5.2 solution based on OuSS' code and requires array elements to be Equatable.

extension Array where Element: Equatable {

    func reorder(by preferredOrder: [Element]) -> [Element] {

        return self.sorted { (a, b) -> Bool in
            guard let first = preferredOrder.firstIndex(of: a) else {
                return false
            }

            guard let second = preferredOrder.firstIndex(of: b) else {
                return true
            }

            return first < second
        }
    }
}

let currentPositions = ["RB", "AA", "BB", "CC", "WR", "TE"]
let preferredOrder = ["QB", "WR", "RB", "TE"]
let sorted = currentPositions.reorder(by: preferredOrder)
print(sorted) // ["WR", "RB", "TE", "AA", "BB", "CC"]
RobertChals
  • 406
  • 7
  • 4
  • 3
    Great solution. Although you used same type of objects it can be adapted to use different type of objects in each array, for example the preferred order to represent only a field in a bigger object you want to sort. – Cristi Băluță Dec 18 '18 at 13:29
  • @CristiBăluță Thanks, great observation! I didn't realize that when I provided my initial solution but have now edited it to do exactly what you suggested. – RobertChals Jun 09 '20 at 19:42
  • 1
    I was going to post an answer like the new solution you recently included, but I especially love the way you included the guard statements. Super elegant. This is definitely the best answer now. – Lyndsey Scott Jun 15 '20 at 14:51
  • This is unfortunately horrible and pointless ... since .sort *is already generic*. – Fattie Feb 06 '23 at 12:20
11

Based on Alexander's answer, I've implemented an extension to do this.

extension Array where Element == String {

func reordered() -> [String] {

    let defaultOrder = ["orange", "pear", "watermelon", "grapefruit", "apple", "lemon", "tomatoes"]

    return self.sorted { (a, b) -> Bool in
        if let first = defaultOrder.index(of: a), let second = defaultOrder.index(of: b) {
            return first < second
        }
        return false
    }
}

let arrayToSort = ["lemon", "watermelon", "tomatoes"]
let sortedArray = arrayToSort.reordered()
print(sortedArray) // ["watermelon", "lemon", "tomatoes"]
Emily Fung
  • 133
  • 1
  • 5
  • Defining the default ordering of Strings of Arrays is not something you should be doing, because you're not the author or `String` nor `Array`. What if someone else tries to use this to reorder their own strings? Spoiler: they can't. Extending `Array` would be fine. – Alexander Aug 01 '19 at 18:40
7

SwifterSwift has this implementation

/// SwifterSwift: Sort an array like another array based on a key path. If the other array doesn't contain a certain value, it will be sorted last.
    ///
    ///        [MyStruct(x: 3), MyStruct(x: 1), MyStruct(x: 2)].sorted(like: [1, 2, 3], keyPath: \.x)
    ///            -> [MyStruct(x: 1), MyStruct(x: 2), MyStruct(x: 3)]
    ///
    /// - Parameters:
    ///   - otherArray: array containing elements in the desired order.
    ///   - keyPath: keyPath indiciating the property that the array should be sorted by
    /// - Returns: sorted array.
    func sorted<T: Hashable>(like otherArray: [T], keyPath: KeyPath<Element, T>) -> [Element] {
        let dict = otherArray.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
        return sorted {
            guard let thisIndex = dict[$0[keyPath: keyPath]] else { return false }
            guard let otherIndex = dict[$1[keyPath: keyPath]] else { return true }
            return thisIndex < otherIndex
        }
    }
Alexander Khitev
  • 6,417
  • 13
  • 59
  • 115
  • 1
    A good example of why not to use libraries. The function is completely built-in to Swift. – Fattie Feb 06 '23 at 12:16
5

What I will do:

  1. Create a Dictionary with position as the Key, and an Array of players in that position as the Value. O(n), where n is the number of players.
  2. Loop through your positionOrders and fetch Value to each Key(position).

Here is the code:

    let preSortPlayerList = [Player]() // Filled with your players.
    let positionOrders = ["QB", "WR", "RB", "TE"]
    let dict = preSortPlayerList.reduce([String : [Player]]()) {
        var map = $0
        if var tmp = map[$1.position] {
            tmp.append($1)
            map[$1.position] = tmp
        } else {
            map[$1.position] = [$1]
        }
        return map
    }

    let playersArray: [Player] = positionOrders.flatMap { dict[$0] ?? [Player]() }
    print("\(playersArray)")
antonio081014
  • 3,553
  • 1
  • 30
  • 37
5

It's incredibly simple:

positionOrders.filter{player.contains($0)}

That's all there is to it.

An even more obvious approach:

The way you sort something is simply like this:

    player.sort{
        .. your formula for sorting goes here
    }

For example if you want to sort by "bank balance"

    player.sort{
        $0.bankBalance < $1.bankBalance
    }

To sort by "another array", it's just

    player.sort{
        let i = positionOrders.firstIndex(of: $0) ?? 0
        let j = positionOrders.firstIndex(of: $1) ?? 0
        return i < j
    }

That's all there is to it.

(Note that obviously you'd just use Int.max rather than the 0 if you wanted "missing" items to appear at the end rather than the start, if missing items are conceivable in your use case.)


(Note that the comments on this page about performance are off the mark by many, many orders of magnitude. It's inconceivable performance could be an issue in the milieu stated. If, incredibly, beyond all belief, performance is an issue you just do the obvious, add a line of code to hash the indexes. Take care though, making a hash is actually !!LESS PERFORMANT!! in almost all cases. Again, just TBC, it's incredibly unlikely performance will be an issue; use the code above)

Fattie
  • 27,874
  • 70
  • 431
  • 719
4

For swift 4

Very simple way and diligent( feel free to suggest and correct me )

func reOrder(array : [String] , order : [String]) -> [String]{ 

    //common elments in order
    // order.filter{array.contains($0)}

    // the rest of the array that the order doesnt specify
    // array.filter{!order.contains($0)}

 return order.filter{array.contains($0)} + array.filter{!order.contains($0)}
}


let list =     ["A", "Z", "B", "H", "C", "T", "D", "E"] 
let newOrder = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"]




print("The ordered list is ",reOrder(array: list, order: newOrder))


 // The ordered list is  ["A", "B", "C", "D", "E", "H", "Z", "T"]

it would be good if someone can make it as extension for Generic type, I'm not good with that

Jacob Kazal
  • 149
  • 1
  • 9
3

I love one-liners and here is another one:

positionOrders.compactMap { order in players.first(where: { $0.position == order })}

Only thing to consider about this approach is that it will have the O(n*m) - or O(positionOrders*players) - time complexity.

Cuneyt
  • 931
  • 5
  • 11
2

Based on Emily code, i did some change to extension cause it will not make the elements that doesn't exist in defaultOrder at the end

extension Array where Element == String {

func reordered() -> [String] {

    let defaultOrder = ["lemon", "watermelon", "tomatoes"]

    return self.sorted { (a, b) -> Bool in
        guard let first = defaultOrder.index(of: a) else {
            return false
        }

        guard let second = defaultOrder.index(of: b) else {
            return true
        }

        return first < second
    }
}

let arrayToSort = ["orange", "watermelon", "grapefruit", "lemon", "tomatoes"]
let sortedArray = arrayToSort.reordered()
print(sortedArray) // ["watermelon", "lemon", "tomatoes", "orange", "grapefruit"]
OuSS
  • 1,047
  • 1
  • 15
  • 23
0

Since iOS15, we can use SortComparator to help us sort the objects with custom logic easily.

e.g. A SortComparator that sorts players by predefined position order. If the position isn't included in the order, sort by the player ID instead.

struct Player: Equatable {
    let id: Int
    let position: String
}

struct PlayerSort: SortComparator {
    let positionOrder: [String: Int]
    var order: SortOrder
    
    init(positionOrder: [String]) {
        self.order = .forward
        var order = [String: Int]()
        positionOrder.enumerated().forEach {
            order[$1] = $0
        }
        self.positionOrder = order
    }

    func compare(_ lhs: Player, _ rhs: Player) -> ComparisonResult {
        if positionOrder[lhs.position] ?? Int.max < positionOrder[rhs.position] ?? Int.max {
            return .orderedAscending
        } else if positionOrder[lhs.position] ?? Int.max > positionOrder[rhs.position] ?? Int.max {
            return .orderedDescending
        } else {
            return KeyPathComparator(\Player.id).compare(lhs, rhs)
        }
    }
}

Usage

[player1, player2, player3, player4, player5].sorted(using: PlayerSort(positionOrder: ["QB", "WR", "RB", "TE"]))

Docs:

  1. https://developer.apple.com/documentation/foundation/sortcomparator
  2. https://useyourloaf.com/blog/sortcomparator-and-sortdescriptor/
Sam
  • 125
  • 2
  • 5