Ruby の正規表現で空白文字は (?u:\s) と書ける

スペースやタブ，改行などをまとめて「空白文字」と呼ぶことがある。
何を空白文字とするかは場合によりけりなので，技術文書を読むときはその文書での定義をきちんと把握する必要がある。

この記事では，全角スペースや U+2029 PARAGRAPH SEPARATOR といったものまで含めた広い意味の空白文字を Ruby の正規表現でどう表すのか，というテーマを扱う。
（「U+2029」のようなものは Unicode のコード番号を表している）

なお，話を単純にするため，文字列のエンコーディングは UTF-8 に限定する。

本記事の内容は慎重に調べたつもりだが，この方面の専門家ではないので，100% 正しいという確信はありません。誤りを見つけられましたらぜひご指摘ください。

# `\s`

空白文字の正規表現といえばまず思い浮かぶのが `\s`。
「s」はきっと space の頭文字に由来するのだろう（知らんけど）。

ところが，こいつは全角スペース（U+3000）にはマッチしない。
どういうことかというと，`\s` は ASCII の範囲，つまり U+0000〜U+007F の範囲の空白文字だけを表す文字クラスなのだ。

具体的には以下のものが該当する。

* U+0020 SPACE
* U+0009 CHARACTER TABULATION（`\t`）
* U+000A LINE FEED（`\n`）
* U+000B LINE TABULATION（`\v`）
* U+000C FORM FEED（`\f`）
* U+000D CARRIAGE RETURN（`\r`）

（ ）内に示したのは当該文字のバックスラッシュ記法。

通称（の例）は上から順に，半角スペース，タブ，改行（LF），垂直タブ，改ページ，復帰（CR）。

Unicode には非 ASCII 部分に多様な空白文字類が存在するが，それらが全く拾えない。

# `[[:space:]]`

Unicode 全体の空白文字を拾うには `[[:space:]]` という正規表現が使える。

```rb
str = "あ　う\tえ\n"
p str.scan(/[[:space:]]/) # => ["　", "\t", "\n"]

# 「あ」のあとには全角スペースを入れている
```

`[ ]` が二重になっているのが，慣れないと分かりにくいが，外側の `[ ]` は普通の文字クラスの `[ ]` だ。
つまり，`[0-9]` なんかの `[ ]` と同じもの。

そしてその中に書かれている `[:space:]` は，「POSIX ブラケット表現」と呼ばれるものの一つ。
POSIX ブラケット表現には，他にも，制御文字を表す `[:cntrl:]` なんかがある。
これらは文字クラスの `[ ]` の中に書く必要がある。「空白文字や制御文字」を表したいなら

```
[[:space:][:cntrl:]]
```

のように書くことになる。

`[[:space:]]` に当てはまる空白文字は
https://docs.ruby-lang.org/ja/3.2/doc/spec=2fregexp.html
によれば

> Space_Separator | Line_Separator | Paragraph_Separator | 0009 | 000A | 000B | 000C | 000D | 0085

だそうだ。

このうち，U+0009〜U+000D は `\s` にも含まれている。
また，U+0020 は後述のように「Space_Separator」に含まれているので，`[[:space:]]` が `\s` を完全にカバーしているのは間違いない。

したがって，

* Space_Separator
* Line_Separator
* Paragraph_Separator 
* U+0085

が理解できれば，`[[:space:]]` が完全に把握できたことになる。

## Space_Separator

Space_Separator は Unicode で「一般カテゴリー」と呼ばれるものの一つ。
ええと，たぶんここを見ればいいんだと思う。
https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=%5Cp%7BGeneral_Category%3DSpace_Separator%7D

それによると，以下のものが該当するようだ。

* U+0020 SPACE
* U+00A0 NO-BREAK SPACE
* U+1680 OGHAM SPACE MARK
* U+2000 EN QUAD
* U+2001 EM QUAD
* U+2002 EN SPACE
* U+2003 EM SPACE
* U+2004 THREE-PER-EM SPACE
* U+2005 FOUR-PER-EM SPACE
* U+2006 SIX-PER-EM SPACE
* U+2007 FIGURE SPACE
* U+2008 PUNCTUATION SPACE
* U+2009 THIN SPACE
* U+200A HAIR SPACE
* U+202F NARROW NO-BREAK SPACE
* U+205F MEDIUM MATHEMATICAL SPACE
* U+3000 IDEOGRAPHIC SPACE

これを見ると 1/6 スペースだとか分割不可スペース，数字幅スペースといったものや，オガム文字という馴染みの薄い文字体系で使うらしきスペースなど，多様なスペースがあることが分かる。
もちろん，我らが（俗称）半角スペースや全角スペースも含まれている。

Space_Separator のみの文字クラスは `\p{Space_Separator}` とか `\p{Zs}` と書ける。

## Line_Separator

Line_Separator も一般カテゴリーの一つなのだが，含まれる文字は

* U+2028 LINE SEPARATOR

だけである。「行分割子」という訳語があるようだ。

## Paragraph_Separator

Paragraph_Separator も同様で，

* U+2029 PARAGRAPH SEPARATOR

のみが該当する。「段落分割子」という訳語があるらしい。

## U+0085

この文字は

* U+0085 NEXT LINE

という制御文字なのだが，よく知らない。

# `(?u:\s)`

`(?u: )` という書き方は，馴染みのない方も多いと思う。
これは「オプション」というものの一種。

`u` オプションは，`\s` や `\d` といった文字クラスの範囲を Unicode 全体に広げる働きをする。

オプションには二つの記法がある。`u` オプションを例に取ると，

* `(?u:` と `)` の間に正規表現を入れて使う
* `(?u)` の形で正規表現中に挿入する

前者の場合，中に入れた正規表現中の文字クラスだけが影響を受ける。
後者の場合，これを挿入した箇所以降の文字クラスが影響を受ける。

前者を使って `(?u:\s)` とすれば `[[:space:]]` と同じ意味になる。

`(?u)` を使った場合，途中から元に戻すには `(?-u)` と書く。

だから，たとえば「ASCII 限定の空白文字」→「Unicode 全体の空白文字」→「ASCII 限定の空白文字」という連なりを検索する正規表現は

* `\s(?u:\s)\s`
* `\s(?u)\s(?-u)\s`

と書けるわけだ。

余談になるが，数字の文字クラス `\d` についても同様で，単に `\d` と書けば ASCII の範囲の `0`〜`9` しか拾えないが，`(?u:\d)` とか `(?u)\d` などと書けば，我らが全角数字は言うに及ばず，アラビア文字の数字（١٢٣ など）やビルマ文字の数字（၁၂၃ など）といったものまで拾える。
ただし，漢数字（一二三）は漢字扱いであり，対象外である。