davedave1919
davedave1919

Reputation: 123

Sort Swift array of custom types

I have an array of 5 custom objects (all objects are of the same custom type, City). The below is the output of typing po cities in my xcode terminal:

▿ 5 elements
  ▿ 0 : City
    ▿ name : Name
      - rawValue : "Manchester"
    ▿ description : Optional<String>
      - some : "Old Trafford"
  ▿ 1 : City
    ▿ name : Name
      - rawValue : "London"
    ▿ description : Optional<String>
      - some : "Emirates"
  ▿ 2 : City
    ▿ name : Name
      - rawValue : "Liverpool"
    ▿ description : Optional<String>
      - some : "Anfield"
  ▿ 3 : City
    ▿ name : Name
      - rawValue : "Newcastle"
    ▿ description : Optional<String>
      - some : "St James Park"
  ▿ 4 : City
    ▿ name : Name
      - rawValue : "Norwich"
    ▿ description : Optional<String>
      - some : "Carrow Road"

How can I sort these so the first three items returned from this array are always London , Liverpool, Manchester? The final 2 elements order is irrelevant here.

I've tried forcing the order of the elements by their current array position, i.e. forcing City[0] to move to City[2] - however this isn't suitable as the order I receive the elements within City can change - the position needs to be set based on the output of the specific City rawValue

TIA

Upvotes: 0

Views: 215

Answers (3)

Duncan C
Duncan C

Reputation: 131398

Assuming you want to sort your cities based on raw value:

let sortedCities = cities.sorted { $0.name.rawValue < $1.name.rawValue }
sortedCities.forEach {print($0)} 

However, you say you want them sorted in the order London , Liverpool, Manchester, which is not alphabetical.

Edit:

If you have some arbitrary sort order for your Name values, I suggest using a dictionary of sort indexes:

enum Name: String {
    case Manchester
    case London
    case Liverpool
    case Newcastle
    case Norwich
}

let sortIndexes: [Name: Int] = [.London:0,
                              .Liverpool:1,
                              .Manchester: 2,
                              .Newcastle: 3,
                              .Norwich: 4,
    ]

struct City: CustomStringConvertible {
    let name: Name
    let info: String?
    var description: String {
        let info = self.info ?? "nil"
        return "City(name: \(self.name.rawValue), info: \(info))"
    }
}


let cities = [
    City(name: .London, info:  "Emirates"),
    City(name: .Liverpool, info:  "Anfield"),
    City(name: .Manchester, info:  "Old Trafford"),
    City(name: .Newcastle, info:  "St James Park"),
    City(name: .Norwich, info:  "Carrow Road"),
]

let sortedCities = cities.sorted { sortIndexes[$0.name]! < sortIndexes[$1.name]! }

sortedCities.forEach {print($0)}

Alternately, you can make your enums be type Int, and use a description to map to display names, as suggested in Leo's comment.

Here is working example code for that approach:

enum Name: Int, CustomStringConvertible {
    case london
    case liverpool
    case manchester
    case newcastle
    case norwich
    
    var description: String {
        switch self {
        case .london:
            return "London"
        case .liverpool:
            return "Liverpool"
        case .manchester:
            return "Manchester"
        case .newcastle:
            return "Newcastle"
        case .norwich:
            return "Norwich"
        }
    }
}
    
struct City: CustomStringConvertible {
    let name: Name
    let info: String?
    var description: String {
        let info = self.info ?? "nil"
        return "City(name: \(self.name.description), info: \(info))"
    }
}


let cities = [
    City(name: .london, info:  "Emirates"),
    City(name: .liverpool, info:  "Anfield"),
    City(name: .manchester, info:  "Old Trafford"),
    City(name: .newcastle, info:  "St James Park"),
    City(name: .norwich, info:  "Carrow Road"),
]

let sortedCities = cities.sorted { $0.name.rawValue < $1.name.rawValue }

sortedCities.forEach {print($0)}

Upvotes: 1

New Dev
New Dev

Reputation: 49590

Since you don't need to sort the entire array, but just ensure the order of specific elements at the top of the array, it might be best to just extract the elements of interest and construct a new array with those elements at the top and with the rest left unchanged.

This way it has an O(n) complexity (assuming the custom sorting of the top is relatively small so can be assumed to be a constant), so for larger arrays it should be faster than doing the full array sorting.

The general approach would be:

let unsorted: [City] = ...

let topOrder = ["London", "Liverpool", "Manchester"]
var top: [String: City] = [:]
var rest: [City] = []

for city in unsorted {
   if topCorder.contains(city.name) {
      top[city.name] = city
   } else {
      rest.append(city)
   }
}

let sorted = topOrder.compactMap{ top[$0] } + rest

This doesn't handle the case of duplicates (it will drop them), but does handle the case of missing top cities.

Upvotes: 1

user652038
user652038

Reputation:

Comparable can be automatically synthesized for CaseIterable as of Swift 5.3.

import Foundation

struct City {
  enum Name: CaseIterable, Comparable {
    case london
    case liverpool
    case manchester
    case newcastle
    case norwich
  }

  let name: Name
}

extension City.Name: RawRepresentable {
  init?(rawValue: String) {
    guard let name = ( Self.allCases.first { $0.rawValue == rawValue } )
    else { return nil }

    self = name
  }

  var rawValue: String { "\(self)".capitalized }
}
AnyIterator { }
  .prefix(5)
  .map { City.Name.allCases.randomElement()! }
  .map(City.init)
  .sorted(\.name)
public extension Sequence {
  /// Sorted by a common `Comparable` value.
  func sorted<Comparable: Swift.Comparable>(
    _ comparable: (Element) throws -> Comparable
  ) rethrows -> [Element] {
    try sorted(comparable, <)
  }

  /// Sorted by a common `Comparable` value, and sorting closure.
  func sorted<Comparable: Swift.Comparable>(
    _ comparable: (Element) throws -> Comparable,
    _ areInIncreasingOrder: (Comparable, Comparable) throws -> Bool
  ) rethrows -> [Element] {
    try sorted {
      try areInIncreasingOrder(comparable($0), comparable($1))
    }
  }
}

Upvotes: 0

Related Questions