「文字数」ってなぁに？〜String, NSString, Unicodeの基本〜

# はじめに

この記事は、[第5回スタートアップiOS勉強会 - connpass](https://connpass.com/event/43260/)での同名の発表を文章形式でリライトしたものです。
スライド版(Speaker Deck)は[こちら](https://speakerdeck.com/takasek/wen-zi-shu-tutenaani-string-nsstring-unicodefalseji-ben)。


# `String` と `NSString`

## 「文字列の特定の単語に色をつけて、textViewに表示したい」

…という、ヒトなら誰しもが持つ欲望をさらけ出してください。
そう言われたあなたは、以下のようなコードを書くかもしれません。

```swift
func coloredText(from str: String, target: String) -> NSAttributedString {
    // 対象単語の出現範囲( Range<String.Index> )を取得
    let range: Range<String.Index> = str.range(of: target)!

    // Range<String.Index> を NSRange に変換
    let nsRange = NSRange(location: str.distance(from: str.startIndex,
                                                 to: range.lowerBound),
                          length: target.characters.count
    )

    // 対象単語に色をつけて返す
    let result = NSMutableAttributedString(string: str)
    result.addAttributes(
        [NSForegroundColorAttributeName: UIColor.red],
        range: nsRange
    )
    return result
}
```

この関数に `"日本チャチャチャ！"` と `"チャチャチャ"` を与えれば、

<img width="535" alt="no_emoji.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/23c46185-f088-45ee-b347-b223ca1c0c6a.png">

このように、「チャチャチャ」だけが赤く色づきました。
一見、うまく動いているコードに見えます。

しかし、これで安心してしまったあなたは、実は見えない罠に嵌まってます。
試しに、 `"日本チャチャチャ！"` に絵文字 `"🇯🇵"` を付け加えてみましょう。

<img width="226" alt="UITextView.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/b56e3aec-4e7b-5753-298e-a614618106cf.png">

「チャチャチャ」ではない部分が赤くなった上に、文字化けまで起こっています。
一体、何故こんなことが起きたのでしょうか。

それは、StringとNSStringの違いを理解せずにコードを書いてしまったからです。

## String と NSString 何が違う？

struct と class?
Swift と Obj-C?

いいえ。
それ以上に重要な違いがあります。

`NSString` は、

- 内部的には**UTF-16**でバイト列を保持
- UTF-16のバイト列を操作するためのAPIを提供

というデータ構造です。
それに対してSwiftの `String` は、

- 内部のバイト列は隠蔽
- 文字列操作のために、**書記素クラスタ**および**各種UnicodeのコードユニットのView**を提供している

というデータ構造です。
おわかりでしょうか。

…はい、よくわかりませんね。View?　書記素クラスタ??　各種Unicodeのコードユニット??? 🤔

これらの宇宙語を理解するためには、「**そもそもUnicodeとは何か**」を紐解く必要があります。

# Unicodeとは

## コード空間とコードポイント

https://ja.wikipedia.org/wiki/Unicode

>符号化文字集合や文字符号化方式などを定めた、文字コードの業界規格である。
>**文字集合が単一の大規模文字セットであること**（「Uni」という名はそれに由来する）などが特徴である。

Unicode規格は、21bitの整数値空間（**コード空間**)を提供しています。これは概念的な空間です。実際のバイトデータとは切り離して考えてください。
世界中のすべての文字は、この空間内のユニークな整数値へと割り当てられます（**符号化**）。
そして、割り当てられた値を、**コードポイント**と呼びます。

コードポイントは `U+(16進数)` で表現されます。

- BMP<sub>（基本多言語面）</sub> = `U+0000` 〜 `U+FFFF`
- SMP<sub>（追加面）</sub> = `U+10000` 〜

具体的には、こんな感じ👇ですね。

<img width="1324" alt="2016-11-13 20.06.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/7cdbcd9c-932d-6253-2441-f32003e5cba0.png">


## エンコーディング

### 21bitのコード空間を、どうやって実際のバイト列として表現するか？

Unicodeの「コード空間」「コードポイント」という概念については理解できました。
しかし、実際にこれをバイト列として表現するにあたっては、いくつか方法が考えられます。
この方法を **エンコーディング** と呼びます。
同じコードポイントでも、バイト列表現にはバリエーションがあるのです。

![](http://cdn-ak.f.st-hatena.com/images/fotolife/n/nobuoka/20140604/20140604234845.png)[^1]

[^1]: http://www.unicode.org/versions/Unicode6.2.0/ch02.pdf より

各エンコーディングの違いは、 **「特定のコードポイントを表現するための最小単位を何bitにするか」**の違いです。
そして、最小単位を **コードユニット** と呼びます。
`UTF-8` `UTF-16` `UTF-32`…という、この数字は、コードユニットが何bitになるかを示すものです。

コードポイントとコードユニットという単語、紛らわしいけど、よく覚えておいてくださいね。
コードポイントってのは、概念的なidみたいなもの。
コードユニットは、そのid自体がデカいので小分けにして詰め込むための、箱のサイズです。

それでは各エンコーディングを、詳しく見てみましょう。


### UTF-32

![image](https://qiita-image-store.s3.amazonaws.com/0/34405/cf9407dc-8d21-6491-c572-3c4a3afbcf31.png)

- 1コードユニット = 32bit(4バイト)
- 1コードポイント = 4バイトの固定長

UTF-32のコードユニットは、32bit。コード空間(21bit)より巨大です。そのため完全固定長で表現できる！というのがUTF-32の強みです。
たとえ1世紀後に宇宙文明とコンタクトして、収録しなきゃいけない文字が今の1000倍になったとしても、耐えきれるポテンシャルの持ち主です。
その分、データとしては無駄が大きいんですけどね。 `abc` と書くだけで12バイト。ヤバいですね。

### UTF-16

![image](https://qiita-image-store.s3.amazonaws.com/0/34405/b412e5ac-faaf-0230-4245-6b2f17ee1f63.png)

- 1コードユニット = 16bit(2バイト)
- 1コードポイント = 2バイト or 4バイトの可変長

実は、Unicode 1.0.0の頃は、このUTF-16の範囲内に世界中の文字が収まる想定でした。かつてコード空間は16bitだったのです。
しかし、想定外の事件が次々に起こり、コード空間は21bitに拡大を余儀なくされました。結果、UTF-16は**サロゲートペア**という苦し紛れの策を取ったのですが、それについては省略。
我々プログラマが憂慮すべきは、その頃生まれたプログラム言語が「Unicodeの最終形はUTF-16だ」と思い込んで設計されていることです。
Objective-Cも、例外ではありません。

### UTF-8

![image](https://qiita-image-store.s3.amazonaws.com/0/34405/b48185f0-e0a7-abdf-93a9-e79b1e7c9076.png)

- 1コードユニット = 8bit(1バイト)
- 1コードポイント = 1バイト〜4バイトの可変長

今回のトピック絡みではあまり説明することのないエンコーディング形式です。おじいちゃん(ASCIIコード)とも意気投合できる、良い奴ですよ。

## 抽象文字、書記素クラスタ

ここまで読まれたあなたは、「コードポイントって用語、分かりづらいな」と思われているかもしれません。
「要は『一文字』のことでしょ」と理解しているかもしれませんね。
けど、実は違うんです。

「一文字」は、Unicode界隈では**抽象文字(abstract character)**と呼ばれるそうです。
言い換えると、「カーソルキーの移動単位」と考えるといいかもしれません。

で。抽象文字と符号化文字は **多対多** の関係なのです。

![](http://cdn-ak.f.st-hatena.com/images/fotolife/n/nobuoka/20140604/20140604222944.png)

さらに、コードポイントにはそれ単体では成立せず、組み合わせて使うものもあります。
組み合わされて成立した抽象文字を **書記素クラスタ (grapheme cluster)** といいます。

![](http://re.buildinsider.net/language/csharpunicode/01/05.gif)

[Unicodeとは？ その歴史と進化、開発者向け基礎知識](http://www.buildinsider.net/language/csharpunicode/01)より


### 余談：グリフ(glyph)

グリフ(glyph)というまた別の概念もありまして、

- 文字の見た目（位置、サイズ）の情報
    - 文字は必ず左から右に流れるわけじゃないでしょ、ってこと
- CoreTextで管理される

…のだけど、今回は省略。

## ここまでのまとめ

Unicode自体の話は、これでおしまいです。
だいぶ用語が増えて混乱してきた頃合いでしょうから、一旦まとめましょう。

- 抽象文字の作りかた
    - **コード空間**<sub>(21bit)</sub>内に**コードポイント**があるので、
    - 割り当てられた符号化文字 <img width="45" alt="u0041.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/7d0e8034-272c-c47c-a841-c16a0f88ef02.png"> , <img width="44" alt="u030A.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/0a96078a-e306-3690-a837-68ede6eca98c.png"> を組み合わせて
    - **抽象文字** / **書記素クラスタ** <img width="43" alt="合字.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/2b0a929b-1cb7-bf3d-bffc-e40d75dee428.png"> を作る
- エンコーディングとは
    - 特定の **コードポイント** <sub>(最大21bit)</sub>を
    - 規定サイズの **コードユニット** <sub>(8bit / 16bit / 32bit)</sub> に詰め込む方法


# Unicode と `Swift.String` のAPI

ここまで、Unicodeとは何か、どのように文字が表現され、そこにエンコーディングがどう関わるかを説明してきました。
ここからは、Unicodeがどのように `Swift.String` のAPIで表現されているかを説明します。[^3]

[^3]: 正直、公式リファレンス( https://developer.apple.com/reference/swift/string )がとても分かりやすいのでそっち読むといいです。


`Swift.String` は、**書記素クラスタ**および**各種UnicodeのコードユニットのView**を提供している、と述べました。

- `var characters: String.CharacterView`
- `var unicodeScalars: String.UnicodeScalarView`
- `var utf16: String.UTF16View`
- `var utf8: String.UTF8View`

Stringは、これらの4つのViewを通して操作することができます。

※UTF-32はUnicodeのコード空間を内包するので、 `UnicodeScalarView` という名前

## 4つのViewを使ってみよう

![](http://cdn-ak.f.st-hatena.com/images/fotolife/n/nobuoka/20140604/20140604234845.png)

👆の図に登場する文字列 `"\u{41}\u{3A9}\u{8A9E}\u{10384}"` を各Viewで表現したとき、そのcountはいくつになるのか確かめてみましょう。

```swift
let str = "\u{41}\u{3A9}\u{8A9E}\u{10384}"

str.characters.count //4 👈書記素クラスタ
str.unicodeScalars.count // 4 👈UTF-32でのコードユニット数
str.utf16.count // 5 👈UTF-16でのコードユニット数
str.utf8.count // 10 👈UTF-8でのコードユニット数
```

図と出力結果の辻褄、合っていますね！

## `String.CharacterView` は 強い

書記素クラスタは、人間の考える「一文字」だと先程述べました。
つまり、「一文字」をカウントするのは `String.CharacterView` であると言えます。
このイケメンは、Unicodeの複雑な仕様を吸収してくれます。合字もいい感じに処理してくれます。

```swift
var cafe = "Cafe" // "Cafe"
cafe.characters.last // "e"
cafe.characters.count // 4

cafe += "\u{301}" // "Café"
cafe.characters.last // "é"
cafe.characters.count // 4
```

うーん強い。

## `String.CharacterView` は 常に最強ではない

…ただ、物によってはCharacterViewをもってしても「1文字」を認識できないこともあります。

```swift
"👩‍👩‍👧‍👦".characters.count // 4
```

何故でしょうか。
[Unicodeとは？ その歴史と進化、開発者向け基礎知識 - Build Insider](http://www.buildinsider.net/language/csharpunicode/01)から引用します。

>ただし、これらのルールや新しい文字は、Unicodeのバージョンアップに伴って随時追加されているわけで、実際にこれらが1文字として描画されるかどうかは環境依存である。OSがそのバージョンによって異なるのはもちろん、フォントによっても1文字で表示できるかどうかが変わる。結合文字などは古くからあり対応しているものも多いが、絵文字に関しては最近の仕様なのもあってかなりばらつきがある。

どれくらいバージョンアップがあるのかは、[Unicode - Wikipedia #各バージョンとその特徴](https://ja.wikipedia.org/wiki/Unicode#.E5.90.84.E3.83.90.E3.83.BC.E3.82.B8.E3.83.A7.E3.83.B3.E3.81.A8.E3.81.9D.E3.81.AE.E7.89.B9.E5.BE.B4)で確認できます。
結構頻繁ですね。

ということは、わかりますか？
**「文字数に従って云々してくれ」というアプリの仕様が提示されたときには、心して挑まなければなりませんよ**、ということです。

---

(追記 12/6)
ちなみに `"👩‍👩‍👧‍👦"` の正体については、丁度タイムリーに解説記事が上がっていました。

家族👨‍👩‍👦‍👦はreplaceされてしまうのか？あるいはZWJの話😂 - Qiita
http://qiita.com/todokr/items/8b813e14d3fdb4111cb7

---

いずれにせよ、SwiftのString APIはよく考えられています。
UTF-16が前提であるNSStringよりも、もっと本質的に「文字」を扱うことができるのが、 `Swift.String` のAPIであるといえます。

というわけで。
みなさん、もうNSStringのことはもう忘れて、今後は `Swift.String` と楽しく過ごして下さい💮

…。
…ってわけには、いかないんですよね。

# Foundationに根を下ろし、NSStringと共に生きよう

<img width="226" alt="UITextView.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/b56e3aec-4e7b-5753-298e-a614618106cf.png">

そもそもの話の発端は、 `NSAttributedString` と `UITextView` でした。
どんなに良く設計されたValueTypeを作っても、Cocoaから離れては生きられないのです。
現実を受け入れた上で、ではどう生きるかを考えましょう。

## NSStringは UTF-16

もう一度、バグを生んでいるコードを確認しましょう。

```swift
func coloredText(from str: String, target: String) -> NSAttributedString {
    // 対象単語の出現範囲( Range<String.Index> )を取得
    let range: Range<String.Index> = str.range(of: target)!

    // Range<String.Index> を NSRange に変換
    let nsRange = NSRange(location: str.distance(from: str.startIndex, to: range.lowerBound),
                          length: target.characters.count
    )

    // 対象単語に色をつけて返す
    let result = NSMutableAttributedString(string: str)
    result.addAttributes(
        [NSForegroundColorAttributeName: UIColor.red],
        range: nsRange
    )
    return result
}
```

`Range<String.Index> を NSRange に変換` している部分が、なんだか臭いますね。
`"🇯🇵"` に対する `String.characters` と `NSString` でカウントの方式の違いを確かめてみましょう。

```swift
let flag = "\u{1F1EF}\u{1F1F5}" // "🇯🇵"

flag.characters.count // 1 👈書記素クラスタは 1
flag.unicodeScalars.count // 2 👈コードポイント2個
flag.utf16.count // 4 👈2コードポイント × 2コードユニット
flag.utf8.count // 8 👈2コードポイント × 4コードユニット

(flag as NSString).length // 4 👈utf16.countと一致
```

NSStringの内部表現はUTF-16です。納得の結果ですね。

じゃあ、rangeを取得する際には、 `String.characters` のかわりに `String.utf16` を使えばいい？
いや、取得した範囲のRangeを構成する `String.Index` を `Int` に変換するのは手間ですよね？
そういうところでミスを犯したくないですよね？[^2]

いえいえ、もっとシンプルで分かりやすい解決策があるのです。

[^2]: あえてそういうデザインになっている理由を考えるにあたっては、[「Swift 3のStringのViewに対して、Intでsubscript出来ない理由 – Swift・iOSコラム – Medium」](https://medium.com/swift-column/swift-string-7147f3f496b1#.x1n3vrh1p)が参考になると思います。 

## NSStringを使わなきゃいけないなら、最初からもう、Cocoaの世界で処理を閉じたほうが良い

つまり、

```swift
let range = str.range(of: target)!

// Range<String.Index> を NSRange に変換
let nsRange = NSRange(
    location: str.distance(from: str.startIndex, to: range.lowerBound),
    length: target.characters.count
)
```

👆こう書くのではなく、
👇こう書けばいいのです。

```swift
let nsStr = NSString(string: str)
// 一度NSStringの世界に入れば…
let nsRange = nsStr.range(of: target)
// Range<String.Index>を介さないので、変換処理不要
```

<img width="537" alt="cocoaの世界.png" src="https://qiita-image-store.s3.amazonaws.com/0/34405/27fbe91b-f1e5-4145-54e8-2551b72e35cd.png">

範囲ズレも文字化けも解決し、コードも分かりやすくなりました :tada: 

# 今日の結論。

- `String` と `NSString` は別物
    - `Range<String.Index>` と `NSRange` も別物
    - Cocoa世界が関わる文字列操作は、必ず `NSString` のAPIを通しましょう
- あと、仕様に「文字数をカウントして云々」が出てきたら身構えましょう

以上、みなさん気をつけていきましょうね。

# 参考リンク

Unicode のサロゲートペアとは何か - ひだまりソケットは壊れない
http://vividcode.hatenablog.com/entry/unicode/surrogate-pair

なぜSwiftの文字列APIは難しいのか | プログラミング | POSTD
http://postd.cc/why-is-swifts-string-api-so-hard/

Unicodeとは？ その歴史と進化、開発者向け基礎知識 - Build Insider
http://www.buildinsider.net/language/csharpunicode/01

Unicodeと、C#での文字列の扱い - Build Insider
http://www.buildinsider.net/language/csharpunicode/02

Swift 3のStringのViewに対して、Intでsubscript出来ない理由 – Swift・iOSコラム – Medium
https://medium.com/swift-column/swift-string-7147f3f496b1#.x1n3vrh1p