UTF-32/UCS-4 に対応した islower/tolower 関数を作ってみよう

今回は、ロケールや処理系、標準ライブラリの実装に依存しない UTF-32/UCS-4 向けの islower/isupper/tolower/toupper 関数を作ります。 C++11 から UTF-32/UCS-4 の文字を格納できる型 char32_t が追加されたので、文字の型は char32_t で指定できるようにします。 また Unicode の文字セットをすべてカバーできるように、 Unicode Character Database を利用してみます。

レターケース (Letter case)

今回は Unicode の文字セットと UTF-32/UCS-4 エンコーディングされた文字を扱います。 アルファベットの中には 大文字 (Uppercase), 小文字 (Lowercase) などの区別ができるものがあります。 例えば A (U+0041) は大文字で、 a (U+0061) は小文字です。また、A の小文字は a になり、 a の大文字は A になります。 こういった区別を レターケース (letter case) といいます。 Unicode では、大文字・小文字の他に次のレターケースが定義されています:

  • (1) Uppercase
  • (2) Lowercase
  • (3) Titlecase
  • (4) Modifier letter
  • (5) Other letter

それぞれについて説明すると:

  • (1) 大文字のことです。 Capital letter とも言います。例えば A (U+0041) や Σ (U+03A3) が Uppercase です。文字列を Uppercase で示すと UPPERCASE となります。
  • (2) 小文字のことです。 Small letter とも言います。例えば a (U+0061) や σ (U+03C3) が Lowercase です。文字列を Lowercase で示すと lowercase となります。
  • (3) 文字列の先頭が大文字に、先頭以外では小文字になるレターケースです。文字列を Titlecase で示すと Titlecase となります。例えば Dz (U+01F2) が Titlecase です。Dz (U+01F2) の Uppercase は DZ (U+01F1) で、 Lowercase は dz (U+01F2) です。
  • (4) Unicode の中では、隣の文字を修飾する文字や記号を示します。例えば (U+3005) は Modifier letter です。
  • (5) (1) から (4) 以外のその他のレターケースです。日本語のひらがなとカタカナがこれに当たります。例えば (U+3041) 1 (U+30A2) が (5) に含まれます。

Unicode Data File

Unicode の文字セットは Unicode Character Database (UCD) として公開されています。 今回は UCD の中に含まれている UnicodeData.txt ファイル 2 を使って文字のレターケースを列挙してみます。 UnicodeData.txt ファイルのダウンロードとそのファイルフォーマットについて、詳しくは次のページを参照してください。

次のテキストは UnicodeData.txt の一部を引用したものです。

005F;LOW LINE;Pc;0;ON;;;;;N;SPACING UNDERSCORE;;;;
0060;GRAVE ACCENT;Sk;0;ON;;;;;N;SPACING GRAVE;;;;
0061;LATIN SMALL LETTER A;Ll;0;L;;;;;N;;;0041;;0041
0062;LATIN SMALL LETTER B;Ll;0;L;;;;;N;;;0042;;0042

UnicodeData.txt は、 ASCII コードのみで書かれたテキストファイルになっていて、一行ごとに 1 文字の定義がされています。 各フィールドはセミコロンで区切られているので、取り扱いも難しくありません。 例えば、次の行は a (U+0061) の文字とその定義です。

0061;LATIN SMALL LETTER A;Ll;0;L;;;;;N;;;0041;;0041

フィールドは 15 項目に分かれており、最初のフィールドは code point を表しています。例では 0061 が code point です。 3 番目のフィールド 3Ll は "Letter, Lowercase" のことで、レターケースを示しています。

略語 レターケース
Lu Letter, Uppercase
Ll Letter, Lowercase
Lt Letter, Titlecase
Lm Letter, Modifier letter
Lo Letter, Other

また最後の 3 つのフィールド 0041;;0041 は Uppercase/Lowercase/Titlecase へのマッピングを示しています。 例えば、a (U+0061) の文字の Uppercase または Titlecase が A (U+0041) になります。

UnicodeData.txt を読み込む

さっそく読み込んでみます。文字データは次の構造体に格納することにしました。

struct UnicodeCharacterData {
    char32_t codePoint;
    std::string characterName;
    std::string generalCategory;
    std::string canonicalCombiningClasses;
    std::string bidirectionalCategory;
    std::string characterDecompositionMapping;
    std::string decimalDigitValue;
    std::string digitValue;
    std::string numericValue;
    std::string mirrored;
    std::string unicode_1_0_name;
    std::string commentField10646;
    std::optional<char32_t> uppercaseMapping;
    std::optional<char32_t> lowercaseMapping;
    std::optional<char32_t> titlecaseMapping;
};

任意の区切り文字で文字列を分割するために Split 関数を定義します:

