Swift Digest
SE-0221 | Swift Evolution

Character Properties

Proposal
SE-0221
Authors
Michael Ilseman, Tony Allevato
Review Manager
Ben Cohen
Status
Implemented (Swift 5.0)

01 何が問題だったのか

Swift の StringCharacter(拡張書記素クラスタ、extended grapheme cluster)のコレクションです。Character は Swift の入門時にも、スクリプトやテキスト処理で慣れた開発者が触れるときにも、きわめて早い段階で登場する型です。

ところが Character 自身が提供する機能は乏しく、他の Character との順序比較と、構成要素である Unicode スカラ値へのアクセスくらいしかありませんでした。実用上よく必要になる「この文字は数字か?」「空白か?」「改行か?」「アルファベットか?」といった素朴な問い合わせを行うには、いちいち構成スカラを取り出して Unicode.Scalar.Properties を調べる必要があります。

// 改行かどうかを知りたいだけなのに、スカラ単位で調べる必要がある
let c: Character = "\n"
let isNewline = c.unicodeScalars.first.map { scalar in
    // Unicode の改行系スカラを列挙して判定…
    scalar == "\n" || scalar == "\r" || scalar.value == 0x0085
        || scalar.value == 0x2028 || scalar.value == 0x2029
} ?? false

Unicode.Scalar.Properties(SE-0211 で導入)は Unicode に精通した利用者向けに、Unicode Character Database の詳細なプロパティを公開しています。しかしこれは「書記素全体として数字か」「ASCII 値は何か」といったカジュアルな用途には粒度が細かすぎ、意味の解釈や Unicode 版数の違いまで利用者が引き受ける必要がありました。

結果として、Character まわりのごくありふれた判定コードが必要以上に冗長になり、Swift 全体の使いやすさを損なっていました。素朴で分かりやすい API を Character 自体に生やす余地が十分に残っている、というのがこの proposal の問題意識です。

02 どのように解決されるのか

Character に、ASCII・数字・文字・空白・改行・記号・句読点などを判定するプロパティと、大文字・小文字変換のメソッドが一式追加されました。いずれも書記素(extended grapheme cluster)全体に対する操作として、日常的に必要になる粒度で提供されます。

ASCII に関するプロパティ

ASCII かどうかの判定と、ASCII の場合の符号値の取得ができます。

let a: Character = "A"
a.isASCII        // => true
a.asciiValue     // => Optional(65)

let ha: Character = "は"
ha.isASCII       // => false
ha.asciiValue    // => nil

asciiValueUInt8? を返します。なお CR-LF("\r\n")は LF に正規化されるため asciiValue0x0A になります。

空白・改行

isWhitespaceisNewline で、空白文字か改行かを判定できます。タブや半角スペースだけでなく、段落区切り(U+2029)や全角スペース(U+3000)なども空白として扱われ、LF / CR / CR-LF に加えて NEL(U+0085)や LINE SEPARATOR(U+2028)なども改行として扱われます。

let text = "Hello,\nworld!"
let lineCount = text.filter { $0.isNewline }.count + 1 // => 2

先頭が空白や改行で、その後ろに結合用スカラが続くような書記素については、意味が曖昧なため判定結果は意図的に未規定とされています。

数値

数字関連の API は「数字らしさを緩く問う」ものと「具体的な値を厳密に取り出す」ものの 2 種類に分かれます。

let seven: Character = "7"
seven.isNumber          // => true
seven.isWholeNumber     // => true
seven.wholeNumberValue  // => Optional(7)

let fraction: Character = "⅚" // U+215A VULGAR FRACTION FIVE SIXTHS
fraction.isNumber         // => true(数を表す文字ではある)
fraction.wholeNumberValue // => nil(整数としての解釈はできない)

let thaiNine: Character = "๙" // U+0E59 THAI DIGIT NINE
thaiNine.wholeNumberValue // => Optional(9)

let cjkTenThousand: Character = "万"
cjkTenThousand.wholeNumberValue // => Optional(10_000)

isNumber は「数を表す文字か」を緩く問うフラグで、分数や丸囲み数字なども含みます。一方 isWholeNumber / wholeNumberValue は、書記素全体が単一スカラで、かつ整数値として一意に解釈できる場合だけ真(あるいは Int 値)を返す厳密な API です。たとえば "7̅""7" + U+0305 COMBINING OVERLINE)は wholeNumberValuenil になります。

16 進数字(0〜9 と a〜f、A〜F、および全角互換形)についても同様に、判定用の isHexadecimalDigit と値取得用の hexadecimalDigitValue が用意されています。

let f: Character = "f"
f.isHexadecimalDigit   // => true
f.hexadecimalDigitValue // => Optional(15)

文字種・大文字小文字

アルファベットや表意文字などの「文字(letter)」かを判定する isLetter と、大文字小文字まわりの API が揃います。

let e: Character = "é" // U+0065 + U+0301
e.isLetter      // => true
e.isLowercase   // => true
e.uppercased()  // => "É"(U+0045 + U+0301)

let sharpS: Character = "ß"
sharpS.uppercased() // => "SS"(書記素 2 個分に展開される)

大文字小文字変換は書記素 1 個が複数の書記素になり得るため、uppercased() / lowercased() はいずれも String を返します。isUppercase は「大文字化しても変わらないが小文字化で変化する」文字、isLowercase はその逆、isCased は「何らかの大小変換で変化する」文字を示します。

記号・句読点

記号や句読点を判定するプロパティも追加されます。

let plus: Character = "+"
plus.isSymbol         // => true
plus.isMathSymbol     // => true

let yen: Character = "¥"
yen.isCurrencySymbol  // => true

let emDash: Character = "—"
emDash.isPunctuation  // => true

isMathSymbolisSymbol の厳密な部分集合ではない点に注意が必要です。たとえば "ϰ"(GREEK KAPPA SYMBOL)は数式記号としても文字としても扱われるため、isLetterisMathSymbol の両方で真になります。

設計方針

これらの API は、値を「取り出す」ものと「緩く問い合わせる」ものとで扱いが意識的に分けられています。

  • wholeNumberValueasciiValueuppercased() のように具体的な値・結果を産出するもの(restrictive)は、書記素全体を見て曖昧さのないケースだけ結果を返します。
  • isNumberisLetterisSymbol のような緩い問い合わせ(permissive)は、書記素の先頭スカラなどをもとに、多少の結合スカラが付いていても素直に真を返します。

「受け入れる側は寛容に、産出する側は厳密に」という方針により、日常用途では直感通りに動き、厳密な値を扱うケースでは誤った解釈が生じないようになっています。