Проблема с алфавитной сортировкой строк в Android и буквой Ё

Задача: есть список имён - нужно разбить на группы по первой букве: А, Б, В, Г, Д, Е, Ё, Ж, ...

Я попробовал String.compareTo и java.text.Collator но не нашел работающего варианта (чтобы сортировал строго по порядку букв в русском алфавите)


String.compareTo - сравнивает не по порядку букв в алфавите (несмотря на то что в описании указан lexicographic ordering)

val list = listOf("аа", "яя", "жж", "еж", "ёж", "ежовый")
val sorted = list.sortedWith(Comparator { a, b -> a.compareTo(b, ignoreCase = true) })
println(sorted.toString())

result: [аа, еж, ежовый, жж, яя, ёж]


java.text.Collator - не нашел варианта при котором Ё не смешивалась бы с Е

val list = listOf("аа", "яя", "жж", "еж", "ёж", "ежовый")
val collator = java.text.Collator.getInstance().apply { 
    decomposition = Collator.NO_DECOMPOSITION
    strength = Collator.TERTIARY
}
val sorted = list.sortedWith(Comparator { a, b -> collator.compare(a, b) })
println(sorted.toString())

result: [аа, еж, ёж, ежовый, жж, яя]


Правильно ли я понимаю что в Android нет сортировки по алфавиту и нужно сочинять свою ?


Ответы (3 шт):

Автор решения: Alex_Skvortsov

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

val l = listOf("аа", "яя", "жж", "еж", "ёж", "ежовый")
val sorted = l.sortedWith(Comparator { a, b -> a.compareTo(b, ignoreCase = true) }) //сортируем стандартно
val indexE = sorted.indexOfFirst { it.toLowerCase().firstOrNull() == 'ё' } 
val result = sorted.subList(0, indexE).toMutableList() //получаем отсортированный список слов, начинающихся НЕ с буквы Ё
val eList = sorted.subList(indexE, sorted.size) //получаем отсортированный список слов, начинающихся с Ё
val insertPos = sorted.indexOfLast { it.toLowerCase().firstOrNull() == 'е' }
result.addAll(insertPos + 1, eList) //вставляем слова, начинающиеся с Ё за словами, начинающимися с Е
println(result.toString())

Результат вроде правильный, но, понятно, хреново выглядит))

Сортировка будет корректной только для первых букв, вторые и прочие - ё будет считаться самой последней буквой алфавита.

→ Ссылка
Автор решения: savin

Пока не придумал ничего лучше чем изменить String.compareToIgnoreCase таким образом:

val CASE_INSENSITIVE_ORDER_RU = kotlin.Comparator { s1: String, s2: String ->
    val n1 = s1.length
    val n2 = s2.length
    val min = Math.min(n1, n2)
    for (i in 0 until min) {
        var c1 = s1[i]
        var c2 = s2[i]
        if (c1 != c2) {
            c1 = Character.toUpperCase(c1)
            c2 = Character.toUpperCase(c2)
            if (c1 != c2) {
                c1 = Character.toLowerCase(c1)
                c2 = Character.toLowerCase(c2)
                if (c1 != c2) {
                    // No overflow because of numeric promotion
                    return@Comparator when {
                        c1 == 'ё' ->
                            if (c2 == 'е') 1 else 'е' - c2

                        c2 == 'ё' ->
                            if (c1 == 'е') -1 else c1 - 'е'

                        else ->
                            c1 - c2
                    }
                }
            }
        }
    }
    return@Comparator n1 - n2
}
val list = listOf("аа", "яя", "жж", "еж", "ёж", "ежовый", 
                  "Аа", "Яя", "Жж", "ЕЖ", "Ёж", "Ежовый")
val sorted = list.sortedWith(CASE_INSENSITIVE_ORDER_RU)

result: [аа, Аа, еж, ЕЖ, ежовый, Ежовый, ёж, Ёж, жж, Жж, яя, Яя]


Update:

решение от @barmaley-red-star на основе java.text.RuleBasedCollator

val list = listOf("0", "9", "1", 
                  "аа", "яя", "жж", "еж", "ёж", "ежовый",
                  "Аа", "Яя", "Жж", "Еж", "Ёж", "Ежовый", 
                  "aa", "bb", "cc")
val yoRule = "& а < б < в < г < д < е < ё < ж < з < и < й "+
             "< к < л < м < н < о < п < р < с < т < у < ф "+
             "< х < ц < ч < ш < щ < ъ < ы < ь < э < ю < я"
val ruleBasedCollator = java.text.RuleBasedCollator(yoRule)
val locale = Locale("ru")
val sorted = list.sortedWith(Comparator { a, b -> 
    ruleBasedCollator.compare(a.toLowerCase(locale), b.toLowerCase(locale)) 
})
println("RuleBasedCollator -> $sorted")

RuleBasedCollator -> [0, 1, 9, aa, bb, cc, аа, Аа, еж, Еж, ежовый, Ежовый, ёж, Ёж, жж, Жж, яя, Яя]

→ Ссылка
Автор решения: Barmaley Red Star

Коллеги,

Не там по-моему ищете:

1) Android здесь не причем, корень в Java конечно же

2) Надо все же таки явно указывать на локаль, иначе будет взята локаль по умолчанию, которая может быть какой угодно:

Locale   locale = Locale.RU;
Collator collator = Collator.getInstance(locale);

3) Чтобы различать е и ё надо задавать флаг коллатора как SECONDARY согласно мануалу:

Collator strength value. When set, only SECONDARY and above differences are considered significant during comparison. The assignment of strengths to language features is locale dependant. A common example is for different accented forms of the same base letter ("a" vs "ä") to be considered a SECONDARY difference.

И только потом уже запускать компарацию.

3) Если уж совсем неймётся, то можно применить RuleBasedCollator для ё, и обойтись без велосипедов:

String yoRule = "< е < ё";
RuleBasedCollator ruleBasedCollator =
    new RuleBasedCollator(yoRule);

И напоследок, не надо думать, что наш русский язык какой-то супер особенный, есть языки и позаковыристее, в комитете Unicode отнюдь не дураки сидят и давно уже разработали корректный алгоритм сравнения, на основе которой и работает Collation в Java

Update

val list = listOf("аа", "яя", "жж", "еж", "ежп", "ёж", "ёжя", "ежовый")
val yoRule = "< а < б < в < г < д < е < ё < ж < з < и < й < к < л < м < н < о < п < р < с < т < у < ф < х < ц < ч < ш < щ < ъ < ы < ь < я < ю < я"
val ruleBasedCollator = RuleBasedCollator(yoRule)
val sorted = list.sortedWith(Comparator { a, b -> ruleBasedCollator.compare(a, b) })
println(sorted.toString())

[аа, еж, ежовый, ежп, ёж, ёжя, жж, яя]

Так работает?

→ Ссылка