问题
I have an array of custom objects. The objects have a custom enum var 'type'. The different types are as follows:
- .movie
- .tv
- .trailer
- .genre
- .article
I would like to sort the array by a pattern [movie, tv, trailer, genre, article, movie, tv, trailer, genre, article, ....etc]
I have made the enum conform to comparable but (perhaps I am mistaken), if I sort by type won't it sort the array as such:
[movie, movie, movie, tv, tv, tv, trailer, trailer, trailer, etc...]
..when in fact I want them in a pattern one type after another.
[movie, tv, trailer, genre, article, movie, tv, trailer, genre, article, movie, tv, trailer, genre, article, and so on ...]
回答1:
Here is one way to approach it. First use map
to associate a sorting Int
index
with each item. Use a dictionary to keep track of the last index associated with each Kind
and increment it by the number of different kinds. This will give a unique sorting index to every item in your array with items being sorted into the desired patten due to the increments added to repeated Kind
s.
enum Kind: Int, CaseIterable {
case movie, tv, trailer, genre, article
}
struct Item: CustomStringConvertible {
var description: String { "\(name): \(kind)" }
let id: Int
let name: String
let kind: Kind
}
let items: [Item] = [
.init(id: 1, name: "D", kind: .tv),
.init(id: 2, name: "B", kind: .movie),
.init(id: 3, name: "F", kind: .trailer),
.init(id: 4, name: "H", kind: .genre),
.init(id: 5, name: "J", kind: .article),
.init(id: 6, name: "C", kind: .tv),
.init(id: 7, name: "A", kind: .movie),
.init(id: 8, name: "E", kind: .trailer),
.init(id: 9, name: "G", kind: .genre),
.init(id: 10, name: "I", kind: .article)]
// Dictionary used to generate a unique sorting index for each kind
var dict: [Kind: Int] = [:]
typealias IndexedItem = (index: Int, element: Item)
// Assign a sorting index to each item. Repeated Kinds will be incremented by
// allCases.count so that they sort into the next group
let items2: [IndexedItem] = items.map { item in
dict[item.kind, default: item.kind.rawValue] += Kind.allCases.count
return (dict[item.kind]!, item)
}
let result = items2.sorted { $0.index < $1.index }.map(\.element)
print(result)
Output
[B: movie, D: tv, F: trailer, H: genre, J: article, A: movie, C: tv, E: trailer, G: genre, I: article]
Radix Sort - A faster sort
Since all of the indices are unique, we can create the result
array with a radix sort:
// Assign a sorting index to each item. Repeated Kinds will be incremented by
// allCases.count so that they sort into the next group
let cases = Kind.allCases.count
let items2: [IndexedItem] = items.map { item in
dict[item.kind, default: item.kind.rawValue - cases] += cases
return (dict[item.kind]!, item)
}
// Use a radix sort to order the items
let maxIndex = dict.values.max() ?? -1
var slots = [Item?](repeating: nil, count: maxIndex + 1)
items2.forEach { slots[$0.index] = $0.element }
let result = slots.compactMap { $0 }
This amounts to creating an array of nil
large enough to hold the largest index, putting the items into the array using their index
, and then removing the nil
s (empty slots) with compactMap()
. This sorting algorithm is O(n) instead of O(n log n) like the general sorting algorithm.
回答2:
In old Swift versions (Swift 5.2.x or earlier) when conforming an enumeration to Comparable
protocol you would need to declare its rawValue as Int
instead of String
otherwise it wouldn't make any sense since enumerations are not lexicographically sorted in general. In Swift 5.3 or later If you would like it to be Synthesized automatically you can't declare any rawValue type. You can check this post at Swift evolution about Synthesized Comparable conformance for enum types
Enumeration types which opt-in to a synthesized Comparable conformance would compare according to case declaration order, with later cases comparing greater than earlier cases. Only enum types with no associated values and enum types with only Comparable associated values would be eligible for synthesized conformances. The latter kind of enums will compare by case declaration order first, and then lexicographically by payload values. No enum types with raw values would qualify.
Swift 5.3 or later
enum Kind: Comparable {
case movie, tv, trailer, genre, article
}
Once you have done that you can simply sort your collection using the custom sort I have pointed as duplicate for this question:
extension MutableCollection where Self: RandomAccessCollection {
mutating func sort<T: Comparable>(_ predicate: (Element) -> T, by areInIncreasingOrder: ((T, T) -> Bool) = (<)) {
sort { areInIncreasingOrder(predicate($0),predicate($1)) }
}
}
extension Sequence {
func sorted<T: Comparable>(_ predicate: (Element) -> T, by areInIncreasingOrder: ((T,T)-> Bool) = (<)) -> [Element] {
sorted { areInIncreasingOrder(predicate($0),predicate($1)) }
}
}
Playground testing:
struct Item {
let id: Int
let name: String
let kind: Kind
}
let items: [Item] = [
.init(id: 1, name: "D", kind: .tv),
.init(id: 2, name: "B", kind: .movie),
.init(id: 3, name: "F", kind: .trailer),
.init(id: 4, name: "H", kind: .genre),
.init(id: 5, name: "J", kind: .article),
.init(id: 6, name: "C", kind: .tv),
.init(id: 7, name: "A", kind: .movie),
.init(id: 8, name: "E", kind: .trailer),
.init(id: 9, name: "G", kind: .genre),
.init(id: 10, name: "I", kind: .article)]
items.sorted(\.kind) // [{id 2, name "B", movie}, {id 7, name "A", movie}, {id 1, name "D", tv}, {id 6, name "C", tv}, {id 3, name "F", trailer}, {id 8, name "E", trailer}, {id 4, name "H", genre}, {id 9, name "G", genre}, {id 5, name "J", article}, {id 10, name "I", article}]
edit/update
I don't know if there is a simpler way to accomplish this kind of sort (I would love to get some feedback into this) but You can sort your items by name, group them by kind and then transpose your items. You would need to make your enumeration CaseIterable and declare its rawValue as Int starting from zero. So add those helpers to your project:
extension Collection where Element: RandomAccessCollection, Element.Indices == Range<Int> {
func transposed() -> [[Element.Element]] {
(0..<(max(\.count)?.count ?? .zero)).map {
index in compactMap { $0.indices ~= index ? $0[index] : nil }
}
}
}
extension Sequence {
func max<T: Comparable>(_ predicate: (Element) -> T) -> Element? {
self.max(by: { predicate($0) < predicate($1) })
}
}
And then:
enum Kind: Int, CaseIterable {
case movie = 0, tv, trailer, genre, article
}
let grouped: [[Item]] = items.reduce(into: .init(repeating: [], count: Kind.allCases.count)) { result, item in
result[item.kind.rawValue].append(item)
}
let transposed = grouped.map{$0.sorted(\.name)}.transposed()
print(transposed) // [[Item(id: 7, name: "A", kind: Kind.movie), Item(id: 6, name: "C", kind: Kind.tv), Item(id: 8, name: "E", kind: Kind.trailer), Item(id: 9, name: "G", kind: Kind.genre), Item(id: 10, name: "I", kind: Kind.article)], [Item(id: 2, name: "B", kind: Kind.movie), Item(id: 1, name: "D", kind: Kind.tv), Item(id: 3, name: "F", kind: Kind.trailer), Item(id: 4, name: "H", kind: Kind.genre), Item(id: 5, name: "J", kind: Kind.article)]]
回答3:
This is possible with rawValue. You should declare the Enum of type Int.
enum Catagory: Int {
case movie, tv, trailer, genre, article
}
then, you can sort an array of objects having enum variable "type" using a sort function
let sortedArray = array.sorted(by: {$0.type.rawValue < $1.type.rawValue})
来源:https://stackoverflow.com/questions/65501583/ios-swift-sort-array-by-enum-pattern