Pretty fuzzy search, close #122

This commit is contained in:
Him188 2020-08-29 15:36:31 +08:00
parent 234eeb7540
commit f38062e0ec
2 changed files with 77 additions and 10 deletions

View File

@ -12,10 +12,7 @@ package net.mamoe.mirai.console.command.description
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.* import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.internal.command.fuzzySearchMember import net.mamoe.mirai.console.internal.command.fuzzySearchMember
import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.User
import net.mamoe.mirai.getFriendOrNull import net.mamoe.mirai.getFriendOrNull
import net.mamoe.mirai.getGroupOrNull import net.mamoe.mirai.getGroupOrNull
import net.mamoe.mirai.message.data.At import net.mamoe.mirai.message.data.At
@ -288,9 +285,25 @@ internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArg
fun Group.findMemberOrFail(idOrCard: String): Member { fun Group.findMemberOrFail(idOrCard: String): Member {
if (idOrCard == "\$") return members.randomOrNull() ?: illegalArgument("当前语境下无法推断随机群员") if (idOrCard == "\$") return members.randomOrNull() ?: illegalArgument("当前语境下无法推断随机群员")
return idOrCard.toLongOrNull()?.let { getOrNull(it) } idOrCard.toLongOrNull()?.let { getOrNull(it) }?.let { return it }
?: fuzzySearchMember(idOrCard) this.members.singleOrNull { it.nameCardOrNick.contains(idOrCard) }?.let { return it }
?: illegalArgument("无法找到目标群员 $idOrCard") this.members.singleOrNull { it.nameCardOrNick.contains(idOrCard, ignoreCase = true) }?.let { return it }
val candidates = this.fuzzySearchMember(idOrCard)
candidates.singleOrNull()?.let {
if (it.second == 1.0) return it.first // single match
}
if (candidates.isEmpty()) {
illegalArgument("无法找到成员 $idOrCard")
} else {
var index = 1
illegalArgument("无法找到成员 $idOrCard。 多个成员满足搜索结果或匹配度不足: \n\n" +
candidates.joinToString("\n", limit = 6) {
val percentage = (it.second * 100).toDecimalPlace(0)
"#${index++}(${percentage}%)${it.first.nameCardOrNick.truncate(10)}(${it.first.id})" // #1 15.4%
}
)
}
} }
fun CommandSender.inferBotOrFail(): Bot = fun CommandSender.inferBotOrFail(): Bot =
@ -306,3 +319,28 @@ internal interface InternalCommandArgumentParserExtensions<T : Any> : CommandArg
fun CommandSender.inferFriendOrFail(): Friend = fun CommandSender.inferFriendOrFail(): Friend =
(this as? FriendCommandSender)?.user ?: illegalArgument("当前语境下无法推断目标好友") (this as? FriendCommandSender)?.user ?: illegalArgument("当前语境下无法推断目标好友")
} }
internal fun Double.toDecimalPlace(n: Int): String {
return "%.${n}f".format(this)
}
internal fun String.truncate(lengthLimit: Int, replacement: String = "..."): String = buildString {
var lengthSum = 0
for (char in this@truncate) {
lengthSum += char.chineseLength()
if (lengthSum > lengthLimit) {
append(replacement)
return toString()
} else append(char)
}
return toString()
}
internal fun Char.chineseLength(): Int {
return when (this) {
in '\u0000'..'\u007F' -> 1
in '\u0080'..'\u07FF' -> 2
in '\u0800'..'\uFFFF' -> 3
else -> 4
}
}

View File

@ -13,7 +13,6 @@ import net.mamoe.mirai.console.command.*
import net.mamoe.mirai.console.command.description.CommandArgumentParserException import net.mamoe.mirai.console.command.description.CommandArgumentParserException
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.nameCardOrNick
import kotlin.math.max import kotlin.math.max
@ -97,8 +96,38 @@ internal inline fun <T : Any> Collection<T>.fuzzySearchOnly(
} }
internal fun Group.fuzzySearchMember(nameCardTarget: String): Member? { /**
return this.members.fuzzySearch(nameCardTarget) { it.nameCardOrNick } * @return candidates
*/
internal fun Group.fuzzySearchMember(
nameCardTarget: String,
minRate: Double = 0.5, // 参与判断, 用于提示可能的解
matchRate: Double = 0.6,// 最终选择的最少需要的匹配率, 减少歧义
/**
* 如果有多个值超过 [matchRate], 并相互差距小于等于 [disambiguationRate], 则认为有较大歧义风险, 返回可能的解的列表.
*/
disambiguationRate: Double = 0.1,
): List<Pair<Member, Double>> {
val candidates = (this.members + botAsMember)
.associateWith { it.nameCard.fuzzyMatchWith(nameCardTarget) }
.filter { it.value >= minRate }
.toList()
.sortedByDescending { it.second }
val bestMatches = candidates.filter { it.second >= matchRate }
return when {
bestMatches.isEmpty() -> candidates
bestMatches.size == 1 -> listOf(bestMatches.single().first to 1.0)
else -> {
if (bestMatches.first().second - bestMatches.last().second <= disambiguationRate) {
// resolution ambiguity
candidates
} else {
listOf(bestMatches.first().first to 1.0)
}
}
}
} }