Posts Tagged ‘Unicode’

絵文字を含むNSStringの正確な文字数をカウントする(2)


 ちょっと前に絵文字の混在した文字列のカウント方法について書いたら、ちゃんとカウントできない文字があると指摘された。
なるほど、確かにカウントできない文字がある。いったいどんな規則性があるのかと思っていたら、絵文字の文字コードをまとめていたサイトを教えてもらった。
それがこれ↓
iOS Emoji

 う~ん、まるで規則性が見当たらないと思っていたらピンときた。前は UTF8String とかやってたけど、内部の文字コードはもしかしてUTF-16ではなかろうか?と思って調べたらやっぱりUTF-16だった。それならわざわざUTF-8に変換しないでUTF-16のまま処理した方が良い。

 UTF-16といえば主要なトピックはサロゲートペアだ。0xD800-0xDBFFが上位サロゲート、0xDC00-0xDFFFが下位サロゲート。( Wikipedia#Unicode 参考)なので上位サロゲートを検出したら1文字スキップする。(1)

 さらに、国旗の絵文字には Regional Indicator という専用のシンボルが2つの組み合わせで使われている。( Unicode.orgのこのページ の最下段に記載されている。)なのでRegional Indicatorを検出して次のサロゲートペアもRegional Indicatorだったら3文字スキップする。(2)

 あと、ガラケーではお馴染みの四角で囲まれた数字には通常の数字の後に COMBINING ENCLOSING KEYCAP という専用の文字が配置されている。だがこれは単純に無視すればいいだろう。(3)

 そうすると、こんな感じで書けばいいかな?ちゃんと実装するには文字のインデックスが out of bounds にならんようにした方がいいかも。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (NSUInteger) actualNSStringSize:(NSString *) txt {
    int len = [txt length];
    NSUInteger count = 0;
    for (NSUInteger i = 0; i < len; i++) {
        unichar c = [txt characterAtIndex:i];
       
        if (0xD83C == c) {
            unichar c1 = [txt characterAtIndex:i+1];
            if ((0xDDE6 <= c1) && (c1 <= 0xDDFF)) {
                unichar c2 = [txt characterAtIndex:i+2];
                if (0xD83C == c2) {
                    unichar c3 = [txt characterAtIndex:i+3];
                    if ((0xDDE6 <= c3) && (c3 <= 0xDDFF)) {
                        // 国旗なのでスキップ・・・(2)
                        i += 3;
                        ++count;
                        continue;
                    }
                }
            }
            i++;
            ++count;
        }
        else if (0xD800 <= c && c <= 0xDBFF) {
            // 上位サロゲート・・・(1)
            i++;
            ++count;
        }
        else if (0xDC00 <= c && c <= 0xDFFF) {
            // 下位サロゲートなので念のため無視
        }
        else if (0x20E3 == c) {
            // 囲みなので無視・・・(3)
        }
        else {
            ++count;
        }
    }
    return count;
}

 ちなみに、これらの絵文字はiOS独自の実装かと思っていたらそうではなく、Unicode6.0 というれっきとした国際規格なのだそうだ。ガラケーに端を発した日本発の絵文字が知らない間に国際規格に…っていうか Unicode のバージョンって知らない間にこんなに上がっていたんだ。

絵文字を含むNSStringの正確な文字数をカウントする


 iPhone/iPad のキーボード設定で「絵文字」というキーボードを追加すると、ガラケーよろしく絵文字を入力できるようになる。なので、何も考えないとUITextFieldなどに絵文字を入力されてしまう可能性がある。ここでアプリの仕様上、絵文字を許容するかしないかという議論は当然起こるが、許容したくないというケースはひとまず置いといて、許容する場合のトピックを取り上げる。

 絵文字も他の文字と同様UTF-8であるようだが、絵文字の中には4バイトで表現されるものもあるようで、その4バイトの絵文字を含んだ文字列は [str length] で得られる文字数が狂ってしまうのだ。そうなるとバリデーションとして文字数制限を設ける場合にうまくない。

 この現象はどうやらNSStringクラスのUTF-8の取り扱いに原因があるようだが、不具合だと認識されていれば将来的にSDKの方で対応される可能性がある。それまでは次のような関数で凌ぎたいと思う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (NSUInteger) actualUtf8StringSize:NSString str {
     const char* cstr = [str UTF8String];
     int len = strlen(cstr);
     NSUInteger count = 0;
     unsigned char c;
     for (int i = 0; i < len; i++) {
          c = (unsigned char)cstr[i];
          if (0x00 <= c && c <= 0x7F) {
               ;
          }
          else if (0xC2 <= c && c <= 0xDF) {
               i += 1;
          }
          else if (0xE0 <= c && c <= 0xEF) {
               i += 2;
          }
          else if (0xF0 <= c && c <= 0xF7) {
               i += 3;
          }
          else {
               continue;
          }
          ++count;
     }
     return count;
}

 これはUTF-8の性質を利用して文字数をカウントしている。UTF-8は文字の1バイト目でその文字が何バイトかを示している。なので、バイト列を先頭から調べて行き、文字のバイト数分スキップしながらカウンタを増加している。ひとつ留意したいのは、バイト列から1バイト取得する時に char 型ではなく unsigned char 型の変数に代入することだ。C言語では常識の範疇だが、慣れていないと見落としやすい。

アーカイブ