Всем привет! Сегодня расскажу о том, как решить довольно интересную задачу, которая возникла у меня однажды при работе с 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:
И вот такой результат:
Добавим Comparer в функцию OrderBy:
Теперь результат тот, что нам нужен, и ведущие нули игнорируются:
Заключение
В общем, это не самая сложная задача — отсортировать строки (да и любые объекты) по любым заданным правилам. Так что не изобретайте велосипеды, не просите добавлять 0 и 00 в начало строк, используйте IComparer.
И не забывайте подписываться на мой телеграм-канал о Revit API. До новых встреч!