Сортировка массива объектов по нескольким свойствам с помощью Swift Tuple

В моей последней статье «Как сортировать по нескольким свойствам в Swift» мы узнаем, как сортировать массив объектов по нескольким критериям.

Окончательную реализацию можно резюмировать следующим образом.

Сортировка по нескольким критериям означает сортировку, при которой мы сравниваем первые критерии, и только если первый критерий равен, мы переходим к следующему. Мы делаем это до тех пор, пока не найдем неравный критерий.

Что можно перевести в псевдокод следующим образом:

let sortedObjects = objects.sorted { (lhs, rhs) in
    for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] {
        if lhsCriteria == rhsCriteria {
            continue
        }

        return lhsCriteria < rhsCriteria
    }
}

Это включает в себя объявление массива предикатов и циклов, но мы можем удалить большую часть этой логики с помощью кортежа.

Сравнение кортежей

Swift имеет операторы сравнения, такие как <, >, <=, >= для нас, со следующей реализацией.

/// Returns a Boolean value indicating whether the first tuple is ordered
/// before the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).
///
/// - Parameters:
///   - lhs: A tuple of `Comparable` elements.
///   - rhs: Another tuple of elements of the same type as `lhs`.
@inlinable public func < <A, B>(lhs: (A, B), rhs: (A, B)) -> Bool where A : Comparable, B : Comparable

Я хочу, чтобы вы сосредоточились на следующем предложении:

/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).

Именно так мы реализуем нашу сортировку. Давайте сравним это с нашим псевдокодом. Вы можете видеть, что сравнение кортежей — это то, как мы сравниваем наши критерии сортировки.

let sortedObjects = objects.sorted { (lhs, rhs) in
    for (lhsCriteria, rhsCriteria) in [(lhsCrtria1, rhsCriteria1), (lhsCrtria2, rhsCriteria2), (lhsCrtria3, rhsCriteria3), ... , (lhsCrtriaN, rhsCriteriaN)] {
        if lhsCriteria == rhsCriteria {
            continue
        }

        return lhsCriteria < rhsCriteria
    }
}

Реализуем сортировку критериев по кортежу.

Пример

Мы будем использовать тот же объект, что и в нашем последнем примере.

struct BlogPost {
    let title: String
    let pageView: Int
    let sessionDuration: Double
}

По возрастанию

Чтобы отсортировать по возрастанию, мы используем оператор меньше чем (<) над кортежами.

let posts: [BlogPost] = [
    BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
    BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
    BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
    BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
    BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
    BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
    BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]

let sorted = posts.sorted { (lhs, rhs) -> Bool in
    return (lhs.title, lhs.pageView, lhs.sessionDuration) < (rhs.title, rhs.pageView, rhs.sessionDuration)
}

print(sorted)

Вот результат:

[BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0),
 BlogPost(title: "Alice", pageView: 1, sessionDuration: 2.0),
 BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
 BlogPost(title: "Alice", pageView: 2, sessionDuration: 1.0),
 BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
 BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2.0),
 BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1.0)]

Это сравнит первый элемент lhs.title < rhs.title и если lhs.title == rhs.title, мы сравним (lhs.pageView, lhs.sessionDuration) < (rhs.pageView, rhs.sessionDuration). Это идентично нашей предыдущей реализации.

В порядке убывания

Чтобы отсортировать по убыванию, мы используем оператор больше, чем (>) над кортежами.

let posts: [BlogPost] = [
    BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
    BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
    BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
    BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
    BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
    BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
    BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]

let sorted = posts.sorted { (lhs, rhs) -> Bool in
    return (lhs.title, lhs.pageView, lhs.sessionDuration) > (rhs.title, rhs.pageView, rhs.sessionDuration)
}

print(sorted)

Вот результат:

[BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1.0),
 BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2.0),
 BlogPost(title: "Angero", pageView: 1, sessionDuration: 2.0),
 BlogPost(title: "Alice", pageView: 2, sessionDuration: 1.0),
 BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0),
 BlogPost(title: "Alice", pageView: 1, sessionDuration: 2.0),
 BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0)]

Разный порядок сортировки для каждого элемента

Если вам нужны разные порядки сортировки для каждого свойства, вы можете сделать это, поменяв местами элементы в кортежах.

Мы не можем изменить оператор для каждого элемента в кортежах, но изменение его позиции приведет к тому же эффекту. Рассмотрим следующие примеры.

lhs.property1 < rhs.proerty1 будет сортироваться в порядке возрастания.

lhs.property2 > rhs.proerty2 будет сортироваться в порядке убывания, но все элементы должны сравниваться с использованием одного и того же оператора. Итак, меняем местами.

rhs.property2 < lhs.property2 равно lhs.property2 > rhs.property2, который сортируется по убыванию.

Итак, если мы хотим отсортировать title по возрастанию, а pageView и sessionDuration по убыванию, мы можем использовать следующий код.

let posts: [BlogPost] = [
    BlogPost(title: "Zoo", pageView: 5, sessionDuration: 1),
    BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
    BlogPost(title: "Alice", pageView: 1, sessionDuration: 2),
    BlogPost(title: "Alice", pageView: 2, sessionDuration: 1),
    BlogPost(title: "Zoo", pageView: 4, sessionDuration: 2),
    BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
    BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
]

let sorted = posts.sorted { (lhs, rhs) -> Bool in
    return (lhs.title, rhs.pageView, rhs.sessionDuration) < (rhs.title, lhs.pageView, lhs.sessionDuration)
}

print(sorted)

Предостережения

Мы можем сравнивать только до шести свойств, используя кортежи. Swift только перегружает сравнение кортежей до шести элементов.

/// Returns a Boolean value indicating whether the first tuple is ordered
/// after or the same as the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is after or the same as the second tuple if and only if
/// `a1 > b1` or (`a1 == b1` and
/// `(a2, ..., aN) >= (b2, ..., bN)`).
///
/// - Parameters:
///   - lhs: A tuple of `Comparable` elements.
///   - rhs: Another tuple of elements of the same type as `lhs`.
@inlinable public func >= <A, B, C, D, E, F>(lhs: (A, B, C, D, E, F), rhs: (A, B, C, D, E, F)) -> Bool where A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable, F : Comparable

Я бы сказал, что шести свойств должно быть достаточно для большинства случаев, но если это не так, вы можете вернуться к моей последней реализации.