С++ 11 сортировка коллекции пользовательских объектов по нескольким свойствам

Существует набор настраиваемых элементов структуры:

struct MyStruct
{
    int id;
    std::string currencyCode;
    int month;
    int year;
    int amount;
};

Эти данные будут отображаться в некоторой таблице, которая позволяет сортировать по нескольким столбцам (нажав на столбцы таблицы, удерживая клавишу Ctrl).

Сортировка коллекции объектов клиента по одному свойству выполняется так же, как:

vector<MyStruct> values;

std::sort( values.begin( ), values.end( ), [ ]( const MyStruct& lhs, const MyStruct& rhs )
{
   return lhs.key < rhs.key;
});

или

struct MyStruct_x_Greater
{
    bool operator()( const MyStruct& lMyStruct, const MyStruct& rMyStruct ) const {
        return lMyStruct.x < rMyStruct.x;
    }
};

std::sort( values.begin(), values.end(), MyStruct_x_Greater() );

Но как сделать сортировку по нескольким свойствам один за другим (что-то вроде sql ORDER BY column1 DESC, column2 ASC)?

Ответ 1

Просто измените компаратор. Предположим, вы хотите заказать год, затем месяц, затем по количеству, затем:

std::sort( values.begin(), values.end(), [ ]( const MyStruct& lhs, const MyStruct& rhs )
{
   return std::tie(lhs.year, lhs.month, lhs.amount) <
          std::tie(rhs.year, rhs.month, rhs.amount);
});

std::tuple operator< делает лексикографическое сравнение, поэтому нет необходимости сворачивать свои собственные (и рисковать ошибиться). Чтобы выполнить нисходящий порядок для атрибута, просто замените lhs и rhs на этот атрибут.

Ответ 2

Ваша потребность в выборе порядка сортировки во время выполнения отличается от большинства подобных вопросов (и нескольких полученных вами ответов на коленный рефлекс). Я предлагаю вам дать каждому полю номер id и добавить функцию для сравнения поля, указанного id:

template <typename T>
int cmp(const T& lhs, const T& rhs)
{
    return lhs < rhs ? -1 : lhs == rhs ? 0 : 1;
}

struct MyStruct
{
    int id;   // 0
    std::string currencyCode;  // 1
    int month;  // 2
    int year;  // 3
    int amount;  // 4

    int cmp(int field_id, const MyStruct& rhs) const
    {
        switch (field_id)
        {
          case 0: return cmp(id, rhs.id);
          case 1: return cmp(currencyCode, rhs.currencyCode);
          case 2: return cmp(month, rhs.month);
          case 3: return cmp(year, rhs.year);
          case 4: return cmp(amount, rhs.amount);
          default: throw ...cases out of sync with code...;
        }
    }
};

// update this as your column heading are clicked...
std::vector<int> sort_field_ids = ...;

std::sort(begin(values), end(values),
    [&](const MyStruct& lhs, const MyStruct& rhs)
    {
        for (auto fid : sort_field_ids)
        {
            int cmp = lhs.cmp(fid, rhs);
            if (cmp) return cmp == -1;
        }
        // fall back on something stable and unique
        return lhs.id < rhs.id;
    });

Чтобы поддерживать нисходящие сортировки, поддерживайте дополнительный флаг (например, std::vector<std::pair<int, bool>> sort_field_ids), затем return (cmp == -1) ^ descending; (где fid,descending извлекается из вашего pair или ссылается на них как .first/.second, если вы предпочитаете).

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

Ответ 3

Решение этого путем присвоения идентификаторов полям имеет несколько недостатков:

  • Случаи не могут быть покрыты (как указано throw)
  • Для сортировки в обратном порядке требуется дополнительный флаг
  • Это не очевидно из кода, идентификатор которого обозначает, какое поле
  • Это не легко расширяемое (вам всегда нужно обновлять сопоставления идентификаторов)

В качестве альтернативы вы можете определить "компараторы", которые возвращают -1, 0 или 1, в зависимости от того, меньше ли первый аргумент, равный или больший, чем второй. Затем эти компараторы могут быть объединены и полностью отформатированы и использованы как предикат сортировки.

Учитывая набор этих компараторов для полей вашей структуры, вы можете составить их следующим образом:

Comparator byYearReverseAndMonth = compose(reverse(byYear), byMonth);
std::sort(values.begin(), values.end(), with(byYearReverseAndMonth));

Изменить в ответ на комментарий:

Конечно, для определения каждой комбинации компараторов и обратных компараторов во время компиляции требуется не. Вместо этого можно собрать желаемые компараторы для следующего сортировки во время выполнения, например, в vector<Comparator>. Например, экземпляры компаратора могут быть связаны с столбцами таблицы:

vector<Comparator> comparators;
for (each selected column) {
    comparators.push_pack(comparatorFor(column));
}

Затем эти компараторы могут быть составлены в один, с простым циклом по всем компараторам:

