文字コードを理解してなかったお話

プロコンゼミ(SPC同好会) その2 Advent Calendar 2018 12日目です。

特に技術的なことができていないので、C言語の中で日本語を使って文字整形をしようとして困った話を備忘録として残しておこうと思います。

まえおき

C言語で表的なのを表示したいということで、printf("%5s", "ほげ");みたいなので整形しようとしていたのですが、どうもスペースが追加されない。
いろいろ数字変えてやってみたところ、どうやら全角(ひらがな)1文字を半角文字3としているみたいだとわかりました。

全角文字って半角文字2つ分じゃないの!?

後にこれは半分はあってるということがわかるわけですが、結論を言うと単にUTF-8とか、文字コード全般の仕組みを知らなかっただけでした。

本題

いろいろ調べていくと、どうやらUTF-8の場合はひらがな1文字が3byteということがわかりました。
Shift-JISは全角文字が2byteということは知っていたので、ここで勘違いには気づきました。
それと同時にCのprintfの整形はbyte数で判断してるんだともわかりました。(まぁchar配列を渡してるので当たり前っちゃ当たり前ですが)

しかしまだ問題は解決してません、このままでは半角全角混じりの文字列をきちんと整形できません。

調べ進めていくと、East Asian Widthなるものを見つけました。
お、これで解決やな!と思ったのもつかの間、「これUnicodeじゃん!UTF-8だとどうすればいいの!?」(UTF-8はUnicodeの書き方の一つ)
UTF-8の意味についてきちんと理解していれば悩むようなところではないのですが、
「ぼくが知りたいのは2+1の答えであって1+2の答えじゃないんだよ!!」というアホみたいな状態を数日続けてしまいました。虚無

UTF文字コードとは?Unicodeとは?

UTF系の文字コードにはご存知のとおりいくつか種類がありますね。UTF-8、UTF-16といった風に。
そもそもUTFというのはUnicode Transformation Formatの略です。つまりUTF-8にしろUTF-16にしろ、Unicodeをどうやってデータとして表現するかの方法に過ぎなかったのです。
ぼくは今まで知りませんでした

East Asian Widthって?

先に紹介したEast Asian Widthについての説明がまだでした。
ざっくり説明すると、Unicodeにおいて全角文字とかが定義されている範囲を示したもののことです。
詳しいことはこちらをどうぞ。

やったこと

文字データからUnicodeをとりだし、それをEast Asian Widthの一覧と対応させて全角半角を判定しようと思います。
East Asian Widthについての対応関係は、調べている途中で見つけたこちらのコードを使わせていただいてます。
UTF-8についての中身はWikipediaの説明(表)がわかりやすかったです。
具体的なコードは以下のようになります。

// #include <stdbool.h>  
// #include <string.h>  
char getUTF8ByteSize(const unsigned char b) {  
    if (b <= 0x7f) return 1;  
    if (0xc0 <= b && b <= 0xdf) return 2;  
    if (0xe0 <= b && b <= 0xef) return 3;  
    if (0xf0 <= b && b <= 0xf7) return 4;  
    if (0xf8 <= b && b <= 0xfb) return 5;  
    if (0xfc <= b && b <= 0xfd) return 6;  
    return 0;  
}  

char getMaskedUTF8(const char b) {  
    const char l = getUTF8ByteSize(b);  
    char mask = 0x0;  
    switch (l) {  
        case 1: mask = 0x7f; break;            // 0111 1111  
        case 2: mask = 0x1f; break;            // 0001 1111  
        case 3: mask = 0x0f; break;            // 0000 1111  
        case 4: mask = 0x07; break;            // 0000 0111  
        case 5: mask = 0x03; break;            // 0000 0011  
        case 6: mask = 0x01; break;            // 0000 0001  
        default: mask = 0x3f; break;        // 0011 1111  
    }  
    return b & mask;  
}  

long getUnicode(const char b[]) {  
    const char l = getUTF8ByteSize(b[0]);  
    long u = 0x0;  
    for (int i = 0; i < l; i++) {  
        u <<= 6;  
        u += getMaskedUTF8(b[i]);  
    }  
    return u;  
}  

size_t strwidth(const char text[]) {  
    const size_t l = strlen((char *) text);  
    size_t c = 0;  
    for (int i = 0; i < l; i++) {  
        const char s = getUTF8ByteSize(text[i]);  
        const long a = getUnicode((char *) &text[i]);  
        const bool isF = isFullWidth(a, true);  
        c += isF ? 2 : 1;  
        if (s > 0) i += (s - 1);  
    }  
    return c;  
}  


