UTF-8 をいったりきたり

AquaSKK のベータ版をリリースしました。これでまた AppleScript を忘れてしまいそうです。


さて、新エンジンで文字列と言えば std::string であり、中身は UTF-8 です。これまでのところは何の工夫もないまま、std::string に助けられて順調に進んできたのですが、末尾の文字を削除する Backspace イベントの扱いではたと立ち止まってしまいました。


UTF-8 では文字を構成するバイト数が 1 〜 6 と可変なので、まずは、末尾の一文字が何バイトかを知る必要があります。Core Foundation にはこの手の API がありそうですが、エンジン部分はポータブルにしたいので採用できません。かと言って、std::string にある全てのメソッドをラップした重量級の utf8string クラスを独自に実装するのも避けたい。


そこで、WikipediaUTF-8 を参考にしながら、std::string にカチャっと装着する iterator もどきを考えてみました。
http://ja.wikipedia.org/wiki/UTF-8

class utf8iterator {
    unsigned pos_;
    const std::string& str_;

    unsigned size() const {
	static unsigned table[] = {
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1, // 0x00
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1,
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1,
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1,
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1,
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1,
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1,
	    1, 1, 1, 1, 1, 1, 1, 1,	1, 1, 1, 1, 1, 1, 1, 1,
	    0, 0, 0, 0, 0, 0, 0, 0,	0, 0, 0, 0, 0, 0, 0, 0, // 0x80
	    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,
	    2, 2, 2, 2, 2, 2, 2, 2,	2, 2, 2, 2, 2, 2, 2, 2, // 0xc0
	    2, 2, 2, 2, 2, 2, 2, 2,	2, 2, 2, 2, 2, 2, 2, 2,
	    3, 3, 3, 3, 3, 3, 3, 3,	3, 3, 3, 3, 3, 3, 3, 3, // 0xe0
	    4, 4, 4, 4, 4, 4, 4, 4,	5, 5, 5, 5, 6, 6, 0, 0  // 0xf0
	};

	return table[(unsigned char)str_[pos_]];
    }

    bool leadbyte() const {
	return size() != 0;
    }

    utf8iterator();

public:
    utf8iterator(std::string& str) : pos_(0), str_(str) {}

    bool good() const {
	return pos_ < str_.size();
    }

    operator unsigned() const {
	return pos_;
    }

    unsigned charsize() const {
	return size();
    }

    std::string operator*() const {
	return good() ? str_.substr(pos_, size()) : "";
    }

    utf8iterator& first() {
	pos_ = -1;
	return next();
    }

    utf8iterator& last() {
	pos_ = str_.size();
	return prev();
    }

    utf8iterator& next() {
	for(++ pos_; good() && !leadbyte(); ++ pos_) {}
	return *this;
    }

    utf8iterator& prev() {
	for(-- pos_; good() && !leadbyte(); -- pos_) {}
	return *this;
    }
};

utf8iterator を書いたことで、文字単位の移動*1がお手軽になりました。substr、replace、erase などの更新操作は、std::string のメソッドをそのまま使う戦略です。begin、end、++、-- といったお馴染のインタフェースとは非互換ですが、単純明快なので使い方を迷うこともないと思います。


UTF-8 な str の末尾文字を削除するなら、以下のように書きます。

utf8iterator iter(str);
str.erase(iter.last());

カチャっと装着してスチャっと位置を取得する。これで目的は達成できました。次は、補完候補の管理を検討する中で生まれた汎用 ring buffer を取り上げたいと思います。

*1:next() はもっと効率的に書けると思いますが、prev() との対称を取って素朴な実装にしてます。AquaSKK で扱う文字列は短いので、まあ、これで充分かなぁと。