Unicode のスペース関連の文字

ワケあって Unicode のスペース文字について調べる必要があった筆者が、調べた内容をまとめたものである。

## この記事を書いたきっかけ

最近コード書いている中で、スペース文字を検出する必要がある場面に遭遇した。ユーザが与えたテキストにスペースが含まれているとバリデーションエラーを出力するというものだ。
Unicode にはスペースに関する文字は結構存在する。スペースとしてよく見る半角スペースや全角スペースだけを検出するように実装していると、変なスペースがやってきた時にすり抜けてしまうかもしれない。
こうした変なスペースにも対応して、引っ掛ける必要があるのだが、そもそもどこまでを引っ掛けた方がよくわかっていなかったので、詳しいことは分かっていなかったので改めてスペースについて調べることにした。

## 文字の一覧

以下は個人的な見解で Unicode 文字のうち、スペースに関連する文字を幾つかのグループに分けて説明している。
主に Wikipedia の [Whitespace character](https://en.wikipedia.org/wiki/Whitespace_character) の記事を参考にまとめている。

### 半角文字として使われるスペース

#### SPACE (SP)

* Unicode コードポイント: `U+0020` (Basic Latin)
* Unicode のカテゴリ: Separator, space

最も一般的に使われている、半角文字としてのスペースである。

#### NO-BREAK SPACE (NBSP)

* Unicode コードポイント: `U+00A0` (Latin-1 Supplement)
* Unicode のカテゴリ: Separator, space
* HTML 実体参照: `&nbsp;`

通常折り返しを行うテキストでは、上記のスペース文字が行末に達すると折り返しが行われる。
しかし、ノーブレークスペースでは折り返しが発生しない。

##### macOS での NBSP

macOS では Option + スペース と押下することで NBSP が入力される。

そのお陰でターミナル上でスペース文字を入力する際には事故が起こりやすい。例えば次のようなスペース文字を含むファイルを扱う場合である。
```shell
ls My\ Directory\ Containing\ Space\ Char
```
`\` は Option + | で入力されるため、 `\` を打った直後に Option をうっかり離さずにスペースを押してしまうと NBSP になってしまう。なので (SP と見分けがつかない) NBSP が名前に入ったファイルが生成されてしまう。 (ということを何回かやらかしたことがある)

最近では Option + スペース が ChatGPT の Chat Bar を呼び出すキーボードショートカットにデフォルトで設定されているので、 Option + スペース で NBSP が入力される代わりに ChatGPT の入力ボックスが表示されるようだ。

#### HTML での SP と NBSP

上記の通り、 HTML では `&nbsp;` で NBSP がレンダリングされます。しかし、 SP に対応する実体参照は存在しない。 (Unicode の `&#x20;` を除く) 単純に SP の文字を入力すればそれが SP としてレンダリングされるからだろうか。
また、 HTML ソース中で (`pre` でない場合) 複数個 SP が含まれているとされると1つにまとめられるが、 NBSP は1つにはまとめられず、そのままレンダリングされる。

### 様々な幅のスペース

#### 文字の名前にスペースの大きさが含まれているスペースの一覧

| サンプル | 幅 | 名称 | Unicode | HTML |
|:--:|:--:|:--:|:--:|:--:|
| \|&#x2003;\| | 1 em | EM SPACE | `U+2003` | `&emsp;` |
| \|&#x2002;\| | 1 en = 1/2 em | EN SPACE | `U+2002` | `&ensp;` |
| \|&#x2004;\| | 1/3 em | THREE-PER-EM SPACE<br>THICK SPACE | `U+2004` | `&emsp13;` |
| \|&#x2005;\| | 1/4 em | FOUR-PER-EM SPACE<br>MID SPACE | `U+2005` | `&emsp14;` |
| \|&#x2006;\| | 1/6 em | SIX-PER-EM SPACE | `U+2006` | - |
| \|&#x2008;\| | 句読点幅 | PUNCTUATION SPACE | `U+2008` | `&puncsp;` |
| \|&#x2009;\| | 1/5 ~ 1/6 em | THIN SPACE | `U+2009` | `&thinsp;` |
| \|&#x200A;\| | 1/6 em より細め | HAIR SPACE | `U+200A` | `&hairsp;` |

* これらはいずれも General Punctuation グループに属し、 Separator, space として扱われる。
* 1 em はフォントサイズと等しいため、 EM SPACE はほぼ正方形になると考えられる。

#### MEDIUM-MATHEMATICAL SPACE (MMSP)

* Unicode コードポイント: `U+205F` (General Punctuations)
* Unicode のカテゴリ: Separator, space
* HTML 実体参照: `&MediumSpace;`

横幅として大体 4/18 em 程度になるとされているスペースである: |&#x205F;|
その名の通り、数式中の記号や文字の間に入るスペースの幅とされている。

### 幅があるノーブレークスペース

ノーブレークスペースとしては NBSP (`U+00A0`) が存在するが、他にもノーブレークスペースとなる文字がある。

#### FIGURE SPACE / NUMERIC SPACE

* Unicode コードポイント: `U+2007` (General Punctuations)
* Unicode のカテゴリ: Separator, space
* HTML 実体参照: `&numsp;`

日本語で図形間隔と呼ばれる文字である。
数字の1桁の幅とされている。等幅フォントなら厳密に1桁の幅として決められるが、そうでない場合は数字によって横幅は変わるのに何を基準にこの幅が決められているのでしょうか。

#### NARROW NO-BREAK SPACE (NNBSP)

* Unicode コードポイント: `U+202F` (General Punctuations)
* Unicode のカテゴリ: Separator, space

モンゴル語以外の環境では Thin Space (U+2009) と同じ幅 (1/5 ~ 1/6 em) を持つ。
SI 単位系では、単位を持った値を示す際に、値と単位の間を隔てるように NNBSP を挿入することが推奨されているようだ。

### 様々なスペース文字

#### IDEOGRAPHIC SPACE

* Unicode コードポイント: `U+3000` (CJK Symbols and Punctuation)
* Unicode のカテゴリ: Separator, space

日本語で「和字間隔」と呼ばれる、全角スペースである。日本語の文章中で用いられるスペースは基本的にこの文字である。

#### OGHAM SPACE MARK

* Unicode コードポイント: `U+1680` (Ogham)
* Unicode のカテゴリ: Separator, space

中世アイルランドで普及していた[オガム文字](https://ja.wikipedia.org/wiki/オガム文字)におけるスペースであり、 Unicode でもスペース記号の1種として扱われている。
他のスペースとは異なり、横一文字で描画される: &#x1680;
オガム文字は基準となる横線上に縦線などを引くことにより構成される文字体系である。何も縦線を引かない状態がスペースに該当し、単に横線のみが描画されたのがこの OGHAM SPACE MARK である。

オガム文字による文の一例 ([Wikipedia](https://en.wikipedia.org/wiki/Ogham_inscription#Ireland) より引用)
```
᚛ᚂᚔᚓ ᚂᚒᚌᚅᚐᚓᚇᚑᚅ ᚋᚐᚉᚉᚔ ᚋᚓᚅᚒᚓᚆ᚜
```

### 組版目的のスペース

幅のスペースのうち、現在は改めて使う必要性のないスペースについてここに挙げる。

| サンプル | 幅 | 名称 | Unicode |
|:--:|:--:|:--:|:--:|
| \|&#x2001;\| | 1 em | EM QUAD | `U+2001` |
| \|&#x2000;\| | 1 en = 1/2 em | EN QUAD | `U+2000` |

これらはそれぞれ EM SPACE (`U+2003`), EN SPACE (`U+2002`) と同等に扱われており、通常は EM SPACE や EN SPACE を使うことが推奨されている。

ではなぜ EM SPACE と EM QUAD という別々の文字に分けられているのか? (EN SPACE と EN QUAD も同様)
これには歴史的経緯が関わっているようだ。

[とある Unicode のメールアーカイブ](https://unicode.org/mail-arch/unicode-ml/Archives-Old/UML023/0481.html) にこのことが書かれていた。
現代では2つの文字の区別は意味をなしていないが、元々はスペースの横幅の取り方の違いから区別していたようだ。2つの方法について、このページでは EM SPACE に対するものを linear measure (線的計測) 、 EM QUAD に対するものを square measure (面的計測) と呼んでいる。

* square measure では、フォントサイズと同じ1辺を持つ正方形の活字を挿入することに相当する。そのため、横幅は常に一定である。
* linear measure では、活字は挿入せずに適切な大きさのスペースを空けていた (組版では金属のバーを使っていたという) ことに相当し、スペースはフォントサイズと同じ幅にする。なので横幅は基本的に同じである。

ということで、元々のスペースの取り方に違いはあったものの結局とる幅は同じになっている。 Unicode 1.0 の時点でついうっかり別々にコートポイントを充当してしまったようだ。後に同等に扱うように決めたようだ。

### スペースらしきもの

文字の一覧の最後には、 Unicode ではスペースだとは規定されていないものの、スペースと関連しているので取り上げる

#### MONGOLIAN VOWEL SEPARATOR (MVS)

* Unicode コードポイント: `U+180E` (Mongolian)
* Unicode のカテゴリ: Other, Format

モンゴル語等で見られる母音調和の特性を、この文字を挟むことで抑制することができる。 (らしい)
スペースとしての幅を取る文字ではないが、 Unicode における `White_Space` のプロパティが設定されているようなので、稀にスペース文字として扱われることがある。

#### ZERO WIDTH SPACE (ZWSP)

* Unicode コードポイント: `U+200B` (General Punctuation)
* Unicode のカテゴリ: Other, Format
* 実体参照: `&ZeroWidthSpace;`

コードポイントを踏まえると EN SPACE ~ HAIR SPACE の流れの次であり、幅を小さくしていった極限のように思えるが、 Unicode のカテゴリではスペース扱いではない。
幅がないので隠し文字である。どうやらスペースで区切られていない文字列中で、単語の境界を表すのに使うようだ。テキストを折り返す際に、この位置で折り返すことができるようになる。
以下のように PascalCase や camelCase で表記する場合に単語の途中で ZWSP 挿入した方がいいということだと思われる。

```php
// 使わない例
LoremIpsumDolorSitAmet
// 使う例
Lorem&#x200B;Ipsum&#x200B;Dolor&#x200B;Sit&#x200B;Amet
```

#### ZERO WIDTH NON-BREAKING SPACE (ZWNBSP)

* Unicode コードポイント: `U+FEFF` (Arabic Presentation Forms-B)
* Unicode のカテゴリ: Other, Format

元々は先ほどの ZWSP とは逆で、この文字がある部分では改行が起こらなくなるという隠し文字だ。
しかし UTF-16 のバイトオーダーマーク (ファイル先頭に配置され、ファイルが Unicode 形式でエンコーディングされていることを示す文字列) としても使われているため、役割を明確にするために後述の WJ が用意されたようだ。

#### WORD JOINER (WJ)

* Unicode コードポイント: `U+2060` (General Punctuation)
* Unicode のカテゴリ: Other, Format
* 実体参照: `&NoBreak;`

ZWSP とは逆で、この文字がある部分では改行が起こらなくなるという隠し文字だ。
日本語のように単語間がスペースで区切られていない文字列で、単語の途中であることを示すのに使えそうだという。
(ならば全ての単語の途中箇所で WJ を入れなくてはならないのでは? 逆に単語間に ZWSP を入れた方がいいのでは?)
ZWSP は単語を区切るのでスペースと言えなくもないが、こちらは明らかにスペースではないだろう。

## オマケ: LaTeX での表記

LaTeX には様々なスペースを空ける表記があるが、それぞれ対応している文字が異なるようだ。
LaTeX を書くときにこうしたスペース文字との対応があるとは考えていなかったので、本筋とか関係がないが、重要そうである。

| LaTeX での表記 | 対応するスペース |
|:--:|:--:|
| `\ ` | SP |
| `~` | NBSP |
| `\quad` | EM SPACE |
| `\enspace` | EN SPACE |
| `\;` | THREE-PER-EM SPACE |
| `\:` | MEDIUM MATHEMATICAL SPACE |

## 様々なスペースの判断基準

ここまでで様々なスペースに類する文字を紹介してきた。次に幾つかの例でどの文字をスペースと判断しているか、見ていくことにする。

### Unicode の Separator, space カテゴリ

既に上記で示したように、「スペースらしきもの」に該当しない文字は全てこのカテゴリに属する。

### JavaScript で空白として扱われる文字

JavaScript の構文中ではそこかしこにスペースが現れるが、これは必ずしも SP である必要はないそうだ。

[ECMAScript 5.1](https://es5.github.io/#x7.2)のページによれば JavaScript では以下の文字がスペースと見做されるようだ。

> Tab, Vertical Tab, Form Feed, Space, No-break space, Byte Order Mark, Any other Unicode “space separator”

これを書き直すと
* `\t` (タブ), `\v` (垂直タブ), `\f` (フォームフィード)
* SP, NBSP
* 様々な幅のスペース (EM SPACE ~ HAIR SPACE & MMSP)
* 図形間隔, NNBSP, 和字間隔, オガム文字のスペース
* EM QUAD, EN QUAD
* ZWNBSP (BOM)

つまりここで紹介したもののうち、「スペースらしきもの」以外と、「スペースらしきもの」のうち ZWNBSP が該当するようだ。

### 正規表現のスペースの取り扱い

正規表現ではスペース文字にマッチする `\s` という表記がある。この `\s` がマッチする文字の種類にも正規表現ライブラリごとに幾つかのバリエーションがあるようだ。
ここでは公式ドキュメントに書かれている限りで、 `\s` がマッチする対象ごとにカテゴライズする。

#### スペースのみに対応する場合

公式ドキュメントにおいて、 `\s` は `[ \t\r\n\f\v]` と等しいと定義されているライブラリが存在する。
当然この場合は全角スペースや NBSP にはマッチしないので、マッチさせたければ手動で正規表現を記す必要がある。

* PCRE (ref: [pcre2syntax](https://www.pcre.org/current/doc/html/pcre2syntax.html#TOC1))
	* 以下の例も大抵は PCRE か Perl の正規表現に準拠していると書かれている
* Ruby の `RegExp` (ref: [Ruby 3.4 公式ドキュメント](https://docs.ruby-lang.org/ja/3.4/doc/spec=2fregexp.html#string))
* PHP (ref: [Escape sequences](https://www.php.net/manual/en/regexp.reference.escape.php))
	* 但し、ロケール設定によっては NBSP とかも入る可能性があるという
* Go の `regexp` (ref: [`regexp/syntax`](https://pkg.go.dev/regexp/syntax))
* Erlang (ref: [`re`](https://www.erlang.org/doc/apps/stdlib/re.html#module-backslash))
	* 但し、ロケール設定によっては NBSP とかも入る可能性があるという
* Nim の `std/re` (ref: [Generic character types](https://nim-lang.org/docs/re.html#regular-expression-syntax-and-semantics-generic-character-types))
* Wolfram Language の [Regular Expressions](https://reference.wolfram.com/language/tutorial/StringsAndCharacters.html#16225) (ref: [`WhitespaceCharacter`](https://reference.wolfram.com/language/ref/WhitespaceCharacter.html))
* GNU Regex (ref: [The Backslash Character](https://www.gnu.org/software/gnulib/manual/html_node/The-Backslash-Character.html))
	* POSIX の `[:space:]` と同じと書かれているので、おそらくこの通りである。

#### `\s` と `\h` に分かれている場合

`\s` としては先程と同様に `[ \t\r\n\f\v]` と定義されているが、代わりに「水平方向の空白文字」にマッチする表記として `\h` が用意されている場合である。
`\h` は通常以下の文字にマッチする
* `\t` (タブ)
* SP, NBSP
* 様々な幅のスペース (EM SPACE ~ HAIR SPACE & MMSP)
* 図形間隔, NNBSP, 和字間隔, オガム文字のスペース
* EM QUAD, EN QUAD
* MVS

つまり、ここで紹介した「スペースらしきもの」以外と、「スペースらしきもの」のうち、 MVS にのみマッチするようだ。

このような `\s` と `\h` が用意されている例として以下が挙げられる。

* Java の `java.util.regex.Pattern` (ref: [Java 21 公式ドキュメント](https://docs.oracle.com/javase/jp/21/docs/api/java.base/java/util/regex/Pattern.html))
	* `\p{Space}` は `\s` と同じだそう

#### `\s` が幅広いスペース関連の文字に対応している場合

`\h` は存在しておらず、 `\s` 単体で幅広いスペース文字にマッチできる例である。

例えば JavaScript の `RegExp` における `\s` に関しては [MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Regular_expressions/Character_class_escape) によれば、上記の「 JavaScript で空白として扱われる文字」と同じく 「スペースらしきもの」以外 + ZWNBSP である。

Rust の `regex` クレートにおいては、[ドキュメント](https://docs.rs/regex/1.11.1/regex/#perl-character-classes-unicode-friendly)によれば `\s` は Unicode の `White_Space` カテゴリに等しく、以下の文字にマッチする。

* `\t` (タブ), `\v` (垂直タブ), `\f` (FF), `\n` (LF), `\r` (CR)
* NEW LINE (U+0085), LINE SEPARATOR (U+2028), PARAGRAPH SEPARATOR (U+2029)
* SP, NBSP
* 様々な幅のスペース (EM SPACE ~ HAIR SPACE & MMSP)
* 図形間隔, NNBSP, 和字間隔, オガム文字のスペース
* EM QUAD, EN QUAD

つまり「スペースらしきもの」以外である
正規表現ライブラリ「鬼車」も [`doc/RE`](https://github.com/kkos/oniguruma/blob/master/doc/RE) を見る限り同じ基準になっている。

## 自分でスペースかどうかを判定する場合

では、与えられた文字がスペースかを判定するための処理を自分で実装する場合、どの文字をスペースだとみなすべきだろうか。
JavaScript や Python などの正規表現が「よく整備された」言語であれば、 `\s` にマッチするものが存在するか否かで片付けても良さそうだが、 `\s` が U+0020 にしかマッチしない言語を使うかもしれないので、手動実装する場合を念頭に考えておいても良いだろう。

ここまでのことを踏まえると、まず間違いなく Unicode で Separator, Space 扱いになる以下の文字はマッチさせても良いだろう。

* SP, NBSP
* 様々な幅のスペース (EM SPACE ~ HAIR SPACE & MMSP)
* 図形間隔, NNBSP, 和字間隔, オガム文字のスペース
* EM QUAD, EN QUAD

あとは環境によっては偶にマッチする ZWNBSP や MVS をマッチさせるか否かである。スペースとしての使用事例が少ないためあってもなくても大して問題にはならなさそうだけど、バリデーションなら入れておいてもいいだろう。