Character Properties
01 何が問題だったのか
Swift の String は Character(拡張書記素クラスタ、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
asciiValue は UInt8? を返します。なお CR-LF("\r\n")は LF に正規化されるため asciiValue は 0x0A になります。
空白・改行
isWhitespace と isNewline で、空白文字か改行かを判定できます。タブや半角スペースだけでなく、段落区切り(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)は wholeNumberValue が nil になります。
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
isMathSymbol は isSymbol の厳密な部分集合ではない点に注意が必要です。たとえば "ϰ"(GREEK KAPPA SYMBOL)は数式記号としても文字としても扱われるため、isLetter と isMathSymbol の両方で真になります。
設計方針
これらの API は、値を「取り出す」ものと「緩く問い合わせる」ものとで扱いが意識的に分けられています。
wholeNumberValueやasciiValue、uppercased()のように具体的な値・結果を産出するもの(restrictive)は、書記素全体を見て曖昧さのないケースだけ結果を返します。isNumberやisLetter、isSymbolのような緩い問い合わせ(permissive)は、書記素の先頭スカラなどをもとに、多少の結合スカラが付いていても素直に真を返します。
「受け入れる側は寛容に、産出する側は厳密に」という方針により、日常用途では直感通りに動き、厳密な値を扱うケースでは誤った解釈が生じないようになっています。