Android 한글 초성 검색 어댑터

한글 초성 검색

영어의 경우 한글처럼 받침이 있지 않기에 contain()만으로 주어진 목록에서 필요한 데이터를 추출할 수 있다.

한글 단어의 경우 contains()를 통해서 검색 기능을 어느 정도 활용할 수 있지만 초성(ㅅㄱ, ㄱㄴ, ㄱㄴㄷ)을 사용하다면 추가적인 기능이 필요하다.

Unicode 사용


private val koreanUnicodeStart = 44032 // 가
private val koreanUnicodeEnd = 55203   // 힣

private val koreanUnicodeBased = 588   // 각 자음 마다 가지는 글자 수

// 자음
private val koreanConsonant = arrayOf(
    '', '', '', '', '', '', '', '', '',
    '', '', '', '', '', '', '', '', '', ''
)

가~힣까지의 범위에 있으면 한글이라 생각하면 된다. koreanUnicodeBased는 각 자음 마다 가지는 글자 수를 통해 가,갸,고,겨…….등은 ‘ㄱ’ 글자의 맵핑 시키기 위해 사용한다.

검색 키워드의 초성 or 한글 체크

/**
* 해당 문자가 초성인지 체크
*/
private fun isConsonant(ch: Char): Boolean {
  return koreanConsonant.contains(ch)
}

/**
* 해당 문자가 한글인지 체크
*/
private fun isKorean(ch: Char): Boolean {
  val charCode = ch.code
  return charCode in koreanUnicodeStart..koreanUnicodeEnd
}

/**
* 자음을 얻는다
*/
private fun getConsonant(ch: Char): Char {
  val hasBegin = (ch.code - koreanUnicodeStart)
  val idx = hasBegin / koreanUnicodeBased
  return koreanConsonant[idx]
}

비교대상과 검색어의 각 글자를 비교하기 위해서는 초성(ㄱ, ㄴ, ㄷ…..) 또는 한글(가 ~ 힣)인지를 알아야 한다.

(문자열의 코드값 - 유니코드 시작값) / 각 자음마다 가지는 글자 수의 몫은 자음 배열에서 각 위치를 의미한다.

예를 들어 “난”이라고 가정을하면 가 ~ 나 ~~ ~~ 다 …… 처럼 유니코드 순서가 존재한다. 각 자음에 시작코드값을 빼면 ‘가’의 위치는 0 이고 ‘나’의 위치는 1 * koreanUnicodeBased 이고 ‘다의’ 위치는 2 * koreanUnicodeBased 가 된다.

‘난’의 값은 1*koreanUnicodeBased + 알파 이므로 koreanUnicodeBased로 나누면 index 값이 1이 나오고 이는 자음 배열에서 ‘ㄴ’이 되므로 난의 자음을 찾을 수 있게 된다.

비교대상을 검색어와 맵핑

/**
* 초성 또는 한글 검색
* @param based 비교대상
* @param search 검색단어
*/
fun matchKoreanAndConsonant(based: String, search: String): Boolean {
  var temp = 0
  val diffLength = based.length - search.length
  val searchLength = search.length

  if (diffLength < 0) {
      return false
  } else {
      for (i in 0..diffLength) {
          temp = 0

          while (temp < searchLength) {
              if (isConsonant(search[temp]) && isKorean(based[i + temp])) {
                  // 현재 char이 초성이고 based가 한글일 때
                  if (getConsonant(based[i + temp]) == search[temp]) {
                      // 각각의 초성끼리 같은지 비교
                      temp++
                  } else {
                      break
                  }
              } else {
                  // char가 초성이 아니라면
                  if (based[i + temp] == search[temp]) {
                      temp++
                  } else {
                      break
                  }
              }
          }

          // 모두 일치한 결과
          if (temp == searchLength) return true
      }

      // 일치하는 것을 찾지 못했을 때
      return false
  }

}

“가나다” 에서 “ㄱㄴ”, “ㄴㄷ”, “ㄷ” 등 다양하게 찾는 경우가 발생한다. searchKeyword의 모든 자음 또는 문자가 based와 모두 맞는지를 비교하여 일치하는 경우에는 true를 리턴하도록 구현한다.

샘플 코드

override fun getFilter(): Filter {
        return object : Filter() {
            override fun performFiltering(constraint: CharSequence): FilterResults {
                val charString = constraint.toString()

                if (charString.isEmpty()) {
                    filteredList.clear()
                    filteredList.addAll(dataSet)
                } else {
                    val filteringList: ArrayList<String> = ArrayList()

                    val koreanMatcher = KoreanMatcher()

                    for (name in dataSet) {
                        if (koreanMatcher.matchKoreanAndConsonant(name, charString)) filteringList.add(name)
                        else { // 영어인 경우
                            if (name.lowercase(Locale.getDefault()).contains(charString.lowercase(Locale.getDefault()))) {
                                filteringList.add(name)
                            }
                        }
                    }

                    filteredList.clear()
                    filteredList.addAll(filteringList)
                }

                val filterResults = FilterResults()
                filterResults.values = filteredList
                return filterResults
            }

            override fun publishResults(constraint: CharSequence, results: FilterResults) {
                filteredList = results.values as ArrayList<String>
                notifyDataSetChanged()
            }
        }
    }

검색 키워드를 koreanMatcher를 통해 먼저 비교하고 실패할 경우 contains()을 통해 한번 더 비교하도록 구현했다. 코드를 한번 더 리팩토링 한다면 한글 vs 영어를 선 체크후 따로 호출하는 것이 더 좋아보인다.

adapter 나 textWater의 내용은 비슷하기에 Filterable 관련 코드만 추가했고 자세한 내용은 아래의 위치에서 프로젝트를 확인하면 된다. 깃헙코드 참고



Related Posts