Skip to content

Latest commit

 

History

History

InitialConsonantSearch

Initial Consonant Search Samples - 초성검색

Swift Objective-C
iOS macOS

MGRJamo (Objective-C)
SKHJamo (Swift)

  • MGRJamo
    • UTF-8 기준의 Objective-C용 한글 자모 분해 라이브러리
    • Written in Objective-C, Swift and Objective-C compatability
  • SKHJamo
    • UTF-8 기준의 Swift용 한글 자모 분해 라이브러리
    • Written in Swift
  • 텍스트 필드를 사용해서 검색을 구현할 시 한글 자음과 모음을 분리해서도, 합쳐서도 필터링이 되게 한다.

    - "이격도" 라는 텍스트를 검색한다 ->
        - 이격도 라는 글자 + "ㅇ, 이, 익, 이격, 이격ㄷ, 이격도, ㅇㄱㄷ" 까지도 검색이 되게 하려면 다음을 이용한다
    
  • MTS 프로젝트를 진행하면서 초성검색에 대한 요구사항이 있어서 제작함.

  • 추가적으로 Debounce 기능을 추가할 수 있으나, 자료의 양이 많지 않을 경우(경험상 1000개 이하)는 생략하는 것이 UX상 더 나은 것으로 사료된다.

Preview

  • 관련동영상

Usage

Swift

let word = "이격도"
let jamo = SKHJamo.getJamo(word)

// "ㅇㅣㄱㅕㄱㄷㅗ"

Objective-C

    
NSString *word = @"이격도";
NSString *jamo = [MGRJamo getJamo:word];
  
// "ㅇㅣㄱㅕㄱㄷㅗ"

Documentation

  • SKHJamo.swift
    • 한글 자음과 모음의 분리를 처리 (MGRJamo.h, MGRJamo.m 파일은 생략함)
import Foundation

extension CharacterSet{
    static var modernHangul: CharacterSet{
        return CharacterSet(charactersIn: ("".unicodeScalars.first!)...("".unicodeScalars.first!))
    }
}

public class SKHJamo {
    
    // UTF-8 기준
    static let INDEX_HANGUL_START:UInt32 = 44032  // "가"
    static let INDEX_HANGUL_END:UInt32 = 55203    // "힣"
    
    static let CYCLE_CHO :UInt32 = 588
    static let CYCLE_JUNG :UInt32 = 28
    
    static let CHO = [
        "","","","","","","","","","",
        "","","","","","","","",""
    ]
    
    static let JUNG = [
        "", "", "", "", "", "","", "", "", "",
        "", "","", "", "", "", "", "", "", "",
        ""
    ]
    
    static let JONG = [
        "","","","","","","","","","",
        "","","","","","","","","","",
        "","","","","","","",""
    ]
    
    static let JONG_DOUBLE = [
        "":"ㄱㅅ","":"ㄴㅈ","":"ㄴㅎ","":"ㄹㄱ","":"ㄹㅁ",
        "":"ㄹㅂ","":"ㄹㅅ","":"ㄹㅌ","":"ㄹㅍ","":"ㄹㅎ",
        "":"ㅂㅅ"
    ]
    
    static let MO = [
        "":"", "":"", "":"", "":"", "":"", "":"", "":""
    ]
        
    static let MO_LIST = [
        "", "", "", "", "", "", ""
    ]
        
    static let JA = [
        "":2, "":4, "":2, "":3, "":6,
        "":5, "":4, "":4, "":8, "":2,
        "":4, "":1, "":3, "":6, "":4,
        "":3, "":4, "":4, "":3, "":2,
        "":3, "":3, "":4, "":2, "":3,
        "":3, "":4, "":2, "":4, "":5,
        "":3, "":3, "":2, "":4, "":5,
        "":3, "":3, "":1, "":2, "":1,
        "":4, "":5, "":5, "":7, "":9,
        "":9, "":7, "":9, "":9, "":8,
        "":6
    ]
    
    // 주어진 "코드의 음절"을 자모음으로 분해해서 리턴하는 함수
    private class func getJamoFromOneSyllable(_ n: UnicodeScalar) -> String?{
        if CharacterSet.modernHangul.contains(n){
            let index = n.value - INDEX_HANGUL_START
            let cho = CHO[Int(index / CYCLE_CHO)]
            let jung = JUNG[Int((index % CYCLE_CHO) / CYCLE_JUNG)]
            var jong = JONG[Int(index % CYCLE_JUNG)]
            if let disassembledJong = JONG_DOUBLE[jong] {
                jong = disassembledJong
            }
            return cho + jung + jong
        }else{
            return String(UnicodeScalar(n))
        }
    }
    
