Skip to content

Commit bf28ace

Browse files
authored
화장실 검색어 매칭 단순화 (#410)
toilet simple search
1 parent 6a77411 commit bf28ace

File tree

3 files changed

+17
-112
lines changed
  • app-server/subprojects

3 files changed

+17
-112
lines changed

app-server/subprojects/bounded_context/external_accessibility/application/src/main/kotlin/club/staircrusher/external_accessibility/application/port/in/ExternalAccessibilitySearchService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class ExternalAccessibilitySearchService(
3131
}
3232
.filter {
3333
searchText ?: return@filter true
34-
searchText.isSimilarWith(it.name)
34+
it.name.isSimilarWith(pattern = searchText)
3535
}
3636
}
3737
}
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,25 @@
11
package club.staircrusher.stdlib.util.string
22

3-
fun String.emptyToNull() = this.ifBlank { null }
3+
import java.util.*
44

5-
// https://en.wikipedia.org/wiki/Levenshtein_distance#:~:text=The%20Levenshtein%20distance%20between%20two,defined%20the%20metric%20in%201965.
6-
// string similarity by levenshtein distance considering korean
7-
fun String.isSimilarWith(other: String, maxThreshold: Int = 3): Boolean {
8-
val similarity = jamoLevenshtein(this, other)
9-
return similarity <= maxThreshold
10-
}
5+
fun String.emptyToNull() = this.ifBlank { null }
116

12-
private fun Char.decomposeHangul(): List<Char>? {
13-
val result = mutableListOf<Char>()
14-
val choseongs =
15-
listOf('', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '')
16-
val joongseongs =
17-
listOf('', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '')
18-
val jongseongs = listOf(
19-
null,
20-
'',
21-
'',
22-
'',
23-
'',
24-
'',
25-
'',
26-
'',
27-
'',
28-
'',
29-
'',
30-
'',
31-
'',
32-
'',
33-
'',
34-
'',
35-
'',
36-
'',
37-
'',
38-
'',
39-
'',
40-
'',
41-
'',
42-
'',
43-
'',
44-
'',
45-
'',
46-
''
7+
fun String.isSimilarWith(pattern: String): Boolean {
8+
return simpleMatch(
9+
this.lowercase(Locale.US),
10+
pattern.lowercase(Locale.US).filter { it.isWhitespace().not() }
4711
)
48-
val char = this
49-
val codePoint = char.code
50-
51-
if (codePoint in 44032..55203) {
52-
val baseCode = codePoint - 44032
53-
val choseongIndex = baseCode / 21 / 28
54-
val joongseongIndex = baseCode / 28 % 21
55-
val jongseongIndex = baseCode % 28
56-
57-
result.addAll(
58-
listOfNotNull(choseongs[choseongIndex], joongseongs[joongseongIndex], jongseongs[jongseongIndex]),
59-
)
60-
} else {
61-
return null
62-
}
63-
return result
6412
}
6513

66-
67-
private fun levenshtein(s1: String, s2: String, cost: Map<Pair<Char, Char>, Int> = emptyMap()): Int {
68-
if (s1.length < s2.length) {
69-
return levenshtein(s2, s1, cost)
70-
}
71-
72-
if (s2.isEmpty()) {
73-
return s1.length
74-
}
75-
76-
val previousRow = IntArray(s2.length + 1) { it }
77-
for (i in s1.indices) {
78-
val currentRow = IntArray(s2.length + 1)
79-
currentRow[0] = i + 1
80-
for (j in s2.indices) {
81-
val insertion = previousRow[j + 1] + 1
82-
val deletion = currentRow[j] + 1
83-
val substitution = previousRow[j] + if (s1[i] == s2[j]) 0 else cost.getOrDefault(s1[i] to s2[j], 1)
84-
currentRow[j + 1] = minOf(insertion, deletion, substitution)
14+
private fun simpleMatch(text: String, pattern: String): Boolean {
15+
var patternIndex = 0
16+
for (char in text) {
17+
if (patternIndex < pattern.length && pattern[patternIndex] == char) {
18+
patternIndex++
8519
}
86-
previousRow.indices.forEach { previousRow[it] = currentRow[it] } // Optimized copy
87-
}
88-
return previousRow.last()
89-
}
90-
91-
private fun jamoLevenshtein(s1: String, s2: String): Int {
92-
if (s1.length < s2.length) {
93-
return jamoLevenshtein(s2, s1)
94-
}
95-
96-
if (s2.isEmpty()) {
97-
return s1.length
98-
}
99-
100-
val previousRow = IntArray(s2.length + 1) { it }
101-
for (i in s1.indices) {
102-
val currentRow = IntArray(s2.length + 1)
103-
currentRow[0] = i + 1
104-
for (j in s2.indices) {
105-
val insertion = previousRow[j + 1] + 1
106-
val deletion = currentRow[j] + 1
107-
val substitution = previousRow[j] + getJamoCost(s1[i], s2[j])
108-
currentRow[j + 1] = minOf(insertion, deletion, substitution)
20+
if (patternIndex == pattern.length) {
21+
return true
10922
}
110-
previousRow.indices.forEach { previousRow[it] = currentRow[it] } // Optimized copy
11123
}
112-
return previousRow.last()
113-
}
114-
115-
private fun getJamoCost(c1: Char, c2: Char): Int {
116-
if (c1 == c2) return 0
117-
val jamo1 = c1.decomposeHangul()
118-
val jamo2 = c2.decomposeHangul()
119-
return if (jamo1 != null && jamo2 != null) levenshtein(jamo1.joinToString(""), jamo2.joinToString("")) / 3 else 1
24+
return false
12025
}

app-server/subprojects/cross_cutting_concern/stdlib/src/unitTest/kotlin/club/staircrusher/stdlib/geography/StringUtilTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ class StringUtilTest {
1010
assert("농협하나로마트".isSimilarWith("농하나로마트"))
1111
assert("농협하나로마트".isSimilarWith("하나로마트"))
1212
assert("농협하나로마트".isSimilarWith("농협마트"))
13+
assert("농협하나로마트".isSimilarWith("농협"))
1314
assert("NonghyupMart".isSimilarWith("NonghyupMart"))
14-
assert("농협하나로마트".isSimilarWith("농협허너루미틋"))
15+
assert("NonghyupMart".isSimilarWith("nonghyup"))
1516

1617
assert(!"농협하나로마트".isSimilarWith("아무런"))
1718
assert(!"농협하나로마트".isSimilarWith("asdf"))
18-
assert(!"농협하나로마트".isSimilarWith("농협"))
1919
}
2020
}

0 commit comments

Comments
 (0)