Swift Digest
SE-0211 | Swift Evolution

Add Unicode Properties to Unicode.Scalar

Proposal
SE-0211
Authors
Tony Allevato
Review Manager
Ben Cohen
Status
Implemented (Swift 5.0)

01 何が問題だったのか

Swift の StringCharacterUnicode.Scalar は、String 比較の正規化やgrapheme cluster 境界の検出など、Unicode に沿った高水準の処理を手厚く提供しています。しかし、いざ個々のコードポイント(Unicode.Scalar)のレベルまで降りようとすると、他の言語では当たり前に使える基本的な判定・分類のAPIが大きく欠けていました。

たとえば、ある Unicode.Scalar について次のようなことを Swift 単体で調べる手段がありませんでした。

  • 大文字・小文字か(Uppercase / Lowercase プロパティ)、その大文字化・小文字化後の文字列は何か
  • ホワイトスペースか、数字か、記号か、絵文字か
  • 結合文字の並び替えに必要な Canonical Combining Class の値
  • 識別子として使える文字か(ID_Start / ID_Continue)
  • Unicode における正式名称(”LATIN SMALL LETTER A” など)や、どのバージョンで導入された文字か
  • 分数や漢数字のように文字が内在的に持つ数値

回避策として Darwin / Glibc を import して C の isspace などを呼ぶ方法はありますが、これは ASCII 範囲しか扱えず、本来の Unicode 対応にはなりません。

ICU を直接使う道も塞がっている

Swift 標準ライブラリの Unicode 処理はシステムの ICU ライブラリを使って実装されているため、「自分のアプリからも同じ ICU を直接呼べばよい」と思いたくなりますが、実際には Apple 系・Linux のどちらでも現実的ではありません。

  • Apple 系プラットフォームの libicucore.dylib は App Store 審査上のプライベート API 扱いで、直接リンクすると申請が通りません。利用するには ICU を自前でビルドしてアプリに同梱する必要があり、コストが大きくなります。
  • Linux のシステム ICU は関数名がバージョン番号で renaming されており(u_foo_59 のような接尾辞が付きます)、ヘッダ側の #define による別名は Swift にインポートされないため、特定バージョンに縛られない形で使うには中間レイヤを自分で挟む必要があります。

結果として「標準ライブラリで提供されるべき基本的な Unicode プロパティ」が、ユーザーから見るとどこにも使える形で存在しない、という状態になっていました。

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

Unicode.Scalar に、Unicode 標準で定義されている各種プロパティをまとめて提供する入れ子の構造体 Unicode.Scalar.Properties を追加します。スカラ本体の API を肥大化させないために、各種プロパティは scalar.properties.xxx の形で取得する設計になっています。

let scalar: Unicode.Scalar = "A"
if scalar.properties.isUppercase {
    print("uppercase!")
}

プロパティ名は原則として Unicode 標準のプロパティ名をそのまま isXxx 形式にしたもので、Unicode 仕様と対応付けて調べやすくなっています(そのため isXIDContinue のように一見 Swift らしくない名前もそのまま採用されています)。

真偽値プロパティ

ICU の u_hasBinaryProperty で取得できる、Unicode 標準が定めるBoolean プロパティを一通り公開します(ICU 固有のものや、非推奨の Grapheme_Link / Hyphen は除外)。代表的なものは次の通りです。

extension Unicode.Scalar.Properties {
    public var isAlphabetic: Bool { get }
    public var isUppercase: Bool { get }
    public var isLowercase: Bool { get }
    public var isWhitespace: Bool { get }
    public var isHexDigit: Bool { get }
    public var isMath: Bool { get }
    public var isIDStart: Bool { get }
    public var isIDContinue: Bool { get }
    public var isEmoji: Bool { get }
    public var isEmojiPresentation: Bool { get }
    public var isEmojiModifier: Bool { get }
    public var isEmojiModifierBase: Bool { get }
    // ...ほか Dash / Diacritic / Ideographic / Noncharacter_Code_Point /
    //   QuotationMark / Variation_Selector / Pattern_White_Space などを網羅
}

