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!

5
  • You can pass the property name as a string in the sort() method. And then add this in your class: stackoverflow.com/a/24919834/6638533 Commented Oct 6, 2017 at 5:57
  • 1
    @samAlvin He's asking about Swift, not C#. Commented Oct 6, 2017 at 5:57
  • @Alexander whoops my bad, I should be more careful in the future Commented Oct 6, 2017 at 5:59
  • Would you be interested in a Swift 4 solution as well? Commented Oct 6, 2017 at 6:54
  • sure @MartinR. Might help someone else as well. Commented Oct 6, 2017 at 7:56

3 Answers 3

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 {
    ...
Sign up to request clarification or add additional context in comments.

1 Comment

@LeoDabus Thanks. Edited.
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.

5 Comments

Better to extend MutableCollection and constrain Self to RandomAccessCollection. extension MutableCollection where Self: RandomAccessCollection {
extension RangeReplaceableCollection { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> Self { return Self( sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] }) ) } }
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?
I have also considered the following approach extension MutableCollection where Self: RandomAccessCollection { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> Self { var source = self source.sort { $0[keyPath: keyPath] < $1[keyPath: keyPath] } return source } }
@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).
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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.