例外による処理分岐をヘルパーオブジェクトに変換する
AquaSKK のローマ字かな変換は本家 SKK と同じく trie(prefix tree) を利用しています。基本的なアルゴリズムは TSM 版と一緒ですが、IMK 版になってクラスのインタフェースが大きく変化しました。
TSM 版
例外を利用して処理を分岐しています。
CppCFString KanaTreeController::trace(int output_type,CppCFString& queue) { CppCFString result; while (queue.length() > 0) { try { root.trace(queue[0]); } catch (ChildNotFoundException& e) { // rootにqueue[0]の要素が存在しない場合はqueueそのものを返す。この時queueは空にする。 result += queue; queue.clear(); break; } try { // 再帰検索でノードを探す KanaTreeElement& leaf = trace(queue,root); switch (output_type) { case OUTPUT_HIRAGANA: result += leaf.getContentAsHiragana(); break; case OUTPUT_ZENKATA: result += leaf.getContentAsZenKata(); break; case OUTPUT_HANKATA: result += leaf.getContentAsHanKata(); break; } } catch (ReachedEndOfQueueException& e) { // ループを抜ける break; } } return result; }
ノード検索インタフェースはこちら。
KanaTreeElement& KanaTreeController::trace(CppCFString& queue,KanaTreeElement& parent,unsigned level) { try { KanaTreeElement& child = parent.trace(queue[level]); // 例外が起きなかった if (!child.hasAnyChilds()) { // これ以上辿れない。 queue.eraseFirst(level + 1); if (child.getNextState() != 0x00) { queue.append(child.getNextState()); } return child; } else { if (level >= queue.length() - 1) { // これ以上辿れるにも関わらずキューの終わりに来てしまった。 throw ReachedEndOfQueueException(); } else { // 再帰的にトレースを続ける。 return trace(queue,child,level + 1); } } } catch (ChildNotFoundException& e) { // これ以上辿れない。 queue.eraseFirst(level); if (parent.getNextState() != 0x00) { queue.append(parent.getNextState()); } return parent; } }
スタイルの問題ですが、引数の文字列を変更しているのが気になるところです。
初期の IMK 版
例外を使わずにやろうと思って作ったバージョンです。
bool SKKRomanKanaConverter::Execute(SKKInputMode mode, const std::string& in, std::string& out, std::string& next) { bool converted = false; std::string str(in); out.clear(); next.clear(); while(!str.empty()) { int state; const SKKTrie* node = root_.Traverse(str, state); // 変換できた? if(node) { out += node->KanaString(mode); next = node->NextState(); converted = true; } else { converted = false; } // 部分的に一致しているがデータ不足のためこれ以上処理できない if(!state) { next = str; return false; } // 最初の一文字が木に存在しない場合、出力にコピーして次の文字を調べる if(state < 0) { out += str[0]; state = 1; } // 調べた部分を削り取って次の文字を調べる str = str.substr(state); } return converted; }
ノード検索部分は以下のようになっています。
const SKKTrie* SKKTrie::Traverse(const std::string& str, int& state, unsigned depth) { // [1] データ不足(ex. "k" や "ch" など) if(depth == str.size()) { state = 0; return 0; } // 一致? if(children_.find(str[depth]) != children_.end()) { SKKTrie* node = &children_[str[depth]]; // 末端でないなら再帰検索 if(!node->children_.empty()) { return node->Traverse(str, state, depth + 1); } // [2] 完全一致 state = depth + 1; return node; } // [3] 部分一致(ex. "kb" や "chm" など) if(0 < depth) { state = depth; // 節かつ葉でもある「n」のような場合には、一致として扱う if(leaf_) { return this; } return 0; } // [4] 最初の一文字が木に存在しない state = -1; return 0; }
例外がなくなったかわりに謎の引数が増え、戻り値の解釈も複雑になってしまいました。これではとても改善とは言えませんねぇ。
現在の IMK 版
悩んだ末に辿り着いたのが、ヘルパーオブジェクトを導入したバージョンです。
bool SKKRomanKanaConverter::Convert(SKKInputMode mode, const std::string& str, SKKRomanKanaConversionResult& result) { bool converted = false; std::string queue(str); result = SKKRomanKanaConversionResult(); while(!queue.empty()) { ConversionHelper helper(mode, queue); root_.Traverse(helper); result.output += helper.Output(); result.next = helper.Next(); result.intermediate = helper.Intermediate(); converted = helper.IsConverted(); if(helper.IsShort()) { result.next = helper.Queue(); break; } queue = helper.Queue(); } return converted; }
検索部分はこちら。
void SKKTrie::Traverse(SKKTrieHelper& helper, unsigned depth) { const std::string& str = helper.SKKTrieRomanString(); // [1] データ不足(ex. "k" や "ch" など) if(depth == str.size()) { if(IsLeaf()) { helper.SKKTrieNotifyIntermediate(this); } return helper.SKKTrieNotifyShort(); } // 一致? if(children_.find(str[depth]) != children_.end()) { SKKTrie* node = &children_[str[depth]]; // 末端でないなら再帰検索 if(!node->children_.empty()) { return node->Traverse(helper, depth + 1); } // [2] 完全一致 helper.SKKTrieNotifyConverted(node); helper.SKKTrieNotifySkipLength(depth + 1); return; } // [3] 部分一致(ex. "kb" や "chm" など) if(0 < depth) { if(IsLeaf()) { helper.SKKTrieNotifyConverted(this); } helper.SKKTrieNotifySkipLength(depth); return; } // [4] 最初の一文字が木に存在しない helper.SKKTrieNotifyNotConverted(str[0]); helper.SKKTrieNotifySkipLength(1); }
ConversionHelper は SKKRomanKanaConverter.cpp の内部で定義されています。
class ConversionHelper : public SKKTrieHelper { bool converted_; bool short_; SKKInputMode mode_; std::string queue_; std::string output_; std::string next_; std::string intermediate_; virtual const std::string& SKKTrieRomanString() const { return queue_; } virtual void SKKTrieNotifyConverted(const SKKTrie* node) { converted_ = true; output_ += node->KanaString(mode_); next_ = node->NextState(); intermediate_.clear(); } virtual void SKKTrieNotifyIntermediate(const SKKTrie* node) { intermediate_ = node->KanaString(mode_); } virtual void SKKTrieNotifyNotConverted(char code) { converted_ = false; output_ += code; } virtual void SKKTrieNotifySkipLength(int length) { queue_ = queue_.substr(length); } virtual void SKKTrieNotifyShort() { short_ = true; } public: ConversionHelper(SKKInputMode mode, const std::string& queue) : converted_(false), short_(false), mode_(mode), queue_(queue) {} const std::string& Output() const { return output_; } const std::string& Next() const { return next_; } const std::string& Intermediate() const { return intermediate_; } const std::string& Queue() const { return queue_; } bool IsConverted() const { return converted_; } bool IsShort() const { return short_; } };
やあやあやあ。随分とコードが増えてしまいました。これは改善と言えるのかな。ううむ。微妙ですね。しかし、プロトコルが明確になったおかげでコードは鬱陶しいくらい説明的になりました。