fix(search): order numbered headwords by numeric value
The final ranking tie-break compared headwords lexicographically, so entries numbered with digits or CJK numerals sorted by text instead of by number. Compare runs of digits or numeral characters by their numeric value, saturating on very long runs.
This commit is contained in:
1 parent
ad3d4f83fb
commit
f3ecc3086c
2 files changed
+135
-1
No files matched your search
@@ -0,0 +1,128 @@
|
|||||||
|
package com.example.research.core.util
|
||||||
|
|
||||||
|
val NaturalStringComparator: Comparator<String> = 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)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.research.data.parser
|
package com.example.research.data.parser
|
||||||
|
|
||||||
import com.example.research.core.domain.model.IndexEntry
|
import com.example.research.core.domain.model.IndexEntry
|
||||||
|
import com.example.research.core.util.NaturalStringComparator
|
||||||
import com.example.research.core.util.foldLatinDiacritics
|
import com.example.research.core.util.foldLatinDiacritics
|
||||||
|
|
||||||
internal data class SearchRankingContext(
|
internal data class SearchRankingContext(
|
||||||
@@ -40,6 +41,11 @@ internal fun List<IndexEntry>.rankBySearchRelevance(
|
|||||||
entry = entry,
|
entry = entry,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.sortedWith(compareBy({ it.matchTier }, { it.exactMatch }, { it.charCount }, { it.word }))
|
.sortedWith(
|
||||||
|
compareBy<SortKey> { it.matchTier }
|
||||||
|
.thenBy { it.exactMatch }
|
||||||
|
.thenBy { it.charCount }
|
||||||
|
.thenBy(NaturalStringComparator) { it.word }
|
||||||
|
)
|
||||||
.map { it.entry }
|
.map { it.entry }
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user