0

I have an array of Item. The order is never guaranteed as the collection is served from an API based on a number of user actions elsewhere.

However I need to order this collection by a type field so I can iterate over it elsewhere correctly.

In the case of the example playground below, I need a reordered collection that would follow this logic:

[
  Item(type: .foo),
  Item(type: .bar),

  Item(type: .boo),
  Item(type: .baz),

  Item(type: .boom),
  Item(type: .bang)

]

Specifically .foo and .bar should be the first 2 items, .boom and bang will be the last 2 items.

These fixed items are unique, no duplicates will exist in the response.

Everything left over should be between these 2 groups, in the same order as they are in the original collection.

I have tried to split the item into 2 collections within var output: [Item] and insert the second collection at an index, however the ordering is still off.

How can I achieve this?

import UIKit

enum ItemType: String {
  case foo
  case bar
  case boo
  case baz
  case boom
  case bang
}

struct Item {
  let type: ItemType
}

let props = [
  Item(type: .bang),
  Item(type: .bar),
  Item(type: .baz),
  Item(type: .boo),
  Item(type: .boom),
  Item(type: .foo)
]

/*
 case foo
 case bar
 > ----------- should be ordered in same order as in `props`
 case boom
 case bang
*/

var output: [Item] {
  var fixedPosition: [Item] = []
  var dynamicPosition: [Item] = []

  props.forEach { item in
    if (item.type == .foo || item.type == .bar || item.type == .boom || item.type == .bang ){
      fixedPosition.append(item)
    } else {
      dynamicPosition.append(item)
    }
  }

  var result: [Item] = fixedPosition
  result.insert(contentsOf: dynamicPosition, at: 1)

  return result
}

print(output.map { $0.type })
Harry Blue
  • 4,202
  • 10
  • 39
  • 78

2 Answers2

1

You can give each enum case an int value, and stable sort it:

func intValue(for itemType: ItemType) -> Int {
    switch itemType {
    case .foo:
        return 0
    case .bar:
        return 1
    case .boo, .baz: // the order of these should remain the same, so they get the same number
        return 2
    case .boom:
        return 3
    case .bang:
        return 4
    }
}

let sorted = props.stableSorted(by: { intValue(for: $0.type) < intValue(for: $1.type) })

stableSort is taken from this answer by leavez:

extension RandomAccessCollection {

    /// return a sorted collection
    /// this use a stable sort algorithm
    ///
    /// - Parameter areInIncreasingOrder: return nil when two element are equal
    /// - Returns: the sorted collection
    public func stableSorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element] {

        let sorted = try enumerated().sorted { (one, another) -> Bool in
            if try areInIncreasingOrder(one.element, another.element) {
                return true
            } else {
                return one.offset < another.offset
            }
        }
        return sorted.map { $0.element }
    }
}
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • It can be done without even `intValue` casting. You just make `ItemType` `Comparable`, and in function `<` make sure `.foo` < `.bar` < (`.boo`, `.baz`) < `.boom` < `.bang`. – user28434'mstep Jun 01 '20 at 09:49
  • @user28434 Yeah, but it might not make sense for `ItemType` to be comparable. And I can't implement `<` in such a way that expresses the intention of the OP as clearly as listing a bunch of _numbers_ in a big switch statement does. Nevertheless, why not post your solution as answer? – Sweeper Jun 01 '20 at 10:04
1

Variation with ItemType implementing Comparable protocol:

extension ItemType: Comparable {
    static func < (lhs: ItemType, rhs: ItemType) -> Bool {
        // function returns `true` iff lhs is smaller than res

        // order of cases matters here, switch matches them left-to-right, top-to-bottom
        switch (lhs, rhs) {
        case (.foo, _): // .foo is before everyone else
            return true
        case (_, .foo): // nothing is "smaller" than .foo
            return false
        case (.bar, _):  // .bar can be preceded only by .foo (previous 2 cases handle it)
            return true
        case (_, .bang): // .bang is always last
            return true
        case (.bang, _): // nothing is "bigger" than .bang
            return false
        case (_, .boom): // .boom can be only before .bang (previous 2 cases handle it)
            return true
        default: // otherwise we don't care about comparing items, neither item is "smaller" than other
            return false
        }
    }
}
user28434'mstep
  • 6,290
  • 2
  • 20
  • 35