4

I want to create a generic function to sort an array of classes based on a property passed.

For example, I have these classes

public class Car {
    var id: Int
    var manufacturer: String
    var variant: String

    init(id: Int, manufacturer: String, variant: String) {
        self.id = id
        self.manufacturer = manufacturer
        self.variant = variant
    }
}

enum Gender {
    case male
    case female
}

public class Person {
    var id: Int
    var name: String
    var age: Int
    var gender: Gender

    init(id: Int, name: String, age: Int, gender: Gender) {
        self.id = id
        self.name = name
        self.age = age
        self.gender = gender
    }
}

And these arrays,

let cars = [
    Car(id: 1, manufacturer: "Ford", variant: "Focus"),
    Car(id: 2, manufacturer: "Nissan", variant: "Skyline"),
    Car(id: 3, manufacturer: "Dodge", variant: "Charger"),
    Car(id: 4, manufacturer: "Chevrolet", variant: "Camaro"),
    Car(id: 5, manufacturer: "Ford", variant: "Shelby")
]

let persons = [
    Person(id: 1, name: "Ed Sheeran", age: 26, gender: .male),
    Person(id: 2, name: "Phil Collins", age: 66, gender: .male),
    Person(id: 3, name: "Shakira", age: 40, gender: .female),
    Person(id: 4, name: "Rihanna", age: 25, gender: .female),
    Person(id: 5, name: "Bono", age: 57, gender: .male)
]

How to write a generic extension for the array, to sort it based on the property passed? (eg. persons.sort(name) or cars.sort(manufacturer))

Thanks!

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
avrospirit
  • 173
  • 4
  • 15

3 Answers3

6

Here you go:

extension Array {
    mutating func propertySort<T: Comparable>(_ property: (Element) -> T) {
        sort(by: { property($0) < property($1) })
    }
}

Usage:

persons.propertySort({$0.name})

And here is a non-mutating version:

func propertySorted<T: Comparable>(_ property: (Element) -> T) -> [Element] {
    return sorted(by: {property($0) < property($1)})
}

As Leo Dabus pointed out, you can generalise the extension to any MutableCollection that is also a RandomAccessCollection:

