NSStringでUnicodeを数値文字参照に変換する

2chはテキストエンコーディングとしてShift-JISを使っているので、Cocoa2chブラウザはNSStringの内部表現であるUnicodeとShift-JISの相互変換をする必要がある。Shift-JIS→UnicodeCocoa標準のAPIで出来るし、それで変換出来ない一部のShift-JISについてもText Encoding Converterを使えばなんとかなる*1
ではUnicode→Shift-JISはどうか。これもCocoa標準のAPIはあるのだが、Unicodeでは定義されているがShift-JISでは定義されていない文字が存在するために、それらを含むNSStringでは変換が失敗する。しかし2chでShift-JISに定義されていない文字は使えないのかというと、HTMLで定義されている「文字参照」を使えば少なくともWebブラウザ上ではそれらの文字が表示される。
ならば2chブラウザ文字参照を変換出来た方がいい。Thousandは表示にWebViewを使うので、表示には問題ないのだが、Shift-JISに無い文字を書き込むことは出来ない。これらの文字を自動的に文字参照に変換して書き込むことが出来ればより便利である。
というわけでCocoaでそんな変換を行うコードを書いてみた。このコードは、NSStringをあるエンコーディングに変換する前に、変換出来ない文字のみを数値文字参照に変換するもので、エンコーディング自体は変換しない。

-(NSString *)stringByAddingNumericCharacterReferencesForEncoding:(NSStringEncoding)encoding {
	NSMutableString *resultString = [NSMutableString string];
	NSMutableArray *parts= [NSMutableArray arrayWithObject:self];
	unsigned i=0;
	while (i<[parts count]) {
		NSString *part = [parts objectAtIndex:i];
		NSData *data = [part dataUsingEncoding:encoding];
		if (data && [data length] > 0) {
			[resultString appendString:part];
			i++;
		} else {
			unsigned length = [part length];
			if (length == 0) {
				i++;
			} else if (length == 1) {
				unichar character = [part characterAtIndex:0];
				NSString *numericCharacterReference = [NSString stringWithFormat:@"&#%d;", character];
				[resultString appendString:numericCharacterReference];
				i++;
			} else if (length == 2) {
				unichar high = [part characterAtIndex:0];
				unichar low = [part characterAtIndex:1];
				if (high >= 0xD800 && high <= 0xDBFF && low >= 0xDC00 && low <= 0xDFFF) {
					UInt32 character = (((high & 0x3FF) << 10) | (low & 0x3FF)) + 0x10000;
					NSString *numericCharacterReference = [NSString stringWithFormat:@"&#%d;", character];
					[resultString appendString:numericCharacterReference];
					i++;
				} else {
					NSString *string1 = [part substringToIndex:1];
					NSString *string2 = [part substringFromIndex:1];
					[parts removeObjectAtIndex:i];
					[parts insertObject:string1 atIndex:i];
					[parts insertObject:string2 atIndex:i+1];
				}
			} else {
				unsigned location = length / 2;
				NSString *string1 = [part substringToIndex:location];
				NSString *string2 = [part substringFromIndex:location];
				[parts removeObjectAtIndex:i];
				[parts insertObject:string1 atIndex:i];
				[parts insertObject:string2 atIndex:i+1];
			}
		}
	}
	return [[resultString copy] autorelease];
}

いつもながら大変汚い方法とコードである。何をやっているかと言うと、エンコーディング変換を試みて失敗した文字列はまず2分割し、それぞれで再度変換を試みる。これを繰り返して一文字まで絞り込んだら数値参照に変換するという、本末転倒そのものなコードである。だがしかし、数値参照が必要な文字の割合が低ければ、実用には足りるのかもしれない。
サロゲートペアの判別と変換には、

藤棚工房別棟 −徒然−: Cocoaで文字列処理
http://blogs.dion.ne.jp/fujidana/archives/891208.html#comments

で見つけた式を使用させていただいた。

*1:ThousandはここでSevenFourのコードを拝借している