diff --git a/app/src/main/java/com/example/research/core/util/NaturalSort.kt b/app/src/main/java/com/example/research/core/util/NaturalSort.kt new file mode 100644 index 0000000..581ed6c --- /dev/null +++ b/app/src/main/java/com/example/research/core/util/NaturalSort.kt @@ -0,0 +1,128 @@ +package com.example.research.core.util + +val NaturalStringComparator: Comparator = Comparator { a, b -> compareNatural(a, b) } + +private const val NUMERAL_CAP = 1_000_000_000_000_000_000L + +private fun digitOf(c: Char): Int = when (c) { + '〇', '零' -> 0 + '一', '壹', '壱', '弌' -> 1 + '二', '貳', '贰', '弐', '弍', '兩', '两' -> 2 + '三', '叁', '參', '参', '弎' -> 3 + '四', '肆' -> 4 + '五', '伍' -> 5 + '六', '陸', '陆' -> 6 + '七', '柒' -> 7 + '八', '捌' -> 8 + '九', '玖' -> 9 + else -> Character.digit(c, 10) +} + +private fun smallUnitOf(c: Char): Long = when (c) { + '十', '拾' -> 10L + '百', '佰' -> 100L + '千', '仟' -> 1000L + else -> 0L +} + +private fun fixedTensOf(c: Char): Long = when (c) { + '廿' -> 20L + '卅' -> 30L + '卌' -> 40L + else -> 0L +} + +private fun bigUnitOf(c: Char): Long = when (c) { + '万', '萬' -> 10_000L + '億', '亿' -> 100_000_000L + '兆' -> 1_000_000_000_000L + '京' -> 10_000_000_000_000_000L + else -> 0L +} + +private fun isNumeralChar(c: Char): Boolean = + digitOf(c) >= 0 || smallUnitOf(c) > 0L || fixedTensOf(c) > 0L || bigUnitOf(c) > 0L + +private fun satMul(a: Long, b: Long): Long = + if (a != 0L && b > NUMERAL_CAP / a) NUMERAL_CAP else minOf(a * b, NUMERAL_CAP) + +private fun satAdd(a: Long, b: Long): Long = minOf(a + b, NUMERAL_CAP) + +private fun numeralValue(s: String, start: Int, end: Int): Long { + var total = 0L + var section = 0L + var pending = -1L + for (k in start until end) { + val c = s[k] + val digit = digitOf(c) + if (digit >= 0) { + pending = if (pending < 0) digit.toLong() else satAdd(satMul(pending, 10L), digit.toLong()) + continue + } + val small = smallUnitOf(c) + if (small > 0L) { + section = satAdd(section, satMul(if (pending < 0) 1L else pending, small)) + pending = -1 + continue + } + val fixed = fixedTensOf(c) + if (fixed > 0L) { + section = satAdd(section, fixed) + pending = -1 + continue + } + val base = satAdd(section, maxOf(pending, 0L)) + total = satAdd(total, satMul(if (base == 0L) 1L else base, bigUnitOf(c))) + section = 0L + pending = -1 + } + return satAdd(total, satAdd(section, maxOf(pending, 0L))) +} + +private fun compareNatural(a: String, b: String): Int { + var i = 0 + var j = 0 + while (i < a.length && j < b.length) { + val ca = a[i] + val cb = b[j] + val asciiA = ca in '0'..'9' + val asciiB = cb in '0'..'9' + when { + !asciiA && !asciiB && isNumeralChar(ca) && isNumeralChar(cb) -> { + var endA = i + while (endA < a.length && isNumeralChar(a[endA])) endA++ + var endB = j + while (endB < b.length && isNumeralChar(b[endB])) endB++ + val diff = numeralValue(a, i, endA).compareTo(numeralValue(b, j, endB)) + if (diff != 0) return diff + i = endA + j = endB + } + asciiA && asciiB -> { + var endA = i + while (endA < a.length && a[endA] in '0'..'9') endA++ + var endB = j + while (endB < b.length && b[endB] in '0'..'9') endB++ + var startA = i + while (startA < endA - 1 && a[startA] == '0') startA++ + var startB = j + while (startB < endB - 1 && b[startB] == '0') startB++ + val lengthA = endA - startA + val lengthB = endB - startB + if (lengthA != lengthB) return lengthA.compareTo(lengthB) + for (k in 0 until lengthA) { + val diff = a[startA + k].compareTo(b[startB + k]) + if (diff != 0) return diff + } + i = endA + j = endB + } + else -> { + if (ca != cb) return ca.compareTo(cb) + i++ + j++ + } + } + } + return (a.length - i).compareTo(b.length - j) +} diff --git a/app/src/main/java/com/example/research/data/parser/SearchRanking.kt b/app/src/main/java/com/example/research/data/parser/SearchRanking.kt index 4185480..e3887ef 100644 --- a/app/src/main/java/com/example/research/data/parser/SearchRanking.kt +++ b/app/src/main/java/com/example/research/data/parser/SearchRanking.kt @@ -1,6 +1,7 @@ package com.example.research.data.parser import com.example.research.core.domain.model.IndexEntry +import com.example.research.core.util.NaturalStringComparator import com.example.research.core.util.foldLatinDiacritics internal data class SearchRankingContext( @@ -40,6 +41,11 @@ internal fun List.rankBySearchRelevance( entry = entry, ) } - .sortedWith(compareBy({ it.matchTier }, { it.exactMatch }, { it.charCount }, { it.word })) + .sortedWith( + compareBy { it.matchTier } + .thenBy { it.exactMatch } + .thenBy { it.charCount } + .thenBy(NaturalStringComparator) { it.word } + ) .map { it.entry } }