Scalaで正規表現: ユニコードの空白文字29種類にマッチするか試してみた


入力から空白を削りたいといった要件がたまにあるが、空白と一言で言っても半角スペース・全角スペースだけでなく、ユニコードには様々な空白文字がある。調べた範囲では29種類あった。これらの空白文字を正規表現で一網打尽する方法はないか検証してみた。検証のために用いたコードは、この記事の最後に載せてある。

# 検証結果

検証結果は下の表にまとめた。各行が空白文字に、各列が正規表現になっている。その文字がその正規表現でマッチしたものには「o」を付けた。

|  code | name | `\p{javaWhitespace}` | `\p{javaSpaceChar}` | `\p{Zs}` | `\s` | `\p{Blank}` | 見た目 |
|--------|-----------------------|:--:|:--:|:--:|:--:|:--:|---------------|
| \u0009 | HORIZONTAL TABULATION | o |  |  | o | o | `foo`\u0009`bar` |
| \u000A | LINE FEED | o |  |  | o |  | `foo`\u000A`bar` |
| \u000B | VERTICAL TABULATION | o |  |  | o |  | `foo`\u000B`bar` |
| \u000C | FORM FEED | o |  |  | o |  | `foo`\u000C`bar` |
| \u000D | CARRIAGE RETURN | o |  |  | o |  | `foo`\u000D`bar` |
| \u001C | FILE SEPARATOR | o |  |  |  |  | `foo`\u001C`bar` |
| \u001D | GROUP SEPARATOR | o |  |  |  |  | `foo`\u001D`bar` |
| \u001E | RECORD SEPARATOR | o |  |  |  |  | `foo`\u001E`bar` |
| \u001F | UNIT SEPARATOR | o |  |  |  |  | `foo`\u001F`bar` |
| \u0020 | SPACE | o | o | o | o | o | `foo` `bar` |
| \u00A0 | NO-BREAK SPACE |  | o | o |  |  | `foo` `bar` |
| \u1680 | OGHAM SPACE MARK | o | o | o |  |  | `foo` `bar` |
| \u180E | MONGOLIAN VOWEL SEPARATOR | o | o | o |  |  | `foo`᠎`bar` |
| \u2000 | EN QUAD | o | o | o |  |  | `foo` `bar` |
| \u2001 | EM QUAD | o | o | o |  |  | `foo` `bar` |
| \u2002 | EN SPACE | o | o | o |  |  | `foo` `bar` |
| \u2003 | EM SPACE | o | o | o |  |  | `foo` `bar` |
| \u2004 | THREE-PER-EM SPACE | o | o | o |  |  | `foo` `bar` |
| \u2005 | FOUR-PER-EM SPACE | o | o | o |  |  | `foo` `bar` |
| \u2006 | SIX-PER-EM SPACE | o | o | o |  |  | `foo` `bar` |
| \u2007 | FIGURE SPACE |  | o | o |  |  | `foo` `bar` |
| \u2008 | PUNCTUATION SPACE | o | o | o |  |  | `foo` `bar` |
| \u2009 | THIN SPACE | o | o | o |  |  | `foo` `bar` |
| \u200A | HAIR SPACE | o | o | o |  |  | `foo` `bar` |
| \u200B | ZERO WIDTH SPACE |  |  |  |  |  | `foo``bar` |
| \u202F | NARROW NO-BREAK SPACE |  | o | o |  |  | `foo` `bar` |
| \u205F | MEDIUM MATHEMATICAL SPACE | o | o | o |  |  | `foo` `bar` |
| \u3000 | IDEOGRAPHIC SPACE | o | o | o |  |  | `foo`　`bar` |
| \uFEFF | ZERO WIDTH NO-BREAK SPACE |  |  |  |  |  | `foo``bar` |

* 全角スペースと言われるものはこの表では `\u3000` になる。
* \u001F 以下の文字はMarkdownで表示しきれないものがあるので見た目には反映していない。

`\p{javaWhitespace}` は `java.lang.Character` の `isWhitespace` と同じものらしいが、一番多く空白文字にマッチした。それでも、マッチしない文字もあったので、`\p{javaWhitespace}`とユニコードエスケープを組み合わせた正規表現を作る必要がありそう。

