日々、Xcode の機嫌を伺う読者諸賢の皆様、こんばんは。これは、Objective-C Advent Calendar 2012の3日目の記事です。以前、NSString
な文字列から絵文字だけを除去する必要に駆られたことがあったので、不幸にも同じ事態に陥った方の為のメモを残しておきます。
###サロゲートペア(代用対)
NSString
は UTF-16のラッパだそうで、その文字列にはデータ長が 16bit の文字と、サロゲートペアと呼ばれる 32bit の文字が混在しています。サロゲートペアは、上位 16bit が 0xD800~0xDBFF、下位 16bit が 0xDC00~0xDFFF の範囲に収まることになっていて、ある文字がサロゲートペアかどうかは、最初の 16bit を見ることで判別できます。
###コードポイント(符号点)
通常、Unicode は U+ から始まるコードポイントと呼ばれるもので表され、U+10000 未満に非サロゲートペア、以上にサロゲートペアが当てられています。例えば、非サロゲートペアである「A」は U+0041、「あ」は U+3042、サロゲートペアである「?」は U+1D11E、「?」は U+2F81A、といった具合です。
プログラムで文字を扱うには、コードポイントではなくバイト列であることが望ましいです。非サロゲートペア、つまりコードポイントが U+10000 未満の文字なら数値そのまま、サロゲートペアなら以下の様な変換を噛ませることでバイト列を得ることができます。
#####コードポイントからバイト列
const int codepoint = …;
const unichar high = (codepoint - 0x10000) / 0x400 + 0xd800; // high surrogate
const unichar low = (codepoint - 0x10000) % 0x400 + 0xdc00; // low surrogate
#####バイト列からコードポイント
const unichar high = …; // high surrogate
const unichar low = …; // low surrogate
const int codepoint = ((high - 0xd800) * 0x400) + (low - 0xdc00) + 0x10000;
###実装
実装例です。for
文を回して characterAtIndex:
メソッドで文字を拾おうとすると、サロゲートペアかどうかお構いなく文字列を分割してしまうので、enumerateSubstringsInRange:options:usingBlock:
メソッドを使います。
@interface NSString (RemoveEmoji)
- (NSString*)removedEmojiString;
@end
@implementation NSString (RemoveEmoji)
- (NSString*)removedEmojiString {
NSMutableString* __block buffer = [NSMutableString stringWithCapacity:[self length]];
[self enumerateSubstringsInRange:NSMakeRange(0, [self length])
options:NSStringEnumerationByComposedCharacterSequences
usingBlock: ^(NSString* substring, NSRange substringRange, NSRange enclosingRange, BOOL* stop) {
const unichar high = [substring characterAtIndex: 0];
// Surrogate pair (U+1D000-1F77F)
if (0xd800 <= high && high <= 0xdbff) {
const unichar low = [substring characterAtIndex: 1];
const int codepoint = ((high - 0xd800) * 0x400) + (low - 0xdc00) + 0x10000;
[buffer appendString: (0x1d000 <= codepoint && codepoint <= 0x1f77f)? @"": substring];
// Not surrogate pair (U+2100-27BF)
} else {
[buffer appendString: (0x2100 <= high && high <= 0x27bf)? @"": substring];
}
}];
return buffer;
}
@end
とりあえず、除外する範囲を U+1D000-1F77F と U+2100-27BF としていますが、わりと適当に決めた範囲なので不足があると思います。絵文字の範囲は、正直よくわかりません。適宜変更してください(丸投げ)。
###その他
サロゲートペアは、上位サロゲートと下位サロゲートを並べて書式 @"%C%C"
で表示や生成ができます。
NSLog(@"%C%C", [@"?" characterAtIndex:0], [@"?" characterAtIndex:1]);
NSString* g_clef = [NSString stringWithFormat: @"%C%C", [@"?" characterAtIndex:0], [@"?" characterAtIndex:1]];
###終わりに
日本語版の Wikipedia に、コードポイントの分類などが非常に見やすくまとまっていたので、参照してみてください。
###追記
CocoaPods に登録しました。どうぞ、可愛がってやってください。