Comparator composedComparator = comparators[0];
for (int i=1; i<comparators.size(); ++i) {
    composedComparator = compose(comparator, comparators[i]);
}

sort(v.begin(),v.end(),with(composedComparator));

Набросок того, как это может выглядеть:

#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>

struct MyStruct
{
    int id;
    std::string currencyCode;
    int month;
    int year;
    int amount;
};

typedef std::function<int(const MyStruct& s0, const MyStruct& s1)> Comparator;
typedef std::function<bool(const MyStruct& s0, const MyStruct& s1)> Predicate;

template <typename T>
std::function<int(const T&, const T&)> compose(
    std::function<int(const T&, const T&)> c0,
    std::function<int(const T&, const T&)> c1)
{
    return[c0, c1](const T& t0, const T& t1) -> int
    {
        int r0 = c0(t0, t1);
        if (r0 != 0)
        {
            return r0;
        }
        return c1(t0, t1);
    };
}

template <typename T>
std::function<int(const T&, const T&)> reverse(
    std::function<int(const T&, const T&)> c)
{
    return[c](const T& t0, const T& t1) -> int
    {
        return -c(t0, t1);
    };
}

template <typename T>
std::function<bool(const T&, const T&)> with(
    std::function<int(const T&, const T&)> comparator)
{
    return[comparator](const T& t0, const T& t1) 
    {
        return comparator(t0, t1) < 0;
    };
}


void print(std::vector<MyStruct>& values)
{
    for (auto it = values.begin(); it != values.end(); ++it)
    {
        std::cout << (*it).month << "-"
            << (*it).year << " id "
            << (*it).id << std::endl;
    }
}

int main(int argc, char** argv)
{
    std::vector<MyStruct> values;

    MyStruct m;
    m.year = 1981; m.month = 1; m.id = 4;  values.push_back(m);
    m.year = 1980; m.month = 2; m.id = 5;  values.push_back(m);
    m.year = 1980; m.month = 4; m.id = 2;  values.push_back(m);
    m.year = 1980; m.month = 3; m.id = 3;  values.push_back(m);
    m.year = 1980; m.month = 4; m.id = 1;  values.push_back(m);

    std::cout << "Before sorting" << std::endl;
    print(values);

    Comparator byMonth = [](const MyStruct& s0, const MyStruct& s1)
    {
        if (s0.month < s1.month) return -1;
        if (s0.month > s1.month) return  1;
        return 0;
    };
    Comparator byYear = [](const MyStruct& s0, const MyStruct& s1)
    {
        if (s0.year < s1.year) return -1;
        if (s0.year > s1.year) return  1;
        return 0;
    };
    Comparator byId = [](const MyStruct& s0, const MyStruct& s1)
    {
        if (s0.id < s1.id) return -1;
        if (s0.id > s1.id) return  1;
        return 0;
    };

    Comparator byYearAndMonth = compose(byYear, byMonth);
    std::sort(values.begin(), values.end(), with(byYearAndMonth));

    std::cout << "After sorting by year and month:" << std::endl;
    print(values);


    Comparator byYearReverseAndMonth = compose(reverse(byYear), byMonth);
    std::sort(values.begin(), values.end(), with(byYearReverseAndMonth));

    std::cout << "After sorting by year reverse and month:" << std::endl;
    print(values);


    Comparator byYearAndMonthAndId = compose(byYearAndMonth, byId);
    std::sort(values.begin(), values.end(), with(byYearAndMonthAndId));

    std::cout << "After sorting by year and month and id:" << std::endl;
    print(values);

    return 0;
}

(Извинения за потенциальный faux pas, я новичок С++)

Ответ 4

Используйте следующую клавишу, только если значения текущего ключа равны:

[ ]( const MyStruct& lhs, const MyStruct& rhs )
{
    if(lhs.key1 != rhs.key1) 
        return lhs.key1 < rhs.key1;

    if(lhs.key2 != rhs.key2) 
        return lhs.key2 < rhs.key2;

    ...    

    return lhs.keyN < rhs.keyN;
}

В этом примере записи будут упорядочены по нарастанию key1, чем, key2 и т.д. до keyN.

Ответ 5

Заказ нескольких атрибутов означает определение порядка важности. Например, упорядочение по годам, месяцам: год более важно, чем месяц:

vector<MyStruct> values;
std::sort( values.begin( ), values.end( ), [ ]( const MyStruct& lhs, const MyStruct& rhs )
{
   return lhs.year < rhs.year || ( lhs.year == rhs.year && lhs.month < rhs.month );
});

Ответ 6

Немного громоздкий способ выполнить то, что вы ищете, сделать это несколькими шагами (это тоже будет работать на С++ 03)

struct ColumnOneDESC{...};
struct ColumnTwoASC{...};
...
std::stable_sort( values.begin(), values.end(), ColumnTwoASC());
std::stable_sort( values.begin(), values.end(), ColumnOneDESC());
...

И в курсе вы можете сделать его более общим с помощью std:: not или такого