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.
11 Answers 11
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"]
-
3Great 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ță– Cristi Băluță2018年12月18日 13:29:34 +00:00Commented Dec 18, 2018 at 13:29
-
1@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– RobertChals2020年06月09日 19:42:44 +00:00Commented Jun 9, 2020 at 19:42
-
1I 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– Lyndsey Scott2020年06月15日 14:51:19 +00:00Commented Jun 15, 2020 at 14:51
-
Works beautifully as is. But in my use case, i desparately need the original array to be sorted. a) Rewriting the extension to use self.sort and mutating instead of sorted does not change the order of the original array. b) using removeAll() on original array, and then originalArray = sortedArray doesnt change the order of the original either and c) deleting the original object(which holds an array) from the modelContext, and re-initializing a new object and then copying the sorted array to the array property doesnt work either. Im lost here. Does anyone know how to sort the original array?CameronBaba– CameronBaba2024年07月29日 21:01:54 +00:00Commented Jul 29, 2024 at 21:01
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"]
-
Of course this code needs to safely look at the index results incase a position isn't in the positionOrders array.rmaddy– rmaddy2017年03月27日 21:40:04 +00:00Commented Mar 27, 2017 at 21:40
-
And you need
0ドル.position
and1ドル.position
.rmaddy– rmaddy2017年03月27日 21:40:32 +00:00Commented Mar 27, 2017 at 21:40 -
Maybe ordering[0ドル.position] rather than positionOrders[0ドル.position]antonio081014– antonio0810142017年03月27日 21:43:18 +00:00Commented Mar 27, 2017 at 21:43
-
@antonio081014 Overlooked copy pasting mistakes strike again! Fixed.Alexander– Alexander2017年03月27日 21:44:18 +00:00Commented Mar 27, 2017 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– Fattie2023年02月06日 12:24:09 +00:00Commented Feb 6, 2023 at 12:24
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)
-
I guess worst-case time complexity of the array-by-array sort is O(m n log n), where m is the length of the order-array and n is the length of the array to be sorted...Jens Schwarzer– Jens Schwarzer2023年11月17日 10:02:11 +00:00Commented Nov 17, 2023 at 10:02
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"]
-
Defining the default ordering of Strings of Arrays is not something you should be doing, because you're not the author or
String
norArray
. What if someone else tries to use this to reorder their own strings? Spoiler: they can't. ExtendingArray<YourOwnEnumType>
would be fine.Alexander– Alexander2019年08月01日 18:40:01 +00:00Commented Aug 1, 2019 at 18:40
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
}
}
-
2A good example of why not to use libraries. The function is completely built-in to Swift.Fattie– Fattie2023年02月06日 12:16:40 +00:00Commented Feb 6, 2023 at 12:16
What I will do:
- 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.
- 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)")
-
you can just use flatmap to create the
playersArray
Alexander– Alexander2017年03月28日 01:01:48 +00:00Commented Mar 28, 2017 at 1:01 -
@Alexander, actually you are quite right. Just updated the answer. Thank you.antonio081014– antonio0810142017年03月28日 16:49:30 +00:00Commented Mar 28, 2017 at 16:49
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
-
2That's the straightforward solution but doesn't look very efficient if you have tons of objectsCristi Băluță– Cristi Băluță2018年12月18日 13:31:33 +00:00Commented Dec 18, 2018 at 13:31
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.
-
-
Generally to sort, you use the Swift function ...
.sort
:) :)Fattie– Fattie2023年02月06日 12:15:53 +00:00Commented Feb 6, 2023 at 12:15
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"]
-
2You should not add data (the defaultOrder array) into an extensionCasper Zandbergen– Casper Zandbergen2018年11月28日 11:36:55 +00:00Commented Nov 28, 2018 at 11:36
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:
-
This is staggeringly more code than is needed for a trivial operation.Fattie– Fattie2023年02月06日 12:15:08 +00:00Commented Feb 6, 2023 at 12:15
This post is old but maybe someone (like me) will stumble upon it again in 2024. I had a similar issue, but all the other answers here seemed overly and unnecessarily complicated to me, so I figured out the following solution:
let players: [Player] = ... // your array here
let positionOrders = ["QB", "WR", "RB", "TE"]
let sortedPlayers = players.sorted {
return positionOrders.firstIndex(of: 0ドル.position)! < positionOrders.firstIndex(of: 1ドル.position)!
}
This solution uses Swift's standard sorting method, using the provided predicate of comparing the index of a player.position
within the positionOrders
to determine the sort order.
Disclaimer: Since I am force-unwrapping in my code-snippet, this solution currently does not account for player.position
values that are not included in positionOrders
. Consider checking for and/or removing any items in players
which have a player.position
that is not contained in positionOrders
, or provide a default if the firstIndex cannot be found.
positionOrders
constant or does it change at runtime?struct
of constants). @Alexander thanks for the input. The suggestion you made worked great.