diff --git a/.jules/bolt.md b/.jules/bolt.md index 5762283c6..f778fe2d9 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -21,3 +21,6 @@ ## 2024-05-18 - Pre-compile Regex in OCR formatting **Learning:** `OcrTextFormatter.formatLines` instantiated `Regex` for bullet removal inline every time it was called when `stripBullets` was true. This causes unnecessary allocation and compilation overhead when parsing OCR lines. **Action:** Extract frequently used formatting regexes (such as `bulletRegex`) into `object` or companion object private constants (`private val BULLET_REGEX`) to optimize regex reuse and eliminate unnecessary compilation per text parsing operation. +## 2024-05-18 - Optimize Diacritic Stripping in Suggest +**Learning:** In `Suggest.isSameIgnoringAccents`, `val DIACRITIC_REGEX = "\\p{M}+".toRegex()` was being used in combination with `Normalizer.normalize` and `replace` to strip accents. This created string allocations via `Normalizer.normalize` and `Regex.replace` even for plain ASCII strings that lack accents, which is a hot path during typing and suggestion processing. +**Action:** Replace `Regex.replace` with a manual character loop that avoids `Normalizer.normalize` entirely for pure ASCII strings and strips `Character.NON_SPACING_MARK` without regex allocation when normalizing. diff --git a/app/src/main/java/helium314/keyboard/latin/Suggest.kt b/app/src/main/java/helium314/keyboard/latin/Suggest.kt index 7e8328b34..d6700f11e 100644 --- a/app/src/main/java/helium314/keyboard/latin/Suggest.kt +++ b/app/src/main/java/helium314/keyboard/latin/Suggest.kt @@ -603,11 +603,30 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { return false } - private val DIACRITIC_REGEX = "\\p{M}+".toRegex() + private fun stripAccents(s: String): String { + var hasAccents = false + for (i in s.indices) { + if (s[i].code > 127) { + hasAccents = true + break + } + } + if (!hasAccents) return s + + val norm = java.text.Normalizer.normalize(s, java.text.Normalizer.Form.NFD) + val clean = java.lang.StringBuilder(norm.length) + for (i in norm.indices) { + val c = norm[i] + if (Character.getType(c) != Character.NON_SPACING_MARK.toInt()) { + clean.append(c) + } + } + return clean.toString() + } + private fun isSameIgnoringAccents(a: String, b: String): Boolean { - val normA = java.text.Normalizer.normalize(a, java.text.Normalizer.Form.NFD).replace(DIACRITIC_REGEX, "") - val normB = java.text.Normalizer.normalize(b, java.text.Normalizer.Form.NFD).replace(DIACRITIC_REGEX, "") - return normA.equals(normB, ignoreCase = true) + if (a.equals(b, ignoreCase = true)) return true + return stripAccents(a).equals(stripAccents(b), ignoreCase = true) } private fun getTransformedSuggestedWordInfoList(