Как сортировать по нескольким свойствам в Swift

Сортировка проста, если вы делаете это по одному критерию или одному свойству. В Swift уже есть функция для этого.

Вот пример, где мы сортируем массив int.

let numbers = [3, 5, 6, 1, 8, 2]
        
let sortedNumbers = numbers.sorted { (lhs, rhs) in
    return lhs < rhs
}

// [1, 2, 3, 5, 6, 8]

Но будет время, когда вам нужно отсортировать вещи по нескольким критериям или свойствам. Чтобы продемонстрировать это, давайте создадим структуру в качестве примера.

Здесь у нас есть простая структура BlogPost с title сообщения и два поля со статистическими данными, pageView и sessionDuration.

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

А вот пример данных.

extension BlogPost {
    static var examples: [BlogPost] = [
        BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
        BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
        BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
        BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
        BlogPost(title: "Abena", pageView: 4, sessionDuration: 10)
    ]
}

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

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

Что такое сортировка по нескольким критериям и свойствам

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

Псевдокод будет выглядеть примерно так:

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

        return lhsCriteria < rhsCriteria // <3>
    }
}

<1> Мы прокручиваем список критериев, начиная с самого важного (первого).

<2> Если критерии порядка равны и мы не можем определить порядок, мы переходим к следующему критерию.

<3> Если мы можем определить порядок между двумя объектами по критериям, мы останавливаемся и возвращаем результат.

Если вам трудно понять псевдокод, не волнуйтесь. Я не профессиональный писатель псевдокода. Следующий пример должен прояснить ситуацию.

Сортировка массива объектов по двум полям

Мы будем использовать тот же сценарий, упомянутый выше. Мы хотим отсортировать BlogPost по производительности. Наша производительность определяется количеством просмотров страниц (pageView), и если посты в блоге имеют одинаковое количество просмотров страниц, мы используем продолжительность сеанса (sessionDuration).

Вот структура BlogPost и пример данных, которые мы использовали в предыдущем примере.

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

extension BlogPost {
    static var examples: [BlogPost] = [
        BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
        BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
        BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
        BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
        BlogPost(title: "Abena", pageView: 4, sessionDuration: 10)
    ]
}

То, как мы измеряем производительность, можно перевести в этот код.

let popularPosts = BlogPost.examples.sorted { (lhs, rhs) in
    if lhs.pageView == rhs.pageView { // <1>
        return lhs.sessionDuration > rhs.sessionDuration
    }
    
    return lhs.pageView > rhs.pageView // <2>
}

<1> Если сообщения в блоге имеют один и тот же просмотр страницы, мы используем продолжительность сеанса.

<2> Если количество просмотров страниц не равно, мы можем определить порядок просмотров страниц. (сортируем по убыванию)

Вот наш результат.

[BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2.0), 
BlogPost(title: "Abena", pageView: 4, sessionDuration: 10.0), 
BlogPost(title: "Alice", pageView: 1, sessionDuration: 3.0), 
BlogPost(title: "Peter", pageView: 1, sessionDuration: 2.0), 
BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1.0)]

Сортировка массива объектов по трем полям

Как видите, выполнить сортировку по двум критериям очень просто. Давайте увеличим количество критериев в уравнении. Если посты в блоге имеют одинаковую эффективность, мы отсортируем их по имени.

Давайте добавим больше сообщений в блоге к нашим примерам.

extension BlogPost {
    static var examples2: [BlogPost] = [
        BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2),
        BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
        BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
        BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
        BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
        BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
        BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
    ]
}

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

let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
    if lhs.pageView == rhs.pageView {
        if lhs.sessionDuration == rhs.sessionDuration { // <1>
            return lhs.title < rhs.title
        }
        
        return lhs.sessionDuration > rhs.sessionDuration
    }
    
    return lhs.pageView > rhs.pageView
}

<1> Мы добавляем еще один параметр if, чтобы проверить, имеют ли сообщения в блоге одинаковую продолжительность сеанса, и отсортировать их по заголовку, если они получили одинаковое количество просмотров страниц и продолжительность сеанса.

Полученные результаты:

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

Проблема