extension MutableCollection where Self : RandomAccessCollection {
    ...
Sweeper
  • 213,210
  • 22
  • 193
  • 313
4

Starting with Swift 4 you can define a sorting method which takes a Key-Path Expression as argument. As Leo points out, these methods can be defined more generally as protocols extension methods (for mutable collections and sequences, respectively):

extension MutableCollection where Self: RandomAccessCollection {
    // Mutating in-place sort:
    mutating func sort<T: Comparable>(byKeyPath keyPath: KeyPath<Element, T>) {
        sort(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
    }
}

extension Sequence {
    // Non-mutating sort, returning a new array:
    func sorted<T: Comparable>(byKeyPath keyPath: KeyPath<Element, T>) -> [Element] {
        return sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
    }
}

Example usage:

persons.sort(byKeyPath: \.name)
cars.sort(byKeyPath: \.manufacturer)

For more information about key-path expressions, see SE-0161 Smart KeyPaths: Better Key-Value Coding for Swift.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 1
    Better to extend `MutableCollection` and constrain `Self` to `RandomAccessCollection`. `extension MutableCollection where Self: RandomAccessCollection {` – Leo Dabus Feb 13 '19 at 10:08
  • `extension RangeReplaceableCollection { func sorted(by keyPath: KeyPath) -> Self { return Self( sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] }) ) } }` – Leo Dabus Feb 17 '19 at 22:08
  • And the last question if I extend `StringProtocol` and constrain `Self` to `RangeReplaceableColletion` the filter method declaration changes the return type to `Self ` declaration `func filter(_ isIncluded: (Character) throws -> Bool) rethrows -> Self`. If I remove the constrain it throws "Cannot return `[Character]` instead of `Self`. The filter declaration now shows `[Self.Element]`. I was trying to achieve the same with the sorted method. Is there any special constrain to make the sort method return `Self` instead of `[Self.Element]`? Is my implementation above correct? – Leo Dabus Feb 17 '19 at 22:28
  • 1
    I have also considered the following approach `extension MutableCollection where Self: RandomAccessCollection { func sorted(by keyPath: KeyPath) -> Self { var source = self source.sort { $0[keyPath: keyPath] < $1[keyPath: keyPath] } return source } }` – Leo Dabus Feb 18 '19 at 00:21
  • 1
    @LeoDabus: I modeled my suggestions after the existing methods in the standard library. There is only `MutableCollection.sort()` and `Sequence.sorted() -> [Element]`, but no `MutableCollection.sorted() -> Self` method. One *could* define such a method (and your second approach seems to be the better one, the first approach creates an intermediate array). – Martin R Feb 19 '19 at 06:19
4

edit/update:

For Xcode 13.0+, iOS 15.0+, iPadOS 15.0+, macOS 12.0+, Mac Catalyst 15.0+, tvOS 15.0+, watchOS 8.0+ you can use KeyPathComparator:

let sortedPeople1 = people.sorted(using: KeyPathComparator(\.age))  // [{id 4, name "Rihanna", age 25, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 3, name "Shakira", age 40, female}, {id 5, name "Bono", age 57, male}, {id 2, name "Phil Collins", age 66, male}]
let sortedPeople2 = people.sorted(using: KeyPathComparator(\.age, order: .reverse))  // [{id 2, name "Phil Collins", age 66, male}, {id 5, name "Bono", age 57, male}, {id 3, name "Shakira", age 40, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 4, name "Rihanna", age 25, female}]

You can also use multiple sorting criteria and order:

let sortedPeople3 = people.sorted(using: [KeyPathComparator(\.age, order: .reverse), KeyPathComparator(\.name)])  // [{id 2, name "Phil Collins", age 66, male}, {id 5, name "Bono", age 57, male}, {id 3, name "Shakira", age 40, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 4, name "Rihanna", age 25, female}]
let sortedPeople4 = people.sorted(using: [KeyPathComparator(\.age, order: .reverse), KeyPathComparator(\.name)])  // [{id 2, name "Phil Collins", age 66, male}, {id 5, name "Bono", age 57, male}, {id 3, name "Shakira", age 40, female}, {id 1, name "Ed Sheeran", age 26, male}, {id 4, name "Rihanna", age 25, female}]

original answer
Expanding on @MartinR answer and @Sweeper answer to allow increasing (<) or decreasing (>) sort as well as throw and default sort ascending methods:


extension MutableCollection where Self: RandomAccessCollection {
    mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T) rethrows {
        try sort(predicate, by: <)
    }
    mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T, by areInIncreasingOrder: ((T, T) throws -> Bool)) rethrows {
        try sort { try areInIncreasingOrder(predicate($0), predicate($1)) }
    }
}

extension Sequence {
    func sorted<T: Comparable>(_ predicate: (Element) throws -> T) rethrows -> [Element] {
        try sorted(predicate, by: <)
    }
    func sorted<T: Comparable>(_ predicate: (Element) throws -> T, by areInIncreasingOrder: ((T,T) throws -> Bool)) rethrows -> [Element] {
        try sorted { try areInIncreasingOrder(predicate($0), predicate($1)) }
    }
}

people.sorted(\.age)
people.sorted(\.age, by: >)

cars.sorted(\.manufacturer)
cars.sorted(\.manufacturer, by: >)

edit/update:

To suport sorting a custom object by an optional property that conforms to Comparable protocol:


extension MutableCollection where Self: RandomAccessCollection {
    mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T?) rethrows {
        try sort(predicate, by: <)
    }

    mutating func sort<T: Comparable>(_ predicate: (Element) throws -> T?, by areInIncreasingOrder: ((T, T) throws -> Bool)) rethrows {
        try sort(by: {
            switch try (predicate($0), predicate($1)) {
            case let (lhs?, rhs?): return try areInIncreasingOrder(lhs, rhs)
            case (.none, _): return false
            case (_, .none): return true
            }
        })
    }
}

extension Sequence {
    func sorted<T: Comparable>(_ predicate: (Element) throws -> T?) rethrows -> [Element]  {
        try sorted(predicate, by: <)
    }
    func sorted<T: Comparable>(_ predicate: (Element) throws -> T?, by areInIncreasingOrder: ((T,T) throws -> Bool)) rethrows -> [Element]  {
        try sorted(by: {
            switch try (predicate($0), predicate($1)) {
            case let (lhs?, rhs?): return try areInIncreasingOrder(lhs, rhs)
            case (.none, _): return false
            case (_, .none): return true
            }
        })
    }
}

Usage:

array.sort(\.optionalStringProperty) {
    $0.localizedStandardCompare($1) == .orderedAscending
}
print(array)
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571