static const int EASTASIAN_FULL_SIZE = 35, EASTASIAN_AMBIGUOUS_SIZE = 173;  
static const int32_t EASTASIAN_FULL[] = {  
        0x01100, 0x0115f, 0x02329, 0x0232a, 0x02e80, 0x02e99, 0x02e9b, 0x02ef3, 0x02f00, 0x02fd5, 0x02ff0, 0x02ffb, 0x03000, 0x0303e, 0x03041, 0x03096, 0x03099, 0x030ff, 0x03105, 0x0312d, 0x03131, 0x0318e, 0x03190, 0x031ba, 0x031c0, 0x031e3, 0x031f0, 0x0321e, 0x03220, 0x03247, 0x03250, 0x032fe, 0x03300, 0x04dbf, 0x04e00, 0x0a48c, 0x0a490, 0x0a4c6, 0x0a960, 0x0a97c, 0x0ac00, 0x0d7a3, 0x0f900, 0x0faff, 0x0fe10, 0x0fe19, 0x0fe30, 0x0fe52, 0x0fe54, 0x0fe66, 0x0fe68, 0x0fe6b, 0x0ff01, 0x0ff60, 0x0ffe0, 0x0ffe6, 0x1b000, 0x1b001, 0x1f200, 0x1f202, 0x1f210, 0x1f23a, 0x1f240, 0x1f248, 0x1f250, 0x1f251, 0x20000, 0x2fffd, 0x30000, 0x3fffd};  
