Сортировка массивов русских символов и строк с участием буквы Ё

463
14 августа 2017, 08:24

Простой пример кода, попробуем отсортировать массив русских символов:

var a = new char[]
{
    'д',
    'е',
    'ё',
    'ж'
};
var b = a.OrderBy(x => x).ToList();
Console.WriteLine(string.Concat(b));

Выдаст этот простой код неожиданное на первый взгляд дежё, но тут c# как раз верен стандартам, потому что код буквы ё больше, чем коды всех остальных букв русского алфавита.

Попробуем отсортировать массив строк, одна из которых содержит букву ё:

var a = new string[]
{
    "жар",
    "дом",
    "ели",
    "ёлка",
};
var b = a.OrderBy(x => x).ToList();
Console.WriteLine(string.Join(" ", b));

Получаем ожидаемое дом ели ёлка жар. Вроде бы строки сортируются ожидаемым образом.

Попробуем ели заменить на ель. Получим прекрасное дом ёлка ель жар. Очевидно, при сортировке строк е и ё считаются одним символом, во втором случае ёлка становится перед ель потому что к идёт раньше ь.

Моё наивное понимание предполагаемого алгоритма сортировки массива строк подсказывает, что он должен использовать тот же алгоритм сравнения кодов символов, что и сортировка массива символов. Этого, очевидно, не происходит. Ожидаемой модификацией алгоритма был бы учёт того, что ё в русском алфавите находится всё-таки не там, где она находится в unicode. А на самом деле имеем реализацию, где е и ё - это один символ.

Меня интересует несколько вопросов. Где конкретно определён алгоритм сортировки строк? Мои путешествия по ReferenceSource увели меня куда-то в цппшные недра GitHub'а CLR, не уверен, что двигался верно. Почему было принято решение принимать е и ё за один символ, а не осуществлять честную сортировку? Это чьё-то волевое решение или это всё-таки определено в какой-то из спецификаций?

Понимаю, что не все вопросы подразумевают наличие чёткого ответа у обычных участников сообщества, но ссылаюсь сюда.

А может быть, что я вообще всё интерпретировал неверно, поправьте в таком случае.

Спасибо.

Answer 1

Здесь причиной вашего удивления является не странность алгоритмов BCL, а имплементация стандарта Unicode.

Документация стандарта Unicode Unicode® Technical Standard #10 / Unicode Collation Algorithm гласит (перевод мой):

1.1 Многоуровневое сравнение

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

При сравнении двух слов, самым важным являются базовые буквы, например, отличие между И и Е. Акценты обычно игнорируются, если базовые буквы не совпадают. Различие в регистре (прописные/строчные) также обычно игнорируются, если базовые буквы или их акценты различны.

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

В случае равенства иногда проводится финальное сравнение: если других различий нет, сравниваются (нормализованные) code point'ы.

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

  1. Отбрасываются различия регистра и акценты. Если сравнение установило, какое из слов больше, конец алгоритма.
  2. Возвращаются назад акценты, проводится повторное сравнение. Если сравнение установило, какое из слов больше, конец алгоритма.
  3. Возвращается назад регистр, проводится повторное сравнение. Если сравнение установило, какое из слов больше, конец алгоритма.

и. т. д.

Для русского языка по «принципу кроссворда» Ё считается акцентированным вариантом Е, а Й — акцентированным вариантом И.

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

Как это поменять в .NET — как заставить считать Ё отдельной буквой, расположенной между Е и Ж, я сходу не скажу. (Но смотрите соседний ответ.)

Кстати, имплементация того или иного стандарта Unicode — фича не языка, а системы. BCL просит систему сравнить строки, чтобы не дублировать имплементацию Unicode. Это значит, что одна и та же программа, будучи проинсталлированной на Windows 7 и Windows 10, может вести себя по-разному по отношению к сортировке.

Уточнение

