본문 바로가기

프로그래밍/Swift

[Swift] 유니코드와 숫자 간의 변환

프로그래밍을 하다 보면 데이터 타입 간의 다양한 변환(conversion)이 필요하다. 데이터 변환 중에서 문자를 숫자로 변환하는 것이 종종 필요할 때가 있다. 이 번 시간에는 해당 변환을 하는 방법과 주의 사항에 대한 포스팅이다. 

 

문자를 숫자로 변환하는 방법 중에서 String 타입의 유니코드를 Int 타입의 숫자로 변환하는 방법에 대해 살펴보자. 

(유니 코드에 대한 설명은 다음 포스팅을 참고하길 바란다. https://taeminator1.tistory.com/50)

 

유니코드 "A"와 그에 해당하는 숫자 65를 예시로 해서 변환 방법을 살펴보겠다. 

 

먼저 유니코드(String)를 숫자(Int)로 변환하는 방법이다. 

// 유니코드(String)에 해당하는 Int 값으로 반환
let str: String = "A"
let number: Int = Int(UnicodeScalar(str)!.value)
// 위 두 문장을 다음과 같이 표현 가능: let number: Int = Int(UnicodeScalar("A").value)
print(number)                   // 65

Swift가 기본적으로 제공하는 Unicode.Scalar 타입의 value를 활용하여 구했다. (typealias UnicodeScalar = Unicode.Scalar)

value의 타입이 UInt32이므로 Int에 대한 인스턴스를 새로 생성하여 Int 값을 구해줬다. 

 

다음은 숫자(Int)를 유니코드(String)로 변환하는 방법이다. 

// Int 값에 해당하는 유니코드(String) 값 반환
let unicode: String = String(UnicodeScalar(65)!)
print(unicode)                  // A

Unicode.Scalar 타입의 초기화 매개변수로 Int를 전달하여 생성하였다. Unicode.Scalar 타입은 각각의 숫자에 대응되는 유니코드 값이 있을 수도 있고 없을 수도 있어서 랩핑 되어 있다. 여기서는 일단 강제로 언랩핑해주었다.

역시 String에 대한 인스턴스를 생성하여 String 값을 구해줬다. 

 

Swift에서는 16 진수 값을 바로 String으로 출력해주는 방법도 다음과 같이 제공한다. 

// 16 진수 값에 대한 유니코드(String) 값 반환
let unicode2: String = "\u{41}" // 0x41 = 65
print(unicode2)                 // A

 

다음은 각각의 숫자에 대한 유니코드 값을 확인해 보자. 

먼저 0부터 127까지의 숫자에 대한 유니코드(ASCII) 값을 확인해 보자. (아스키는 7비트의 이진수에 대응하므로 128가지의 숫자를 표현할 수 있다)

var asciis: [String] = []
for i in 0 ..< 128 {
    asciis.append(String(UnicodeScalar(i)!))
}
print(asciis)
// ["\0", "\u{01}", "\u{02}", "\u{03}", "\u{04}", "\u{05}", "\u{06}", "\u{07}", "\u{08}", "\t", "\n", "\u{0B}", "\u{0C}", "\r", "\u{0E}", "\u{0F}", "\u{10}", "\u{11}", "\u{12}", "\u{13}", "\u{14}", "\u{15}", "\u{16}", "\u{17}", "\u{18}", "\u{19}", "\u{1A}", "\u{1B}", "\u{1C}", "\u{1D}", "\u{1E}", "\u{1F}", " ", "!", "\"", "#", "$", "%", "&", "\'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "\u{7F}"]

각각의 숫자에 대응되는 128개의 유니코드를 확인할 수 있다. 여기서 "\u{xx}"와 같이 표현된 문자는 실제로는 아무것도 출력되지 않지만 문자로서 역할은 하는 제어 문자에 해당한다. "\x"와 같이 표현된 문자는 제어 문자 중에서도 많이 사용되는 문자들로, "\u{xx}"와 같이 혼용해서 사용 가능하다. (예를 들어 "\n"은 "\u{A}"와 같다)

 

다음은 128부터 65535까지의 숫자에 대한 유니코드 값을 확인해 보자. (유니코드는 2바이트 이진수에 대응하므로 65536(2^16) 가지의 숫자를 표현할 수 있다)

var unicodes: [String] = []
for i in 0 ..< 65536 {
    unicodes.append(String(UnicodeScalar(i)!))      // Fatal error!!!
}

아스키 때와 마찬가지로 유니코드 값을 할당해주면, "Unexpectedly found nil while unwrapping an Optional value" 에러가 발생한다. 강제 언랩핑 시에 nil값이 발견되었다는 말이다. 즉 0부터 65535까지의 숫자 중에 유니코드에 대응되지 않는 숫자들이 존재한다는 것이다.

 

다음과 같이 nil에 대응되는 숫자를 찾았다. 

// 유니코드 중 nil에 대응되는 숫자 찾기
var i: Int = 0
while i < 65536 {
    if UnicodeScalar(i) == nil {
        var j: Int = i + 1
        while UnicodeScalar(j) == nil && j < 65536 {
            j += 1
        }
        print("\(i) ..< \(j)에 대응되는 유니코드는 nil")
        i = j
    }
    i += 1
}

그 결과 숫자 55296부터 57343(55296 ..< 57344)에 대응되는 유니코드는 nil인 것을 확인했다. (65536보다 큰 값에 대한 유니코드도 할당이 되어있는데, 이 부분은 생략하겠다)

 

다음은 유니코드 중에 이상한? 문자들을 소개하겠다. 그것은 바로 유니코드와 결합되어 사용되는 첨자(또는 악센트)이다. 

사실 이것 때문에 이번 포스팅이 작성되었다고 해도 과언이 아니다.. 

결론부터 말하면, 해당 첨자는 다음과 같은 숫자에 대응되는 유니코드이다. 

 

768 ..< 880, 1155 ..< 1162, 1425 ..< 1470, 1471 ..< 1472, 1473 ..< 1475, 1476 ..< 1478, 1479 ..< 1480, 1552 ..< 1563, 1611 ..< 1632, 1648 ..< 1649, 1750 ..< 1757, 1759 ..< 1765, 1767 ..< 1769, 1770 ..< 1774, 1809 ..< 1810, 1840 ..< 1867, 1958 ..< 1969, 2027 ..< 2036, 2045 ..< 2046, 2070 ..< 2074, 2075 ..< 2084, 2085 ..< 2088, 2089 ..< 2094, 2137 ..< 2140, 2259 ..< 2274, 2275 ..< 2308, 2362 ..< 2365, 2366 ..< 2384, 2385 ..< 2392, 2402 ..< 2404, 2433 ..< 2436, 2492 ..< 2493, 2494 ..< 2501, 2503 ..< 2505, 2507 ..< 2510, 2519 ..< 2520, 2530 ..< 2532, 2558 ..< 2559, 2561 ..< 2564, 2620 ..< 2621, 2622 ..< 2627, 2631 ..< 2633, 2635 ..< 2638, 2641 ..< 2642, 2672 ..< 2674, 2677 ..< 2678, 2689 ..< 2692, 2748 ..< 2749, 2750 ..< 2758, 2759 ..< 2762, 2763 ..< 2766, 2786 ..< 2788, 2810 ..< 2816, 2817 ..< 2820, 2876 ..< 2877, 2878 ..< 2885, 2887 ..< 2889, 2891 ..< 2894, 2901 ..< 2904, 2914 ..< 2916, 2946 ..< 2947, 3006 ..< 3011, 3014 ..< 3017, 3018 ..< 3022, 3031 ..< 3032, 3072 ..< 3077, 3134 ..< 3141, 3142 ..< 3145, 3146 ..< 3150, 3157 ..< 3159, 3170 ..< 3172, 3201 ..< 3204, 3260 ..< 3261, 3262 ..< 3269, 3270 ..< 3273, 3274 ..< 3278, 3285 ..< 3287, 3298 ..< 3300, 3328 ..< 3332, 3387 ..< 3389, 3390 ..< 3397, 3398 ..< 3401, 3402 ..< 3406, 3415 ..< 3416, 3426 ..< 3428, 3457 ..< 3460, 3530 ..< 3531, 3535 ..< 3541, 3542 ..< 3543, 3544 ..< 3552, 3570 ..< 3572, 3633 ..< 3634, 3635 ..< 3643, 3655 ..< 3663, 3761 ..< 3762, 3763 ..< 3773, 3784 ..< 3790, 3864 ..< 3866, 3893 ..< 3894, 3895 ..< 3896, 3897 ..< 3898, 3902 ..< 3904, 3953 ..< 3973, 3974 ..< 3976, 3981 ..< 3992, 3993 ..< 4029, 4038 ..< 4039, 4141 ..< 4152, 4153 ..< 4159, 4182 ..< 4186, 4190 ..< 4193, 4209 ..< 4213, 4226 ..< 4227, 4228 ..< 4231, 4237 ..< 4238, 4253 ..< 4254, 4957 ..< 4960, 5906 ..< 5909, 5938 ..< 5941, 5970 ..< 5972, 6002 ..< 6004, 6068 ..< 6100, 6109 ..< 6110, 6155 ..< 6158, 6277 ..< 6279, 6313 ..< 6314, 6432 ..< 6444, 6448 ..< 6460, 6679 ..< 6684, 6741 ..< 6751, 6752 ..< 6753, 6754 ..< 6755, 6757 ..< 6781, 6783 ..< 6784, 6832 ..< 6849, 6912 ..< 6917, 6964 ..< 6981, 7019 ..< 7028, 7040 ..< 7043, 7073 ..< 7086, 7142 ..< 7156, 7204 ..< 7224, 7376 ..< 7379, 7380 ..< 7401, 7405 ..< 7406, 7412 ..< 7413, 7415 ..< 7418, 7616 ..< 7674, 7675 ..< 7680, 8204 ..< 8206, 8400 ..< 8433, 11503 ..< 11506, 11647 ..< 11648, 11744 ..< 11776, 12330 ..< 12336, 12441 ..< 12443, 42607 ..< 42611, 42612 ..< 42622, 42654 ..< 42656, 42736 ..< 42738, 43010 ..< 43011, 43014 ..< 43015, 43019 ..< 43020, 43043 ..< 43048, 43052 ..< 43053, 43136 ..< 43138, 43188 ..< 43206, 43232 ..< 43250, 43263 ..< 43264, 43302 ..< 43310, 43335 ..< 43348, 43392 ..< 43396, 43443 ..< 43457, 43493 ..< 43494, 43561 ..< 43575, 43587 ..< 43588, 43596 ..< 43598, 43644 ..< 43645, 43696 ..< 43697, 43698 ..< 43701, 43703 ..< 43705, 43710 ..< 43712, 43713 ..< 43714, 43755 ..< 43760, 43765 ..< 43767, 44003 ..< 44011, 44012 ..< 44014, 63600 ..< 63616, 63620 ..< 63642, 63647 ..< 63648, 64286 ..< 64287, 65024 ..< 65040, 65056 ..< 65072, 65438 ..< 65440

 

이러한 유니코드에 대응되는 숫자는, 어떤 유니코드(여기서는 공백)와 해당 유니코드가 연속해서 쓰이면, 문자의 길이가 2가 아니라 1이 되는 것을 이용해 찾았다. 

var i: Int = 0
var j: Int = 0
while i < 55296 {
    var tmpA: String = " "
    tmpA.append(String(UnicodeScalar(i)!))
    
    if tmpA.count == 1 {
        j = i + 1
        var tmpB: String = " "
        tmpB.append(String(UnicodeScalar(j)!))
        while tmpB.count == 1 && j < 55296 {
            j += 1
            tmpB = " "
            tmpB.append(String(UnicodeScalar(j)!))
        }
        print("\(i) ..< \(j), ", terminator: "")
        i = j
    }
    i += 1
}

i = 57344
while i < 65536 {
    var tmpA: String = " "
    tmpA.append(String(UnicodeScalar(i)!))
    
    if tmpA.count == 1 {
        j = i + 1
        var tmpB: String = " "
        tmpB.append(String(UnicodeScalar(j)!))
        while tmpB.count == 1 && j < 65536 {
            j += 1
            tmpB = " "
            tmpB.append(String(UnicodeScalar(j)!))
        }
        print("\(i) ..< \(j), ", terminator: "")
        i = j
    }
    i += 1
}

while문을 두 번에 걸쳐 구한 이유는, 중간에 숫자에 대응되는 유니코드가 없는 경우(55296 ..< 57344)를 피하기 위해서다. 

 

당연히 해당 범위를 외울 필요는 없다. 다만 어떤 숫자에 해당하는 유니코드는 다른 유니코드와 결합하여 하나의 유니코드를 만든다는 사실만 알고 있으면 된다. 

 

실제로 768에 해당하는 유니코드에 대해 자세히 살펴보자. 앞에서 해당 유니코드는 다른 유니코드와 결합하여 하나의 유니코드를 만든다고 하였다. "A"에 해당 문자 뒤에 해당 유니코드를 써주면 다음과 같이 출력된다. 

let str2: String = "\u{41}\u{300}"  // 0x41 = 65, 0x300 = 768
print(str2)                         // À
print("A\u{300}")                   // À

A위에 작은 첨자가 붙은 것을 확인할 수 있다. 

 

그럼 해당 문자를 다시 유니코드에 대응되는 숫자로 변경하면 어떻게 될까??

let number2: Int = Int(UnicodeScalar(str2)!.value)            // Fatal error!!!

숫자에 해당하는 유니코드가 없을 때와 마찬가지로 "Unexpectedly found nil while unwrapping an Optional value" 에러가 발생했다. 

 

하지만 각각의 유니코드에 대한 숫자는 또 반환이 가능하다. 

let n1: Int = Int(UnicodeScalar("\u{41}").value)
print(n1)                           // 65
let n2: Int = Int(UnicodeScalar("\u{300}").value)
print(n2)                           // 768

그리고 각각의 유니코드를 따로 출력하는 것도 가능하다.

print("\u{41}")                     // A
print("\u{300}")                    // ̀

 

그렇다면 각각의 유니코드와 결합된 문자의 길이는 어떨까??

print("\u{41}\u{300}".count)        // 1
print("\u{41}".count)               // 1
print("\u{300}".count)              // 1

유니코드가 2개 결합을 해도 문자의 길이는 하나이고, 각각의 유니코드도 길이는 1이다. 

 

이러한 결과로 다음과 같은 사실을 도출할 수 있다. 

  1. 유니코드 중에는 앞의 문자와 결합하여 사용하는 첨자 형식의 유니코드가 있다. 
  2. 어떤 문자와 해당 유니코드를 결합하면 하나의 문자가 된다. 
  3. 결합된 하나의 문자는 유니코드에 대응되는 숫자가 없다. 

여기서 주의해야 할 점은 3번이다. 어떤 문자열을 입력받아 연산을 위해 숫자로 변형하려고 할 때, 위에서와 같이, UnicodeScalar의 value를 이용하면 에러가 발생한다. 다시 말해 해당 문자(유니코드 두 개가 결합한 문자 하나)는 대응되는 숫자가 없다(유니코드가 아니다). 

 

코딩을 하다가, 어떤 문자열을 숫자로 변환하였는데, 자꾸 에러가 나서 한참을 헤매었는데, 3번과 같이 대응되는 숫자가 없다는 것을 알았고, 해당 숫자의 범위를 찾다가 이렇게 정리를 하게 되었다. 

 

혹시 유니코드로 결합된 문자를 다시 유니코드 두 개로 분리할 수 있는 방법을 알게 되면 추가하겠다.