今回は、ロケールや処理系、標準ライブラリの実装に依存しない 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 ファイルのダウンロードとそのファイルフォーマットについて、詳しくは次のページを参照してください。
- Unicode Character Database
- http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
- http://www.unicode.org/reports/tr44/#UnicodeData.txt - 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 番目のフィールド 3 の Ll
は "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; }
参考文献
- Unicode Character Database
- http://www.unicode.org/reports/tr44/#UnicodeData.txt - UnicodeData.txt について
- http://www.unicode.org/reports/tr44/#GC_Values_Table - Letter case などのカテゴリについて
-
余談ですが、ひらがなの
あ
(U+3042) はぁ
(U+3041) へ Lowercase マッピングされているのか気になって確認したところ、 UnicodeData.txt ではそういったマッピングは定義されていませんでした。Unicode の世界ではラテン文字でいうところの "小文字" と、ひらがなでいうところの "小文字" は区別されているようです。 ↩ -
UCD の中でも UnicodeData.txt は比較的に古い形式で書かれていて、後方互換性のために残されています。今回はプレーンな C++ でも取り扱いが簡単なテキストファイルということで UnicodeData.txt を使用しました。 ↩
-
Letter case や文字のカテゴリについてはこちらを参照ください: http://www.unicode.org/reports/tr44/#GC_Values_Table ↩
Leave a Reply