トップC++ > UTF-8文字列変換

UTF-8文字列変換

はじめに

OSM(Open Street Map)の文字コードは UTF-8 である。OSMデータの地名(丁目)では、 ○○五丁目、○○5丁目、○○5、○○5 などマッパーにより様々な表記が使われている。

比較的に多いのは○○五丁目であるが、マイOSM地図としては、なるべくスリムにしたい。 交差点名は概ね「○○五丁目」表記のため、これと区別するためにも、 地名は「○○5」表記としたい。番号はまずは半角としよう。

本ページではこのような地名(丁目)変換プログラムを考える。

UTF-8コード

記事[1]によると、UTF-8コードは以下のようである。文字は 1〜3 バイトで表わされる。 先頭バイトが 0x7f 以下ならば1バイト、0x80〜0xdf ならば2バイト、0xe0〜0xef ならば3バイトとなる。

この表には 0xf0〜0xff で始まる4〜6バイト文字が省略されているようだ。

UnicodeのコードポイントUTF-8
0x0000〜0x007f (ASCII)0x00〜0x7f
0x0080〜0x07ff (各国アルファベット)0xc080〜0xdfbf
0x0800〜0xffff (インド系諸文字、句読点、学術記号、絵文字、東アジアの諸字、全角、半角形) 0xe08080〜0xefbfbf
UnicodeのコードポイントとUTF-8の対応表[1]

記事[2]では、4〜6バイト文字もある。

コードカテゴリ備考
00-7x1バイト文字US-ASCIIにおなじ
8x,9x,Ax,Bx多バイト文字の2バイト目以降
Cx,Dx2バイト文字の開始バイト
Ex3バイト文字の開始バイト漢字はおおむねこれで開始
Fx4バイト以上の文字の開始バイトF0-F7は4バイト、F8-FBは5バイト、FC-FDは6バイト

多分、一、二、... 、十 および 0、1、... 、9 は3バイト文字であろう。

地名(丁目)変換プログラム

○○丁目のように、末尾が丁目の場合にのみ変換する。すなわち、十三(じょうそう)を 13 とはしない。

地名(placeタグ)のみ変換し、交差点名(highway=traffic_signal)、店名などは変換しない。

変換したくないケースがあれば例外処理とする。

OSMデータ調査

途中段階の次のプログラムで2021年3月21日時点の japan.osm.pbf を調べた。 「○○△丁目」で、△の 1, 2, ... を除外すると、○○は全て3バイト文字であることが確認できた。 例外は「=鎌倉台一丁目」であるが、これは明らかに入力ミスであるから、OSMデータを訂正した。

したがって、地名(丁目)変換プログラムでは、丁目が元々半角数字であるケースと 3バイト文字だけを考慮すればよい。その他の文字が含まれれば、 警告メッセージを出して無変換とすればよい。

// ○○十五丁目 → ○○15
string TagConvert::chome(string str) {
    const char * Nums1[] = { "一", "二", "三", "四", "五", "六", "七", "八", "九", "十" };
    const char * Nums2[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" };
    unsigned char *top = (unsigned char *)str.c_str();
    const char *bgn = NULL; // 番号の始まり
    unsigned char *end = top + str.size() - 6;      // 6 は "丁目" の長さ
    int num = 0;

    for (unsigned char* p = (unsigned char*)str.c_str(); p < end; ) {
        int c = (int)*p;
        if (c <= 0x7f) {                            // 1バイト文字
            if ('0' <= c && c <= '9') {
                num = num * 10 + (c - '0');
            } else if (c != ' ') {
                cerr << "丁目変換エラー: " << str << "\n";
                return str;                             // 無変換
            }
            p++;
        } else if (0xc0 <= c && c <= 0xdf) {        // 2バイト文字
                cerr << "2バイト文字: " << str << "\n";
            p += 2;
        } else if (0xe0 <= c && c <= 0xef) {        // 3バイト文字
            p += 3;
        } else if (0xf0 <= c && c <= 0xf7) {        // 4バイト文字
                cerr << "4バイト文字: " << str << "\n";
            p += 4;
        } else if (0xf8 <= c && c <= 0xfb) {        // 5バイト文字
                cerr << "5バイト文字: " << str << "\n";
            p += 5;
        } else if (0xfc <= c && c <= 0xfd) {        // 6バイト文字
                cerr << "6バイト文字: " << str << "\n";
            p += 6;
        } else {
            cerr << "丁目変換エラー: " << str << "\n";
            return str;                             // 無変換
        }
    }
    return str;
}

地名(丁目)変換プログラム

地名自体に数字を含むケースがあるため、番号の読み取りは末尾から行う。 以下のプログラムによりざっとチェックした限りでは所望の結果が得られた。

// ○○十五丁目 → ○○15
string TagConvert::chome(string str) {
    const char * Nums1[] = { "十", "一", "二", "三", "四", "五", "六", "七", "八", "九" };
    const char * Nums2[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
    unsigned char *top = (unsigned char *)str.c_str();
    unsigned char *end = top + str.size() - 6;      // 6 は "丁目" のバイト長
    unsigned char *p = end;
    int num = 0;
    int n;
    int fact = 1;
    bool ten = false;

    while (p > top) {
        int c = (int)p[-1];
        if (c <= 0x7f) {                            // 1バイト文字
            p--;
            if ('0' <= c && c <= '9') {
                num += (c - '0') * fact;
                fact *= 10;
            } else if (c != ' ') {
                cerr << "丁目変換エラー: " << str << "\n";
                return str;                             // 無変換
            }
        } else if (0xe0 <= p[-3] && p[-3] <= 0xef) {        // 3バイト文字
            p -= 3;
            for (n = 0; n < 10; n++) {
                if (strncmp((const char*)p,Nums1[n],3) == 0) break;
            }
            if (n == 10) {
                for (n = 0; n < 10; n++) {
                    if (strncmp((const char*)p,Nums2[n],3) == 0) break;
                }
            }
            if (n == 10) {
                p += 3;
                break;
            }
            if (n == 0) {
                ten = true;
                n = 10;
                if (num >= 10) num -= 10;
                if (fact >= 10) fact /= 10;
            } else if (num > 0 && !ten) {
                p += 3;
                break;
            }
            num += n * fact;
            fact *= 10;
        } else {
            cerr << "丁目無変換: " << str << "\n";
            return str;                             // 無変換
        }
    }
    string out((const char*)top, p-top);
    out = out + to_string(num);
    return out;
}

リファレンス

[1] 文字コードUTF-8とは? 仕組みとコード表
[2] UTF-8コード表(1)