改行コード変換には、改行コードを変換する機能のオマケとして、正規表現を用いた文字列置換を行うための機能が付いているのですが、正規表現を使うほどではない、普通の文字列置換をする場合に不便に感じまして、正規表現を使わない、ごく普通の文字列置換機能を実装することにしました。
ちなみに現在、正規表現を用いた文字列置換については、Boost C++ Librariesのregex_replace()を用いておりますが、非常に簡単に記述できます。
が、逆に、正規表現に寄らない文字列置換をしようとしたとき、パッとその方法が思い浮かばないことに気づきました。あまりにも思い浮かばなかったので、regex_replace()に登場するすべて特殊記号の直前に \ 記号を入れようかと思ったほど。
でも何かしら、通常の STL を使った方法があるんじゃないかと、模索してみることにしました。
ちなみにSTLのbasic_stringの文字列置換は、文字位置と文字列長を指定した方法しかないみたいで、何かもっとシンプルな方法は本当にないかなぁと探してみたのですが、ググるとやっぱり、文字位置と文字列長を指定した方法を使うのが一番良さそう。
とりあえず見つけた方法については、こちらのページを参照で。この方法を採用することにしました。
// 参照サイト:http://d.hatena.ne.jp/kobapan/20090208/1234022527
/** * 文字列中から文字列を検索して別の文字列に置換する * @param str : 置換対象の文字列。上書かれます。 * @param from : 検索文字列 * @param to : 置換後の文字列 */ void strReplace (std::string& str, const std::string& from, const std::string& to) { std::string::size_type pos = 0; while(pos = str.find(from, pos), pos != std::string::npos) { str.replace(pos, from.length(), to); pos += to.length(); } }
次に考えたのが、通常の文字列置換において、アルファベットの大文字と小文字を区別させずに置換を行う方法です。パッと思い浮かんだ方法としては、上記の方法では basic_string::find を使っていますから、basic_string に与えるトレイトを差し替えて、アルファベットの大文字小文字が違っても同一と見なしてくれるようなルールにしてしまえば良いのではないかと思いました。
ちなみに、アルファベットの大文字・小文字のことをcaseと呼び、大文字・小文字の区別をすることをcase-sensitiveと呼びます。
その昔、アルファベットの大文字を印刷するための判子を入れたケースが上段に配置され、アルファベットの小文字を印刷するための判子を入れたケースが下段に配置され、それぞれが upper-case, lower-case と呼ばれていたことから、文字の大小そのものを case と呼ぶんだと聞いたことがありますね。
トレイトの差し替えで大文字小文字を区別しない方法としては、こちらのページに書かれた方法が使えそうです。
// 参照サイト:http://ambiesoft.fam.cx/blog/archives/2226
using namespace std; struct ci_char_traits : public char_traits < char > { static int compare(const char *s1, const char *s2, size_t n) { return memicmp(s1, s2, n); } }; // 文字列の宣言の部分 basic_string<char, ci_char_traits> cistring;
どうやら basic_string::find() は、内部的に compare を使うらしいので、ここをいじれば大文字・小文字を無視した basic_string::find() が実現できるらしいんですね。というわけで、作ってみました。
ちなみに、プログラムはUnicodeで動くので、全体的に wchar_t を使って作ってあります。
_wcsnicmp()は、アルファベットの大文字・小文字を無視し、文字数指定でコンペアしてくれる関数strnicmp()のUnicode版となります。
#include <string> #include <stdio.h> #include <string.h> using namespace std; struct ci_char_traits : public char_traits < wchar_t > { static int compare(const wchar_t *s1, const wchar_t *s2, size_t n) { return _wcsnicmp(s1, s2, n); } }; int wmain() { basic_string<wchar_t, ci_char_traits> wstr1(L"ABCDE"); basic_string<wchar_t, ci_char_traits> wstr2(L"bcd"); basic_string<wchar_t, ci_char_traits>::size_type pos; pos = wstr1.find(wstr2, 0); if (pos == wstring::npos) { puts("見つかりませんでした。"); } else { printf("%d文字目に見つかりました。\n", pos); } return 0; }
ところが、これを実行してみたら「見つかりませんでした。」と表示されてしまった…。何でだろ…どっかバグが入り込んでいるか…。
いろいろな情報を総合すると、これで合っていそうな気がするんですけどね…。よくわかりません。
ちなみに、wstr2を"BCD"と大文字にすれば、正しく動作するみたいだし…。うーん。
デバッグ実行してみると、どうも、compare() の呼び出し回数がおかしいような気がするので、もしかすると、Visual Studio 2010による basic_string::find() の実装では、compare以外も使った検索が行われているのかも知れません。lt(), eq() のような、大小を定義するための関数が他にもありますし、そういったものも定義しないといけないのかも知れません。
とにかく、basic_string::find()の実装を知らないとどうしようもないことが分かりましたし、他に自分の気づかないところで大バグがあるかもしれないし、方法自体を大きく変えることにします。
というわけで、最終的な実装は、残念ながらこんな感じになりました。
// 参照サイト:http://minus9d.hatenablog.com/entry/20130119/1358603761
wstring str_low = str; std::transform(str_low.begin(), str_low.end(), str_low.begin(), ::tolower); wstring from_low = from; std::transform(from_low.begin(), from_low.end(), from_low.begin(), ::tolower); while(pos = str_low.find(from_low, pos), pos != wstring::npos) { str.replace(pos, from_low.length(), to); str_low.replace(pos, from_low.length(), to); pos += to.length(); }
うーん、ちょっと美しくない感じがする…。文字列の小文字化については、こちらのページを参照。
まずはいったん、対象文字列と検索文字列を小文字化し、その中で検索と置換を実施します。それと並行して、小文字化していない元の文字列についても、全く同じ位置に対して、置換を実施します。最終的な出力文字列としては、小文字化していない方の結果を使うという感じです。
とりあえず、これでうまく動いたので、本日のβ版ではこの方法にて実装をしています。