std::vector<std::string> Split(const std::string& source, char separator)
{
    std::vector<std::string> tokens;
    std::string::size_type start = 0;
    std::string::size_type end = 0;
    while ((end = source.find(separator, start)) != std::string::npos) {
        tokens.push_back(source.substr(start, end - start));
        start = end + 1;
    }
    tokens.push_back(source.substr(start));
    return tokens;
}

文字列で表現された code point を char32_t に変換する ToCodePointFromHexString 関数を定義します:

std::optional<char32_t> ToCodePointFromHexString(const std::string& s)
{
    if (s.empty()) {
        return std::nullopt;
    }
    return static_cast<char32_t>(std::stoi(s, nullptr, 16));
}

UnicodeData.txt ファイルを読み込む ReadUnicodeDataFile 関数を定義します。最低限のエラーハンドリングしか書いていないので注意してください。

std::vector<UnicodeCharacterData> ReadUnicodeDataFile(const std::string& path)
{
    std::ifstream ifs(path);
    if (!ifs) {
        std::cerr << "Cannot open the file: '" << path << "'";
        return {};
    }

    std::vector<UnicodeCharacterData> datas;
    std::string line;
    while (std::getline(ifs, line)) {
        auto words = Split(line, ';');
        if (words.size() != 15) {
            continue;
        }
        UnicodeCharacterData data;
        data.codePoint = *ToCodePointFromHexString(words[0]);
        data.characterName = words[1];
        data.generalCategory = words[2];
        data.canonicalCombiningClasses = words[3];
        data.bidirectionalCategory = words[4];
        data.characterDecompositionMapping = words[5];
        data.decimalDigitValue = words[6];
        data.digitValue = words[7];
        data.numericValue = words[8];
        data.mirrored = words[9];
        data.unicode_1_0_name = words[10];
        data.commentField10646 = words[11];
        data.uppercaseMapping = ToCodePointFromHexString(words[12]);
        data.lowercaseMapping = ToCodePointFromHexString(words[13]);
        data.titlecaseMapping = ToCodePointFromHexString(words[14]);
        datas.push_back(std::move(data));
    }
    return datas;
}

実際に UnicodeData.txt ファイルを読み込んで、 code point のリストを表示してみます:

int main(int argc, char *argv[])
{
    // NOTE: http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
    auto unicodeDatas = ReadUnicodeDataFile("UnicodeData.txt");
    for (auto & data : unicodeDatas) {
        std::cout
            << "U+" << std::setfill('0') << std::setw(4)
            << std::uppercase << std::hex << data.codePoint << std::endl;
    }
    return 0;
}

IsLowercase の実装

読み込んだ Unicode Data を使って、 IsLowercase 関数を実装してみます。 レターケースのマッピングを次のような構造体で定義します。

struct LetterCaseMapping {
    std::optional<char32_t> uppercase;
    std::optional<char32_t> lowercase;
    std::optional<char32_t> titlecase;
};

IsLowercase 関数を実装するために、文字セットの中から任意の文字を検索する必要があります。 今回は、ハッシュで検索できる std::unordered_map を使うことにしました。 次の関数は Unicode Data のリストからハッシュマップを作ります。

std::unordered_map<char32_t, LetterCaseMapping>
CreateLetterCaseMap(const std::vector<UnicodeCharacterData>& unicodeDatas)
{
    std::unordered_map<char32_t, LetterCaseMapping> letterCaseMap;
    for (auto & data : unicodeDatas) {
        if (data.generalCategory == "Lu") {
            LetterCaseMapping mapping;
            mapping.uppercase = data.codePoint;
            mapping.lowercase = data.lowercaseMapping;
            mapping.titlecase = data.titlecaseMapping;
            letterCaseMap.emplace(data.codePoint, std::move(mapping));
        }
        else if (data.generalCategory == "Ll") {
            LetterCaseMapping mapping;
            mapping.uppercase = data.uppercaseMapping;
            mapping.lowercase = data.codePoint;
            mapping.titlecase = data.titlecaseMapping;
            letterCaseMap.emplace(data.codePoint, std::move(mapping));
        }
        else if (data.generalCategory == "Lt") {
            LetterCaseMapping mapping;
            mapping.uppercase = data.uppercaseMapping;
            mapping.lowercase = data.lowercaseMapping;
            mapping.titlecase = data.codePoint;
            letterCaseMap.emplace(data.codePoint, std::move(mapping));
        }
    }
    return letterCaseMap;
}

このハッシュマップを使って IsLowercase 関数を次のように実装しました。

bool IsLowercase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    auto iter = map.find(c);
    if (iter == std::end(map)) {
        return false;
    }
    return iter->second.lowercase && (*iter->second.lowercase == c);
}

IsLowercase 関数の使い方は次の通りです:

