検索結果を辿る際に二分探索する

This commit is contained in:
tateisu 2023-05-09 22:21:26 +09:00
parent b10f77a71f
commit e3c19b5361
6 changed files with 140 additions and 75 deletions

View File

@ -1,7 +1,6 @@
plugins { plugins {
id "com.android.library" id "com.android.library"
id "org.jetbrains.kotlin.android" id "org.jetbrains.kotlin.android"
// id "org.jetbrains.kotlin.kapt"
} }
android { android {

View File

@ -163,11 +163,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
} }
} }
//kapt {
// useBuildCache = true
//}
dependencies { dependencies {
// desugar_jdk_libs 2.0.0 AGP 7.4.0-alpha10 // desugar_jdk_libs 2.0.0 AGP 7.4.0-alpha10
//noinspection GradleDependency //noinspection GradleDependency

View File

@ -28,8 +28,6 @@
-keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule -keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep class com.bumptech.glide.GeneratedAppGlideModuleImpl
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES; **[] $VALUES;
public *; public *;

View File

@ -72,6 +72,71 @@ class ActText : AppCompatActivity() {
private class SearchResultSpan(color: Int) : BackgroundColorSpan(color) private class SearchResultSpan(color: Int) : BackgroundColorSpan(color)
private class SearchResult(
val items: List<IntRange> = emptyList(),
val hasMore: Boolean = false,
val error: String? = null,
) {
val size = items.size
fun findNext(curPos: Int, allowEqual: Boolean = false): IntRange? {
var start = 0
var end = items.size
while (end > start) {
val mid = (end + start) shr 1
val item = items[mid]
if (curPos in item) return when {
allowEqual -> item
else -> items.elementAtOrNull(mid + 1)
}
items.elementAtOrNull(mid - 1)?.let { prev ->
if (curPos in prev.last + 1 until item.first) return item
}
when {
curPos > item.first -> start = mid + 1
else -> end = mid
}
}
return null
}
fun findPrev(curPos: Int, allowEqual: Boolean = false): IntRange? {
var start = 0
var end = items.size
while (end > start) {
val mid = (end + start) shr 1
val item = items[mid]
if (curPos in item) return when {
allowEqual -> item
else -> items.elementAtOrNull(mid - 1)
}
items.elementAtOrNull(mid + 1)?.let { next ->
if (curPos in item.last + 1 until next.first) return item
}
when {
curPos > item.first -> start = mid + 1
else -> end = mid
}
}
return null
}
fun index(curPos: Int): Int? {
var start = 0
var end = items.size
while (end > start) {
val mid = (end + start) shr 1
val item = items[mid]
when {
curPos in item -> return mid
curPos > item.first -> start = mid + 1
else -> end = mid
}
}
return null
}
}
private val views by lazy { private val views by lazy {
ActTextBinding.inflate(layoutInflater) ActTextBinding.inflate(layoutInflater)
} }
@ -80,9 +145,7 @@ class ActText : AppCompatActivity() {
private var account: SavedAccount? = null private var account: SavedAccount? = null
private var searchResult: List<IntRange> = emptyList() private var searchResult = SearchResult()
private var searchError: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -115,7 +178,7 @@ class ActText : AppCompatActivity() {
?.let { daoSavedAccount.loadAccount(it) } ?.let { daoSavedAccount.loadAccount(it) }
if (savedInstanceState == null) { if (savedInstanceState == null) {
searchHighlight(null) showSearchResult(null)
val sv = intent.string(EXTRA_TEXT) ?: "" val sv = intent.string(EXTRA_TEXT) ?: ""
val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0 val contentStart = intent.int(EXTRA_CONTENT_START) ?: 0
@ -277,22 +340,31 @@ class ActText : AppCompatActivity() {
val keyword = views.etSearch.text?.toString() ?: "" val keyword = views.etSearch.text?.toString() ?: ""
val content = views.etText.text?.toString() ?: "" val content = views.etText.text?.toString() ?: ""
val useRegex = views.btnToggleRegex.isChecked val useRegex = views.btnToggleRegex.isChecked
val searchResult: List<IntRange> = withContext(AppDispatchers.IO) { this.searchResult = withContext(AppDispatchers.IO) {
buildList {
searchError = null
if (keyword.isEmpty()) {
// nothing to do.
} else if (useRegex) {
try { try {
val limit = 1000
var hasMore = false
val items = buildList {
when {
// 空欄
keyword.isEmpty() -> Unit
// 正規表現
useRegex -> {
val re = keyword.toRegex(RegexOption.IGNORE_CASE) val re = keyword.toRegex(RegexOption.IGNORE_CASE)
re.findAll(content).forEach { mr -> var nextStart = 0
while (nextStart < content.length) {
val mr = re.find(content, startIndex = nextStart) ?: break
if (size >= limit) {
hasMore = true
break
}
add(mr.range) add(mr.range)
nextStart = mr.range.last + 1
} }
} catch (ex: Throwable) {
log.e(ex, "search error.")
searchError = ex.message
} }
} else { // 生テキスト
else -> {
var nextStart = 0 var nextStart = 0
while (nextStart < content.length) { while (nextStart < content.length) {
val pos = content.indexOf( val pos = content.indexOf(
@ -301,6 +373,10 @@ class ActText : AppCompatActivity() {
ignoreCase = true ignoreCase = true
) )
if (pos == -1) break if (pos == -1) break
if (size >= limit) {
hasMore = true
break
}
val end = pos + keyword.length val end = pos + keyword.length
add(pos until end) add(pos until end)
nextStart = end nextStart = end
@ -308,44 +384,39 @@ class ActText : AppCompatActivity() {
} }
} }
} }
this.searchResult = searchResult SearchResult(items = items, hasMore = hasMore)
when { } catch (ex: Throwable) {
searchResult.isEmpty() -> searchHighlight(null) log.e(ex, "search error.")
else -> searchNext(byTextUpdate = true) SearchResult(error = ex.message)
}
}
showSearchResult {
searchResult.findNext(views.etText.selectionStart, allowEqual = true)
?: searchResult.items.firstOrNull()
} }
} }
private fun searchNext(byTextUpdate: Boolean = false) { private fun searchNext() {
try { showSearchResult {
val curPos = views.etText.selectionStart searchResult.findNext(views.etText.selectionStart, allowEqual = false)
val newPos = when { ?: searchResult.items.firstOrNull()
byTextUpdate -> searchResult.find { it.first >= curPos }
else -> searchResult.find { it.first > curPos }
} ?: searchResult.firstOrNull()
searchJump(newPos)
} catch (ex: Throwable) {
log.e(ex, "searchNext failed.")
} }
} }
private fun searchPrev() { private fun searchPrev() {
try { showSearchResult {
val curPos = views.etText.selectionStart.takeIf { it >= 0 } searchResult.findPrev(views.etText.selectionStart, allowEqual = false)
?: views.etText.text?.length ?: searchResult.items.lastOrNull()
?: return }
val newPos = searchResult.findLast { it.first < curPos } }
?: searchResult.lastOrNull()
searchJump(newPos) private fun showSearchResult(newPosFinder: (() -> IntRange?)?) {
val newPos: IntRange? = try {
newPosFinder?.invoke()
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "searchPrev failed.") log.e(ex, "newPosFinder failed.")
null
} }
}
private fun searchJump(newPos: IntRange?) {
searchHighlight(newPos)
}
private fun searchHighlight(newPos: IntRange?) {
val hasKeyword = !views.etSearch.text.isNullOrEmpty() val hasKeyword = !views.etSearch.text.isNullOrEmpty()
views.btnSearchClear.isEnabledAlpha = hasKeyword views.btnSearchClear.isEnabledAlpha = hasKeyword
@ -357,21 +428,23 @@ class ActText : AppCompatActivity() {
val idx = newPos?.let { val idx = newPos?.let {
val end = views.etText.text?.length ?: 0 val end = views.etText.text?.length ?: 0
views.etText.setSelection( views.etText.setSelection(
newPos.first.clip(0, end), it.first.clip(0, end),
(newPos.last + 1).clip(0, end), (it.last + 1).clip(0, end),
) )
searchResult.indexOf(newPos).takeIf { it >= 0 } searchResult.index(it.first)
} }
views.tvSearchCount.text = getString( views.tvSearchCount.text = getString(
R.string.search_result, R.string.search_result,
idx?.plus(1) ?: 0, idx?.plus(1) ?: 0,
searchResult.size searchResult.size,
if (searchResult.hasMore) "+" else ""
) )
} }
views.tvSearchError.vg(hasKeyword && !searchError.isNullOrBlank()) val error = searchResult.error
?.text = searchError views.tvSearchError.vg(hasKeyword && !error.isNullOrBlank())
?.text = error
views.etText.text?.let { e -> views.etText.text?.let { e ->
for (span in e.getSpans(0, e.length, SearchResultSpan::class.java)) { for (span in e.getSpans(0, e.length, SearchResultSpan::class.java)) {
@ -380,7 +453,7 @@ class ActText : AppCompatActivity() {
} catch (ignored: Throwable) { } catch (ignored: Throwable) {
} }
} }
for (pos in searchResult) { for (pos in searchResult.items) {
val attrId = when (newPos) { val attrId = when (newPos) {
pos -> R.attr.colorSearchFormBackground pos -> R.attr.colorSearchFormBackground
else -> R.attr.colorButtonBgCw else -> R.attr.colorButtonBgCw

View File

@ -1295,6 +1295,6 @@
<string name="save_to_local_folder">Save to local folder</string> <string name="save_to_local_folder">Save to local folder</string>
<string name="app_data_export_import">Export/Import app data</string> <string name="app_data_export_import">Export/Import app data</string>
<string name="translate_or_custom_share">Translate/Custom share buttons</string> <string name="translate_or_custom_share">Translate/Custom share buttons</string>
<string name="search_result" translatable="false">%1$d/%2$d</string> <string name="search_result" translatable="false">%1$d/%2$d%3$s</string>
<string name="toggle_regexp">.+\?</string> <string name="toggle_regexp">.+\?</string>
</resources> </resources>

View File

@ -26,7 +26,7 @@ buildscript {
ext.kotlinxCoroutinesVersion = "1.6.4" ext.kotlinxCoroutinesVersion = "1.6.4"
ext.kspVersion = "1.8.20-1.0.11" ext.kspVersion = "1.8.20-1.0.11"
ext.lifecycleVersion = "2.6.1" ext.lifecycleVersion = "2.6.1"
ext.materialVersion = "1.8.0" ext.materialVersion = "1.9.0"
ext.okhttpVersion = "4.10.0" ext.okhttpVersion = "4.10.0"
ext.preferenceVersion = "1.2.0" ext.preferenceVersion = "1.2.0"
ext.stBuildToolsVersion = "33.0.1" ext.stBuildToolsVersion = "33.0.1"