Найти тему

Изменение правил сортировки элементов в C#

Оглавление

Всем привет! Сегодня расскажу о том, как решить довольно интересную задачу, которая возникла у меня однажды при работе с Revit. Впрочем, изложенные здесь вещи можно применить в любом C# приложении.

Задача

У нас есть Revit-документ с некоторым количеством листов. Мы хотим создать окно, где пользователю будут представлены эти листы, отсортированные по номеру листа. Мы пишем код, пишем окно, делаем тестовый файл с 5 листами, всё отлично работает.

Но потом к нам приходят пользователи и говорят "а почему листы отсортировались в порядке 1, 10, 11, 12 и т.д.". Мы осознаём, что свойство SheetNumber у листа — это строка (и правда, мы и в интерфейсе Ревита можем вписать в номер листа всё что угодно). Строки сортируются по правилам сортировки строк, посимвольно и, всё верно, порядок такой и будет: 1, 10, 11, ... 19, 2, 20 и так далее. Это равносильно тому, как если бы у нас были строки А, АА, АБ, АВ, ... АЖ, Б, БA. Они отсортировались по алфавиту посимвольно, просто при сортировке строк символы сравниваются не по тому, как в алфавите, а по тому, как закодирована таблица символов, и какой у каждого символа номер.

Ну ладно, мы пробили решение, что проектировщики будут нумеровать листы так: 01, 02, 03 и т.д. Всё работает, но тут появляется лист 100, а на номер 001 никто не согласен. И что же делать?

Решение

Решение очень простое. Мы всегда можем сказать нашему приложению, как именно мы хотим сравнивать объекты при сортировке. И поможет нам в этом интерфейс IComparer<T>.

Реализация IComparer<T>

Интерфейс устроен очень просто: нужно определить функцию Compare, которая принимает на вход 2 объекта T (в нашем случае это строки), и возвращает 1, если первый объект больше второго, -1 — если второй больше, и 0 — если объекты равны.

В нашем случае, мы можем просто превратить номера строк в числа, и сравнить эти числа. Но в целом, всё не так просто. Для данной ситуации подойдёт вот такой Comparer:

И в редактируемом формате:

public class IntStringComparer : IComparer<string>
{
public int Compare(string? x, string? y)
{
if (x == y) return 0;
if (x is null) return -1;
if (y is null) return 1;
x = x.TrimStart('0');
y = y.TrimStart('0');
if (int.TryParse(x, out int intX) && int.TryParse(y, out int intY))
{
return intX.CompareTo(intY);
}
if (int.TryParse(x, out _)) return 1;
if (int.TryParse(y, out _)) return -1;
return x.CompareTo(y);
}
}

Что тут происходит:

  • Сначала мы сравниваем, не являются ли строки одной и той же строкой. Да, листы не могут иметь одинаковые номера, но, в общем случае, может прилететь всё что угодно, в том числе null. Так что первой строкой мы обработает все случаи, когда обе строки равны null.
  • Отсюда вытекают вторая и третья строка, мы уже знаем, что обе строки вместе — не null. Так что если вторая null, то она автоматом меньше первой.
  • Затем удаляем у строк все ведущие нули, если они есть
  • Затем обрабатываем ситуацию, когда обе строки можно превратить в число, и сравниваем эти числа
  • Далее обрабатываем ситуацию: окей, мы знаем, что хотя бы одну из строк нельзя превратить в число. Тогда автоматом больше та, которая является числом.
  • И в конце обрабатываем ситуацию, когда обе строки одновременно не является числом. Сравниваем их как обычные строки.

Давайте проверим это на реальном проекте: выведем листы в сообщение при обычной сотрировке и при сортировке этим Comparer:

Такой вот список листов
Такой вот список листов

Вот такой код для вывода листов, пока без Comparer:

-3

И вот такой результат:

-4

Добавим Comparer в функцию OrderBy:

-5

Теперь результат тот, что нам нужен, и ведущие нули игнорируются:

-6

Заключение

В общем, это не самая сложная задача — отсортировать строки (да и любые объекты) по любым заданным правилам. Так что не изобретайте велосипеды, не просите добавлять 0 и 00 в начало строк, используйте IComparer.

И не забывайте подписываться на мой телеграм-канал о Revit API. До новых встреч!

-7