static const int32_t EASTASIAN_AMBIGUOUS[] = {  
        0x000a1, 0x000a1, 0x000a4, 0x000a4, 0x000a7, 0x000a8, 0x000aa, 0x000aa, 0x000ad, 0x000ae, 0x000b0, 0x000b4, 0x000b6, 0x000ba, 0x000bc, 0x000bf, 0x000c6, 0x000c6, 0x000d0, 0x000d0, 0x000d7, 0x000d8, 0x000de, 0x000e1, 0x000e6, 0x000e6, 0x000e8, 0x000ea, 0x000ec, 0x000ed, 0x000f0, 0x000f0, 0x000f2, 0x000f3, 0x000f7, 0x000fa, 0x000fc, 0x000fc, 0x000fe, 0x000fe, 0x00101, 0x00101, 0x00111, 0x00111, 0x00113, 0x00113, 0x0011b, 0x0011b, 0x00126, 0x00127, 0x0012b, 0x0012b, 0x00131, 0x00133, 0x00138, 0x00138, 0x0013f, 0x00142, 0x00144, 0x00144, 0x00148, 0x0014b, 0x0014d, 0x0014d, 0x00152, 0x00153, 0x00166, 0x00167, 0x0016b, 0x0016b, 0x001ce, 0x001ce, 0x001d0, 0x001d0, 0x001d2, 0x001d2, 0x001d4, 0x001d4, 0x001d6, 0x001d6, 0x001d8, 0x001d8, 0x001da, 0x001da, 0x001dc, 0x001dc, 0x00251, 0x00251, 0x00261, 0x00261, 0x002c4, 0x002c4, 0x002c7, 0x002c7, 0x002c9, 0x002cb, 0x002cd, 0x002cd, 0x002d0, 0x002d0, 0x002d8, 0x002db, 0x002dd, 0x002dd, 0x002df, 0x002df, 0x00300, 0x0036f, 0x00391, 0x003a1, 0x003a3, 0x003a9, 0x003b1, 0x003c1, 0x003c3, 0x003c9, 0x00401, 0x00401, 0x00410, 0x0044f, 0x00451, 0x00451, 0x02010, 0x02010, 0x02013, 0x02016, 0x02018, 0x02019, 0x0201c, 0x0201d, 0x02020, 0x02022, 0x02024, 0x02027, 0x02030, 0x02030, 0x02032, 0x02033, 0x02035, 0x02035, 0x0203b, 0x0203b, 0x0203e, 0x0203e, 0x02074, 0x02074, 0x0207f, 0x0207f, 0x02081, 0x02084, 0x020ac, 0x020ac, 0x02103, 0x02103, 0x02105, 0x02105, 0x02109, 0x02109, 0x02113, 0x02113, 0x02116, 0x02116, 0x02121, 0x02122, 0x02126, 0x02126, 0x0212b, 0x0212b, 0x02153, 0x02154, 0x0215b, 0x0215e, 0x02160, 0x0216b, 0x02170, 0x02179, 0x02189, 0x02189, 0x02190, 0x02199, 0x021b8, 0x021b9, 0x021d2, 0x021d2, 0x021d4, 0x021d4, 0x021e7, 0x021e7, 0x02200, 0x02200, 0x02202, 0x02203, 0x02207, 0x02208, 0x0220b, 0x0220b, 0x0220f, 0x0220f, 0x02211, 0x02211, 0x02215, 0x02215, 0x0221a, 0x0221a, 0x0221d, 0x02220, 0x02223, 0x02223, 0x02225, 0x02225, 0x02227, 0x0222c, 0x0222e, 0x0222e, 0x02234, 0x02237, 0x0223c, 0x0223d, 0x02248, 0x02248, 0x0224c, 0x0224c, 0x02252, 0x02252, 0x02260, 0x02261, 0x02264, 0x02267, 0x0226a, 0x0226b, 0x0226e, 0x0226f, 0x02282, 0x02283, 0x02286, 0x02287, 0x02295, 0x02295, 0x02299, 0x02299, 0x022a5, 0x022a5, 0x022bf, 0x022bf, 0x02312, 0x02312, 0x02460, 0x024e9, 0x024eb, 0x0254b, 0x02550, 0x02573, 0x02580, 0x0258f, 0x02592, 0x02595, 0x025a0, 0x025a1, 0x025a3, 0x025a9, 0x025b2, 0x025b3, 0x025b6, 0x025b7, 0x025bc, 0x025bd, 0x025c0, 0x025c1, 0x025c6, 0x025c8, 0x025cb, 0x025cb, 0x025ce, 0x025d1, 0x025e2, 0x025e5, 0x025ef, 0x025ef, 0x02605, 0x02606, 0x02609, 0x02609, 0x0260e, 0x0260f, 0x02614, 0x02615, 0x0261c, 0x0261c, 0x0261e, 0x0261e, 0x02640, 0x02640, 0x02642, 0x02642, 0x02660, 0x02661, 0x02663, 0x02665, 0x02667, 0x0266a, 0x0266c, 0x0266d, 0x0266f, 0x0266f, 0x0269e, 0x0269f, 0x026be, 0x026bf, 0x026c4, 0x026cd, 0x026cf, 0x026e1, 0x026e3, 0x026e3, 0x026e8, 0x026ff, 0x0273d, 0x0273d, 0x02757, 0x02757, 0x02776, 0x0277f, 0x02b55, 0x02b59, 0x03248, 0x0324f, 0x0e000, 0x0f8ff, 0x0fe00, 0x0fe0f, 0x0fffd, 0x0fffd, 0x1f100, 0x1f10a, 0x1f110, 0x1f12d, 0x1f130, 0x1f169, 0x1f170, 0x1f19a, 0xe0100, 0xe01ef, 0xf0000, 0xffffd, 0x100000, 0x10fffd};  
bool isFullWidth(const int32_t c, const bool isAF) {  
    if (isTableHas(EASTASIAN_FULL, EASTASIAN_FULL_SIZE, c)) return true;  
    if (isAF) return isTableHas(EASTASIAN_AMBIGUOUS, EASTASIAN_AMBIGUOUS_SIZE, c);  
    return false;  
}  
bool isTableHas(const int32_t table[], const size_t size, const int32_t c) {  
    if (c < table[0]) return false;  
    int l = 0, r = size;  
    while (l < r) {  
        const int center = (r - l) / 2 + l;  
        const int num = table[center * 2];  
        if (c < num) {  
            r = center;  
        } else if (c > num) {  
            l = center + 1;  
        } else {  
            return true;  
        }  
    }  
    return l > 0 && c >= table[l * 2 - 2] && c <= table[l * 2 - 1];  
}  

そこまで難しい内容じゃないと思います。ただ割と適当に書いたのでガバガバなところはあるかもしれません。

おわり

ともあれ、これでやっとUTF-8でもShift-JISみたいに全角文字を幅2扱いすることができました。
今までなんとなくすら知ってなかった文字コードについてちょっと勉強になったのでまぁ悪くはないのかなという感想です。
なにぶんにわか知識で調べたにすぎないので、もし何か間違いなどあれば教えていただきたいです。