-3

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 ...]
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Mike Simz
  • 3,976
  • 4
  • 26
  • 43
  • 3
    What have you tried? Where are you stuck? – jtbandes Dec 30 '20 at 03:18
  • 1
    You just need to make your enumeration conform to Comarable. Btw I would change your enumeration name to `Kind` – Leo Dabus Dec 30 '20 at 03:22
  • I've made the enum conform to Comparable - but wouldn't that make all movies be listed before tv and so on [movie, movie movie, tv, tv, tv, trailer, trailer, trailer, etc...]? – Mike Simz Dec 30 '20 at 03:37
  • 1
    You don’t want to sort them, you want to order them following a pattern. Just group them into arrays by their kind and iterate through the array of arrays getting an item of each array on each pass. – EmilioPelaez Dec 31 '20 at 15:29

3 Answers3

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)]]
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • I have added the minimum Swift version for this to work if that was the missing info. Other than that there is nothing wrong with this post so please leave a comment. – Leo Dabus Dec 30 '20 at 05:12
  • Thank you for the detailed response. Your playground test shows all movies first and then tv and so on. What I am trying to accomplish is a sort with the following pattern: [movie, tv, trailer, genre, article, movie, tv, trailer, genre, article] ...and so on. 1 kind, one after another @Leo Dabus – Mike Simz Dec 30 '20 at 14:14
  • Thats a very unusual kind of sorting and still is not clear the logic of your question. What would be the sorting method if they have the same kind (type)? Btw you said `[movie, movie, movie, tv, tv, tv, trailer, trailer, trailer, etc...]` in your post. – Leo Dabus Dec 30 '20 at 14:18
  • I think my post was confusing. I was saying this is what i dont't want: `[movie, movie, movie, tv, tv, tv, trailer, trailer, trailer, etc...]` – Mike Simz Dec 30 '20 at 14:20
  • @MikeSimz check my last edit. Not sure if there is a simpler way to accomplish what you are asking for – Leo Dabus Dec 30 '20 at 15:03
  • This probably wouldn't be noticeable until the number of elements was in the hundreds, but I believe that dividing the elements into their groups and then sorting each of the smaller groups would be more performant than sorting the large list and then dividing it into smaller groups. – vacawama Dec 31 '20 at 14:07
  • @vacawama OP never said it was necessary to sort by names. I wonder if there is a way to sort it instead of grouping and transposing. Thanks for the input anyway. It is already some improvement – Leo Dabus Dec 31 '20 at 14:08
  • You'd need a way to make subsequent enums of the same type have a higher value for sorting. Perhaps you first map the array assigning sorting keys. The enums could have values 1 through 5, and then each time you see another of a type, you increase by 10. Then sort by this Int key. – vacawama Dec 31 '20 at 14:13
  • Can you show this on a gist? I tried already keeping the last item kind when sorting but I didn't succeed. I think grouping and transposing is the best I can achieve. – Leo Dabus Dec 31 '20 at 14:15
  • @vacawama note that you need to cycle through the kinds and there is no guarantee that you will have all kinds on the collection – Leo Dabus Dec 31 '20 at 14:24
  • 1
    @LeoDabus, check out https://gist.github.com/vacawama/1c6ad1cb6cf2a5d6da5761c20087894b – vacawama Dec 31 '20 at 14:52
  • @vacawama that's clever. I think you should post your solution as well – Leo Dabus Dec 31 '20 at 15:02
  • @vacawama is there a way to sort by name as well on the fly? – Leo Dabus Dec 31 '20 at 15:10
  • @LeoDabus, I added my answer. I can think of no way to sort by name on the fly, so you'd either either need to sort the list by name first before assigning the indices, or you could group the elements and sort the groups and use their positions in the sorted groups to determine the indices. – vacawama Dec 31 '20 at 15:45
  • @vacawama thanks. Is there a way to compare our approaches complexity? – Leo Dabus Dec 31 '20 at 15:51
2

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 Kinds.

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 nils (empty slots) with compactMap(). This sorting algorithm is O(n) instead of O(n log n) like the general sorting algorithm.

vacawama
  • 150,663
  • 30
  • 266
  • 294
  • @LeoDabus, now that I have updated my solution with a radix sort, the complexity of our approaches in similar. Both are O(n). – vacawama Dec 31 '20 at 18:11
0

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})
Khushabu
  • 101
  • 6
  • There is no need to declare any rawValue at all. Check the link I have posted below about Synthesized Comparable conformance for enum types( (Swift 5.3). This is how it used to be done in older Swift versions. – Leo Dabus Dec 30 '20 at 05:08