ケース変換で「その文字が変化するかどうか」を表す次のプロパティも利用できます。

extension Unicode.Scalar.Properties {
    public var changesWhenLowercased: Bool { get }
    public var changesWhenUppercased: Bool { get }
    public var changesWhenTitlecased: Bool { get }
    public var changesWhenCaseFolded: Bool { get }
    public var changesWhenCaseMapped: Bool { get }
    public var changesWhenNFKCCaseFolded: Bool { get }
}

ケースマッピング

小文字化・大文字化・タイトルケース化の結果を返すプロパティも用意されます。ドイツ語の “ß” の大文字化が “SS” になるように、結果が1つの Unicode.Scalar に収まらないケースがあるため、戻り値は Unicode.Scalar ではなく String になっています。

extension Unicode.Scalar.Properties {
    public var lowercaseMapping: String { get }
    public var titlecaseMapping: String { get }
    public var uppercaseMapping: String { get }
}

let eszett: Unicode.Scalar = "ß"
print(eszett.properties.uppercaseMapping)  // "SS"

識別と分類

各スカラの素性を調べるためのプロパティ群です。

extension Unicode.Scalar.Properties {
    public var age: Unicode.Version? { get }            // 初出 Unicode バージョン
    public var name: String? { get }                    // 正式名称(例: "LATIN SMALL LETTER A")
    public var nameAlias: String? { get }               // 別名
    public var generalCategory: Unicode.GeneralCategory { get }
    public var canonicalCombiningClass: Unicode.CanonicalCombiningClass { get }
}

Unicode.Version(major: Int, minor: Int) のタプルのエイリアスで、Unicode.GeneralCategoryuppercaseLetter(Lu)、decimalNumber(Nd)、mathSymbol(Sm)、spaceSeparator(Zs)のように、Unicode のGeneral Category の2文字コードに対応する enum です。

Canonical_Combining_Class は 0〜255 の整数値のうち一部にのみ標準で名前が付いているため、Unicode.CanonicalCombiningClassRawRepresentableUInt8)かつ Comparable な構造体として定義され、名前付きの値は静的プロパティで提供されます。

extension Unicode {
    public struct CanonicalCombiningClass:
        Comparable, Hashable, RawRepresentable
    {
        public static let notReordered = CanonicalCombiningClass(rawValue: 0)
        public static let virama       = CanonicalCombiningClass(rawValue: 9)
        public static let above        = CanonicalCombiningClass(rawValue: 230)
        // ... ほか overlay / nukta / attachedBelow / left / right など
        public let rawValue: UInt8
        public init(rawValue: UInt8)
    }
}

名前付きでない値も rawValue 経由で扱え、Comparable なので手動で decomposition のロジックを書くときにも使えます。

数値プロパティ

Unicode のスカラには、ASCII の 0〜9 だけでなく、分数(½、⅓ など)や漢数字・各言語の数字記号のように「内在的な数値」を持つものがあります。それらを取り出せるプロパティが追加されます。

extension Unicode.Scalar.Properties {
    public var numericType: Unicode.NumericType?   // .decimal / .digit / .numeric
    public var numericValue: Double?
}

let half: Unicode.Scalar = "½"
print(half.properties.numericType)   // Optional(.numeric)
print(half.properties.numericValue)  // Optional(0.5)

これにより、文字列中の数的なデータの抽出や、独自の数値パーサーの実装が Swift 単体で行えるようになります。

今回のスコープ

この提案は意図的に Unicode.Scalar のみを対象としています。ここで導入されるプロパティの一部は Character にも欲しくなる場面がありますが、それらは設計を Unicode.Scalar 側で固めた後の将来の提案に委ねられており、今回の範囲外です。