int main(int argc, char *argv[])
{
    // NOTE: http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
    auto unicodeDatas = ReadUnicodeDataFile("UnicodeData.txt");
    auto letterCaseMap = CreateLetterCaseMap(unicodeDatas);

    std::cout << std::boolalpha << IsLowercase(U'\U00000041', letterCaseMap) << std::endl;
    std::cout << std::boolalpha << IsLowercase(U'\U00000061', letterCaseMap) << std::endl;

    return 0;
}

ToLowercase 関数の実装

IsLowercase と同様に、 ToLowercase 関数を実装してみます。

char32_t ToLowercase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    auto iter = map.find(c);
    if (iter == std::end(map) || !iter->second.lowercase) {
        return c;
    }
    return *iter->second.lowercase;
}

ToLowercase 関数の使い方は次の通りです:

int main(int argc, char *argv[])
{
    // NOTE: http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
    auto unicodeDatas = ReadUnicodeDataFile("UnicodeData.txt");
    auto letterCaseMap = CreateLetterCaseMap(unicodeDatas);

    std::cout << ToLowercase(U'\U00000041', letterCaseMap) << std::endl;
    std::cout << ToLowercase(U'\U00000061', letterCaseMap) << std::endl;

    return 0;
}

その他の関数の実装

その他の IsUppercase, ToUppercase, IsTitlecase, ToTitlecase の実装も載せておきます。 IsLowercase 関数や ToLowercase 関数と同じようにハッシュマップを使っています。

bool IsUppercase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    auto iter = map.find(c);
    if (iter == std::end(map)) {
        return false;
    }
    return iter->second.uppercase && (*iter->second.uppercase == c);
}
char32_t ToUppercase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    auto iter = map.find(c);
    if (iter == std::end(map) || !iter->second.uppercase) {
        return c;
    }
    return *iter->second.uppercase;
}
bool IsTitlecase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    auto iter = map.find(c);
    if (iter == std::end(map)) {
        return false;
    }
    return iter->second.titlecase && (*iter->second.titlecase == c);
}
char32_t ToTitlecase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    auto iter = map.find(c);
    if (iter == std::end(map) || !iter->second.titlecase) {
        return c;
    }
    return *iter->second.titlecase;
}

最適化

これらの関数をどんなアプリケーションに使用するのかにもよりますが、 ASCII コードの範囲内 (U+0000 から U+007F) の文字が頻繁に入力として与えられることが考えられます。 ASCII コードの文字が入力されたときに、 std::unordered_map よりも早く検索できるようにしてみます。 次のようなテーブルを用意します。

constexpr char32_t toLowerCharacters[128] = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
    112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 0, 0, 0, 0, 0,
    0, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111,
    112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 0, 0, 0, 0, 0,
};

その文字が Lowercase ならその文字の code point を格納します。 また Lowercase でない文字で Lowercase mapping が定義されているなら、Lowercase mapping の code point を格納し、それ以外の文字は 0 を指定します。

このテーブルを使って IsLowercase 関数を実装したのが次のコードです:

bool IsLowercase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    if (c < SizeOfArray(toLowerCharacters)) {
        auto t = toLowerCharacters[c];
        return (t != 0) && (t == c);
    }
    auto iter = map.find(c);
    if (iter == std::end(map)) {
        return false;
    }
    return iter->second.lowercase && (*iter->second.lowercase == c);
}

同様に ToLowercase 関数も実装してみました:

char32_t ToLowercase(char32_t c, const std::unordered_map<char32_t, LetterCaseMapping>& map)
{
    if (c < SizeOfArray(toLowerCharacters)) {
        auto t = toLowerCharacters[c];
        return (t == 0) ? c : t;
    }
    auto iter = map.find(c);
    if (iter == std::end(map) || !iter->second.lowercase) {
        return c;
    }
    return *iter->second.lowercase;
}

今回は constexpr の配列サイズを取るために次のような関数を用意しました。

template <typename T, std::size_t N>
constexpr std::size_t SizeOfArray(T (&)[N]) { return N; }

参考文献


  1. 余談ですが、ひらがなの (U+3042) は (U+3041) へ Lowercase マッピングされているのか気になって確認したところ、 UnicodeData.txt ではそういったマッピングは定義されていませんでした。Unicode の世界ではラテン文字でいうところの "小文字" と、ひらがなでいうところの "小文字" は区別されているようです。 

  2. UCD の中でも UnicodeData.txt は比較的に古い形式で書かれていて、後方互換性のために残されています。今回はプレーンな C++ でも取り扱いが簡単なテキストファイルということで UnicodeData.txt を使用しました。 

  3. Letter case や文字のカテゴリについてはこちらを参照ください: http://www.unicode.org/reports/tr44/#GC_Values_Table 

Leave a Reply