Для русской локали Й считается отдельной от И буквой, в то время как для английской Й считается акцентированным вариантом И. Буква Ё и там, и там считается акцентированным вариантом Е. Пример:

var strings = new[] { "Иа", "Йокогама", "Италия", "Ель", "Ёлочка" };
var en = CultureInfo.GetCultureInfo("en-US");
var ru = CultureInfo.GetCultureInfo("ru-RU");
var orderEn = strings.OrderBy(s => s, StringComparer.Create(en, false));
Console.WriteLine("en-US: " + string.Join(" ", orderEn));
var orderRu = strings.OrderBy(s => s, StringComparer.Create(ru, false));
Console.WriteLine("ru-RU: " + string.Join(" ", orderRu));

выдаёт такой результат:

en-US: Ёлочка Ель Иа Йокогама Италия
ru-RU: Ёлочка Ель Иа Италия Йокогама

Мораль этой истории: при сортировке строк всегда указывайте локаль!

Answer 2

В дополнение к ответу @VladD:

Как это поменять в .NET — как заставить считать Ё отдельной буквой, расположенной между Е и Ж, я навскидку не знаю.

Как вариант, можно реализовать свой IComparer, например так:

class MyStringComparer : IComparer<string>
{
    int IComparer<string>.Compare(string x, string y)
    {
        int result = CompareAlgorithm(x.ToLowerInvariant(), y.ToLowerInvariant());
        if (result != 0) return result;
        return CompareAlgorithm(x, y);
    }
    static readonly string symbols = " абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ.,:";
    int CompareAlgorithm(string x, string y)
    {
        int i = 0, j = 0;
        while(x.Length > i && y.Length > j)
        {
            int indexX = symbols.IndexOf(x[i]);
            int indexY = symbols.IndexOf(y[i]);
            if (indexX == -1)
            {
                ++i;
                continue;
            }
            if (indexY == -1)
            {
                ++j;
                continue;
            }
            if (indexX < indexY) return -1;
            if (indexX > indexY) return 1;
            ++i;
            ++j;
        }
        if (x.Length <= i && y.Length <= j) return 0;
        if (x.Length <= i) return -1;
        else return 1;
    }
}

Сравнение строк выполняется посимвольно, символы, участвующие при сравнении указываются в symbols, в желаемом порядке возрастания, символы, отсутствующие в symbols игнорируются.

В самом методе Compare сначала сравниваем без учета регистра и если выдается одинаковый результат, сравниваем с учетом регистра. Это поведение можно изменить. Если хотите всегда сравнивать с учетом регистра, оставьте метод в таком виде:

int IComparer<string>.Compare(string x, string y)
{
    return CompareAlgorithm(x, y);
}

Следующий код:

var a = new string[]
{
    "жар",
    "дом",
    "ель",
    "ёлка",
    "домовой",
    "д.ом",
    "д-ом",
    "ДОм"
};
var b = a.OrderBy(x => x, new MyStringComparer()).ToList();
Console.WriteLine(string.Join(" ", b));

Выдает такой результат:

д-ом дом ДОм домовой д.ом ель ёлка жар
Answer 3

Сортировка по умолчанию в Windows в целом и в .NET в частности всегда вызывает удивление.

Я задавал похожий вопрос на en.so.

Цитата из документации (чуть подправленная мной для читаемости):

Платформа .NET Framework использует три разных способа сортировки: по словам, по строкам и по порядковому номеру.

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

READ ALSO
Unity3d + Google Play Games Проблемы авторизации

Unity3d + Google Play Games Проблемы авторизации

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

458
Отправка почты на яндекс

Отправка почты на яндекс

Добрый день, возможно ли с помощью класса SmtpClient отправить почту на яндекс? Перепробовал уже кучу разных вариантов, никак не могу понять в чем...

489
Задать SelectedIndex в ComboBox при привязке данных

Задать SelectedIndex в ComboBox при привязке данных

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

203