Мы можем использовать ту же логику для двух и трех критериев. Единственная проблема заключается в том, что чем больше критериев, тем больше вложенных if-else вам понадобится.

Вот пример нескольких критериев, которые могут привести к пирамиде гибели.

let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in
    if lhs.pageView == rhs.pageView {
        if lhs.sessionDuration == rhs.sessionDuration { 
            if lhs.nextCriteria == rhs.nextCriteria { 
                if lhs.nextCriteria == rhs.nextCriteria { 
                    ....
                }

                ...
            }

            ...
        }
        
        return lhs.sessionDuration > rhs.sessionDuration
    }
    
    return lhs.pageView > rhs.pageView
}

Сортировать массив объектов по N полям

Чтобы разгадать пирамиду судьбы, давайте вернемся к тому псевдокоду, который мы видели ранее.

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
    }
}

Приведенный выше код — не единственный способ решения такого рода проблем, но ключ должен быть похожим. Суть в том, что мы упаковываем критерии в коллекцию, где мы можем зацикливаться.

extension BlogPost {
    static var examples2: [BlogPost] = [
        BlogPost(title: "Zoo", pageView: 5, sessionDuration: 2),
        BlogPost(title: "Alice", pageView: 1, sessionDuration: 3),
        BlogPost(title: "Peter", pageView: 1, sessionDuration: 2),
        BlogPost(title: "Kofi", pageView: 1, sessionDuration: 1),
        BlogPost(title: "Akosua", pageView: 5, sessionDuration: 2),
        BlogPost(title: "Abena", pageView: 4, sessionDuration: 10),
        BlogPost(title: "Angero", pageView: 1, sessionDuration: 2)
    ]
}

typealias AreInIncreasingOrder = (BlogPost, BlogPost) -> Bool // <1>
    
let popularPosts = BlogPost.examples2.sorted { (lhs, rhs) in    
    let predicates: [AreInIncreasingOrder] = [ // <2>
        { $0.pageView > $1.pageView },
        { $0.sessionDuration > $1.sessionDuration},
        { $0.title < $1.title }
    ]
    
    for predicate in predicates { // <3>
        if !predicate(lhs, rhs) && !predicate(rhs, lhs) { // <4>
            continue // <5>
        }
        
        return predicate(lhs, rhs) // <5>
    }
    
    return false
}

<1> Я объявляю псевдоним AreInIncreasingOrder, который соответствует закрытию сортировки. Это улучшает читаемость, когда мы объявляем нашу коллекцию предикатов.

<2> Мы объявляем набор предикатов.

<3> Мы перебираем предикаты.

<4> Вот сложная часть, мы хотим проверить, могут ли критерии определять порядок сообщений в блоге или нет. Но AreInIncreasingOrder возвращает логическое значение. Как проверить, совпадает ли порядок? Прежде чем ответить на этот вопрос, рассмотрим определение AreInIncreasingOrder.

AreInIncreasingOrder — это предикат, который возвращает значение true, если его первый аргумент должен располагаться перед вторым аргументом; в противном случае ложно. Таким образом, два аргумента находятся в порядке равенства, только если оба аргумента не в порядке возрастания.

Это означает, что наш предикат должен быть ложным независимо от порядка аргументов. Другими словами, lhs.pageView < rhs.pageView и rhs.pageView < lhs.pageView должны быть равны false, чтобы считаться равным порядку. Именно это и означают наши !predicate(lhs, rhs) && !predicate(rhs, lhs).

<5> Если порядок равен, мы переходим к следующему предикату.

<6> Если порядок не равен, мы можем использовать этот предикат для определения порядка.

Полученные результаты:

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

Вывод

Недавно я просто наткнулся на этот вопрос и нашел его интересным. Это простая задача, которая занимает у меня некоторое время, чтобы понять.

Методы в этой статье не привязаны к Swift. Вы можете применить это к любому языку по вашему выбору. Вы можете улучшить код, чтобы сделать его более универсальным, поддерживающим любые объекты или свойства, которые вы хотите, и я оставляю это вашим упражнением. Если вы придумаете что-нибудь интересное, вы можете поделиться своим результатом со мной в Twitter. Хотелось бы увидеть вашу реализацию.