文字列以下略・実践編

http://www.hi-matic.org/diary/?20080401#01-1
http://www.hi-matic.org/diary/?20080403#03-1

深く理解していない分野について偉そうに語ってしまい申し訳ございません。ただC標準とはいえWindowsですとUTF-16になる(からパックとも言い切れない)関数なんですよmbなんたらは。
I18N本はやっぱり買う必要があるのかなあ……高い。

やっぱり「なぜ(内部文字コードを決めずに)wchar_tを使うのか?」とか書いた方がいいのかね。

お願いしますお願いします。

さてBEST SOFTWARE WRITINGを読んでいたら、ストーリーが無いといけないみたいな話があり、激しく同意したので、ストーリーを付けます。
昔々、おじいさんとおばあさんがいました。おじいさんは、山へ芝刈りに、おばあさんは、川で突然Pascalソースからコメントを除去したくなりました。めでたしめでたし。

function RemoveComment(const S: string): string;
var I, Start: Integer;
begin
  Result := '';
  I := 1;
  while I <= Length(S) do
  begin
    if (I + 1 <= Length(S)) and (S[I] = '/') and (S[I + 1] = '/') then {remove //}
    begin
      repeat Inc(I) until S[I] = #10;
      Result := Result + #13#10;
      Inc(I)
    end
    else if (S[I] = '{') and not ((I + 1 <= Length(S)) and (S[I + 1] = '$')) then //remove {..}
    begin
      repeat Inc(I) until S[I] = '}';
      Inc(I)
    end
    else if S[I] = '''' then
    begin
      Start := I;
      repeat Inc(I) until S[I] = '''';
      Result := Result + Copy(S, Start, I - Start + 1);
      Inc(I)
    end
    else
    begin
      Result := Result + S[I];
      Inc(I)
    end
  end
end;

状態作る方が好きな人もおられるかとおもいますが、まあ好きな方でっ。

さてこれをMBCS対応させます。
まずこの処理中、多バイト文字を切ってしまうようなインデックスを作ってしまいそうな箇所を探します。インデックスはIしか使っておらず、最初は1ですのでこれは問題ないです。残るは、これだけです。

Inc(I)

何度かInc(I)が出てきますが、Inc(I)実行時にIが多バイト文字の先頭を指していた場合、次のIは多バイト文字の2バイト目以降になってしまい、2バイト目に他の箇所で比較している記号類があったら、誤動作してしまいします。
ですからInc(I)をこう置き換えます。

if IsDBCSLeadByte(Ord(S[I])) then Inc(I, 2) else Inc(I)

IsDBCSLeadByteはWindowsロケール設定を見てくれますし、WindowsのDBCS対応では3バイト以上の文字があるコード体系は無いことになっていますので、これで各国語版のWindows上ではその国の文字コードを正しく処理できます。勿論日本ならSJISです。
Delphiにはこういうものもあります。

if S[I] in LeadBytes then Inc(I, 2) else Inc(I)

やっていることは完全に同一です。
コードページを指定したい時はIsDBCSLeadByteEx APIもありますし、対象となる文字コードが決まっているなら勿論自前で判定してもOKです。
普通はここまでの対応で充分です、が、ここではもう一捻りしましょうか。
Delphiのソースは、Windowsのコードページと、UTF-8と、UTF-16で書くことができます。UTF-16を無理矢理AnsiStringに詰めても、ASCIIコードの部分にすら1バイト置きに#0が入って共通性が無いため、UTF-16はあきらめます。UTF-16に限らず、ソースコード上にリテラルで記号類を書いてしまっているため、ASCII部分に互換性が無い文字コードはすべて、知ったことか、ですね。
あくまで念押しで書いておきますと、文字数単位で処理をしたところで、何の解決にもならないですよ。複数の文字コードに対応するかどうか、ASCII部分を決め打つかどうかと、文字数単位で処理を行うかは全く別の話ですので。
それで、UTF-16はあきらめるのですが、UTF-8はASCII部分は共通ですし、拾っておきたいです。しかしUTF-8は3バイト以上の文字があるため、DBCS(double以下略)対応だけでは不足です。
そこでこんなものを用意しましょう。

type Tmblen = function(const S: string; Index: Integer): Integer;

何の工夫も無いmblenのPascal版を指せる関数ポインタ型です。文字列を渡す引数型はPCharでもいいんですがいちおうstringで、長さはstringが覚えてますのでサイズを渡す引数は無しです。その代わり文字列の途中の文字を対象にできるよう、インデックスを渡します。
実際の処理を行う関数はこんなで。

function DBCSLen(const S: string; Index: Integer): Integer;
begin
  if IsDBCSLeadByte(Ord(S[Index])) then Result := 2 else Result := 1
end;
function UTF8Len(const S: string; Index: Integer): Integer;
begin
  case Ord(S[Index]) of
  $c0..$df: Result := 2;
  $e0..$ef: Result := 3;
  $f0..$f7: Result := 4;
  $f8..$fb: Result := 5;
  $fc..$fd: Result := 6;
  else Result := 1;
  end
end;

で、Tmblenを引数に追加して、Inc(I)を書き換えます。今度はifも要らないですね。

function RemoveComment(const S: string; mblen: Tmblen): string;
var I, Start: Integer;
begin
  Result := '';
  I := 1;
  while I <= Length(S) do
  begin
    if (I + 1 <= Length(S)) and (S[I] = '/') and (S[I + 1] = '/') then {remove //}
    begin
      repeat Inc(I, mblen(S, I)) until S[I] = #10;
      Result := Result + #13#10;
      Inc(I, mblen(S, I))
    end
    else if (S[I] = '{') and not ((I + 1 <= Length(S)) and (S[I + 1] = '$')) then //remove {..}
    begin
      repeat Inc(I, mblen(S, I)) until S[I] = '}';
      Inc(I, mblen(S, I))
    end
    else if S[I] = '''' then
    begin
      Start := I;
      repeat Inc(I, mblen(S, I)) until S[I] = '''';
      Result := Result + Copy(S, Start, I - Start + 1);
      Inc(I, mblen(S, I))
    end
    else
    begin
      Result := Result + S[I];
      Inc(I, mblen(S, I))
    end
  end
end;

後はDBCSLenとUTF8Len、どちらかの関数ポインタを渡せばOKという寸法です。

DBCSもUTF-8も先頭見るだけで文字のバイト数がわかるため、Tmblenの引数はCharだけでいいのですが、元々のmblenの仕様を見るにきっと世の中にはそれでは済まない文字コードがあるのでしょう、知らんけど。

文字単位の処理を行いたい時はpacked形式推奨と何度か書きました。

でもPascalソースからコメントを除去するぐらいでしたら、コメントを除去する処理そのものが、元々先頭から見ていくしかないため(検索関数でいきなり"//"まで飛んだりしたら、そこは文字列の中かもしれない)、必然的にインデックスをインクリメントしていく処理になります。そこに多バイト文字をスキップさせる記述を挿むぐらいはなんでもないですよね。むしろ逆に文字単位で処理をしようとしたり、コード変換をする方が考えるのも書くのも面倒です。字句解析と呼ばれるようなジャンルは大抵そうですね。(内心は、最初、状態で書いておいたほうが、Inc(I)が一箇所だけになるので、MBCS対応はこんなに楽だぜ!と言い易かったなーと思っております。まあいいや。主張を通すために意図的に選んだ例に意味は無い、と負け惜しみを言う……ああ、本当の本当は、コメントの除去なんてフィルタ動作の典型ですので、入力も出力も文字列ではなくストリームが正解です。そういう意味ではこれも例のための例にしかなってないので、負け惜しみにすらならない……)

packed形式は、後ろから文字列を辿る必要があったり、ランダムアクセスの必要があったり、文字数比較が必要があったりするアルゴリズムで特に有効です。

例えば文字列の類似度(有名なのはLevenshtein距離)を求めるときに、「あ(82/a0)」と「い(82/a2)」が先頭バイトが同じだからって、「あ」と「ア(83/41)」よりも「あ」と「い」の方が近いことにされてしまったら嫌ですよね。
例えばパッチを作るとき、多バイト文字の先頭バイトだけ違うからといって、先頭バイトだけ差し替えるパッチは嫌ですよね。普通のパッチは行単位なのでそういうことは起きないどころかとにかく行が同じか違うかだけわかればよいので改行コードが2バイト目に来るような嫌らしい文字コード(実在するかは不明)を相手にしない限り多バイト文字の事なんか考えなくても関係ないのですが、仮に文字単位のパッチを作りたいなら意識する必要は有ります。

勿論言いたい事は、文字単位の処理はこういった必要なところでは使わざるをえないですが、そうではない、不要なところでは、使うなダメ絶対バイナリイメージ素通ししてくれって事ですけれども。

さて能書きたれて珍しくパフォーマンスも計って例も書いた。書くことがなくなったので終わります。

以下読まないこと。
これ書き始めた最初の動機のほうは、いささか誤解が残っているような気もしますが、まああんだけ通じてればOKの範疇か……誤解のひとつに、私がスルーしたいと思ったDelphiユーザー(達)が、誰を指しているのか、そして誰を指していないのかというのもあるのですが、あえて訂正しないのが大人の対応というものでしょう。(←子供)