    // 주어진 "코드의 음절"중 초성을 분해해서 리턴하는 함수
    private class func getChoFromOneSyllable(_ n: UnicodeScalar) -> String?{
        if CharacterSet.modernHangul.contains(n){
            let index = n.value - INDEX_HANGUL_START
            let cho = CHO[Int(index / CYCLE_CHO)]
            return cho
        } else {
            return String(UnicodeScalar(n))
        }
    }
}

extension SKHJamo {
    
    // 주어진 "단어"를 자모음으로 분해해서 리턴하는 함수
    class func getJamo(_ input: String) -> String {
        var jamo = ""
        //let word = input.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
        for scalar in input.unicodeScalars{
            jamo += getJamoFromOneSyllable(scalar) ?? ""
        }
        return jamo
    }
    
    class func getJamoList(_ input: String) -> [String] {
        var jamos: [String] = []
        //let word = input.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
        for scalar in input.unicodeScalars{
            jamos.append(getJamoFromOneSyllable(scalar) ?? "")
        }
        return jamos
    }
    
    // 주어진 "단어"를 초성만 가져와서 리턴하는 함수
    class func getCho(_ input: String) -> String {
        var jamo = ""
        //let word = input.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: .punctuationCharacters)
        for scalar in input.unicodeScalars{
            jamo += getChoFromOneSyllable(scalar) ?? ""
        }
        return jamo
    }
    
    class func getDanmo(_ input: Character) -> Character {
        for (key, value) in MO {
            if key == "\(input)" {
                return Character(value)
            }
        }
        return input
    }
    
    //이전 입력한 내용과 비교해서 삭제인지 추가 입력인지 확인하는 함수
    class func isDanmoDelete(preInputList: [String], inputList: [String]) -> Bool {
        var preCount = 0
        var curCount = 0
        
        for text in preInputList {
            for (key, value) in JA {
                if text == key  {
                    preCount += value
                    break
                }
            }
        }
        
        for text in inputList {
            for (key, value) in JA {
                if text == key {
                    curCount += value
                    break
                }
            }
        }
        
        if curCount < preCount {
            return true
        }
        return false
    }
}
  • String+Extension.swift
    • 실제 사용에 있어서는 extension을 이용하여 일관된 논리를 유지한다.
import Foundation

public extension String {

    func mgrRange<T>(
        of searchString: T,
        options mask: String.CompareOptions = [],
        range searchRange: Range<Self.Index>? = nil,
        locale: Locale? = nil
    ) -> NSRange where T : StringProtocol {
        var searchString = searchString.trimmingCharacters(in: .whitespaces) // 양끝 트리밍
        let result = self.range(of: searchString, options: mask, range: searchRange, locale: locale)
        if let result = result {
            let loc = result.lowerBound.utf16Offset(in: self)
            let len = result.upperBound.utf16Offset(in: self) - loc
            return NSMakeRange(loc, len)
        }
        var selfString = SKHJamo.getJamo(self)
        searchString = SKHJamo.getJamo(searchString)
        var range = selfString.range(of: searchString, options: mask, range: searchRange, locale: locale)
        if let range = range {
            let loc = range.lowerBound.utf16Offset(in: selfString)
            let len = range.upperBound.utf16Offset(in: selfString) - loc
            return NSMakeRange(loc, len)
        }
        selfString = SKHJamo.getCho(self)
        /// searchString = SKHJamo.getCho(searchString) // 이건 완전히 이미 분해되었음.(∵) 기존에 완전히 분해했으므로.
        range = selfString.range(of: searchString, options: mask, range: searchRange, locale: locale)
        if let range = range {
            let loc = range.lowerBound.utf16Offset(in: selfString)
            let len = range.upperBound.utf16Offset(in: selfString) - loc
            return NSMakeRange(loc, len)
        }
        return NSRange(location: NSNotFound, length: 0)
    }
    
}

Author

sonkoni(손관현), [email protected]

License

This project is released under the MIT License. See LICENSE for more information.