全角スペースと半角スペースとタブくらいにマッチすればいい要件なら、`\p{javaSpaceChar}` や `\p{Zs}` で十分。


# 検証用コード

```scala
import org.scalatest._

class CompilationSpec extends FeatureSpec with GivenWhenThen with BeforeAndAfter with ParallelTestExecution with Matchers {
  feature("Playground") {
    scenario("test \\p{javaSpaceChar}") {
      def isS(char: String) = """\s""".r.findFirstIn(char).nonEmpty
      def isZs(char: String) = """\p{Zs}""".r.findFirstIn(char).nonEmpty
      def isJavaSpaceChar(char: String) = """\p{javaSpaceChar}""".r.findFirstIn(char).nonEmpty
      def isJavaWhitespace(char: String) = """\p{javaWhitespace}""".r.findFirstIn(char).nonEmpty
      def isBlank(char: String) = """\p{Blank}""".r.findFirstIn(char).nonEmpty

      implicit class MyBoolean(bool: Boolean) {
        def o: String = {
          bool match {
            case true  => "o"
            case false => ""
          }
        }
      }

      val unicodeSpaces = Seq(
        Seq("\u0009", "HORIZONTAL TABULATION"),
        Seq("\u000A", "LINE FEED"),
        Seq("\u000B", "VERTICAL TABULATION"),
        Seq("\u000C", "FORM FEED"),
        Seq("\u000D", "CARRIAGE RETURN"),
        Seq("\u001C", "FILE SEPARATOR"),
        Seq("\u001D", "GROUP SEPARATOR"),
        Seq("\u001E", "RECORD SEPARATOR"),
        Seq("\u001F", "UNIT SEPARATOR"),
        Seq("\u0020", "SPACE"),
        Seq("\u00A0", "NO-BREAK SPACE"),
        Seq("\u1680", "OGHAM SPACE MARK"),
        Seq("\u180E", "MONGOLIAN VOWEL SEPARATOR"),
        Seq("\u2000", "EN QUAD"),
        Seq("\u2001", "EM QUAD"),
        Seq("\u2002", "EN SPACE"),
        Seq("\u2003", "EM SPACE"),
        Seq("\u2004", "THREE-PER-EM SPACE"),
        Seq("\u2005", "FOUR-PER-EM SPACE"),
        Seq("\u2006", "SIX-PER-EM SPACE"),
        Seq("\u2007", "FIGURE SPACE"),
        Seq("\u2008", "PUNCTUATION SPACE"),
        Seq("\u2009", "THIN SPACE"),
        Seq("\u200A", "HAIR SPACE"),
        Seq("\u200B", "ZERO WIDTH SPACE"),
        Seq("\u202F", "NARROW NO-BREAK SPACE"),
        Seq("\u205F", "MEDIUM MATHEMATICAL SPACE"),
        Seq("\u3000", "IDEOGRAPHIC SPACE"), // 全角スペース
        Seq("\uFEFF", "ZERO WIDTH NO-BREAK SPACE")
      )

      unicodeSpaces.foreach {
        case Seq(space, name) =>
          val code = space.charAt(0).toInt.formatted("\\u%04X")
          println(List(code, name, isJavaWhitespace(space).o, isJavaSpaceChar(space).o, isZs(space).o, isS(space).o, isBlank(space).o, "`foo`" + ({ space.charAt(0).toInt match { case a if a <= 31 => code; case a => space; } }) + "`bar`").mkString("| ", " | ", " |"))
      }

    }
  }
}
```

# 参考文献

* [Unicode spaces](https://www.cs.tut.fi/~jkorpela/chars/spaces.html)
* [Character (Java Platform SE 7 )](http://docs.oracle.com/javase/7/docs/api/java/lang/Character.html#isWhitespace(char))
* [Pattern (Java Platform SE 7 )](http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html)
* [Scala Standard Library 2.11.1 - Regex - Scala Standard Library 2.11.1 - scala.util.matching.Regex](http://www.scala-lang.org/api/2.11.1/index.html#scala.util.matching.Regex)