改行コードを変換する関数(超高速文字列処理)
55 HighSpeedStringConvertCRLF 動作確認 Delphi2007 更新日 2008/01/30(水)

改行コードがCRLF,CR,LFのテキストを
CRLFに変換するのはVCL標準で AdjustLineBreakes という関数があります。
(D6からクロスプラットホーム向けに
 LFのみの改行コードにも変換できるようになっています)

でも、Macで使われるCRのみの改行コード変換には使えないので
どんな改行コードのテキストでも
CRLF,CR,LF の改行コードに変換してしまう関数を作りました。

その際に文字列を扱う上で高速化する手法が
非常に勉強になりましたのでまとめておきます。
※2chで教えてもらいました。ありがとう名無しのみなさん。

仕様としては
・改行コードをCRLF,CRのみ,LFのみの形式の文字列にして出力する
・LFCRというコードが出てきたら一つの改行コードとして判断する
・文字列中に CR,LF,CRLF,LFCR が混在していても改行コードをそろえる
こんな感じです。

まずは eXtreme Programming (=XP)のテストコードです。
XPではテストを先に書く事が推奨されます。

これで文字列変換の細かな仕様が確認できます。
────────────────────
type
  TTextLineBreakStyleMultiPlatform = (tlbsmpCRLF, tlbsmpCR, tlbsmpLF);
  //CRLF:Windows    LF:Unix/Linux   CR:Mac

//XPテスト
procedure Check(A, B: String);
begin
  if not(A = B) then
  begin
    raise Exception.Create('エラーです ' + A + ':' + B);
  end;
end;

procedure testChangeLineBreakes;

    //総当りする関数
    procedure CheckFunc(Source, CRLFResult, CRResult, LFResult: String);
    begin
      Check(CRLFResult, ChangeLineBreakes(Source, tlbsmpCRLF));
      Check(CRResult  , ChangeLineBreakes(Source, tlbsmpCR));
      Check(LFResult  , ChangeLineBreakes(Source, tlbsmpLF));

      Check(CRResult  , ChangeLineBreakes(CRLFResult, tlbsmpCR));
      Check(LFResult  , ChangeLineBreakes(CRLFResult, tlbsmpLF));

      Check(CRLFResult, ChangeLineBreakes(CRResult  , tlbsmpCRLF));
      Check(LFResult  , ChangeLineBreakes(CRResult  , tlbsmpLF));

      Check(CRLFResult, ChangeLineBreakes(LFResult  , tlbsmpCRLF));
      Check(CRResult  , ChangeLineBreakes(LFResult  , tlbsmpCR));
    end;

begin
  //普通に変換
  CheckFunc(#10#13+'あいう'+#10#13+'えお'+#10#13,
            #13#10+'あいう'+#13#10+'えお'+#13#10,
            #13   +'あいう'+#13   +'えお'+#13,
            #10   +'あいう'+#10   +'えお'+#10);

  //混在
  CheckFunc(#13#10+'123'+#10#13+'ABC'+#13   +'あいう'+#10   ,
            #13#10+'123'+#13#10+'ABC'+#13#10+'あいう'+#13#10,
            #13   +'123'+#13   +'ABC'+#13   +'あいう'+#13   ,
            #10   +'123'+#10   +'ABC'+#10   +'あいう'+#10   );

  //連続
  CheckFunc(#10#13#10#13+'ABC'+#10#13#10#13+'あいう'+#10#13#10#13,
            #13#10#13#10+'ABC'+#13#10#13#10+'あいう'+#13#10#13#10,
            #13#13      +'ABC'+#13#13      +'あいう'+#13#13      ,
            #10#10      +'ABC'+#10#10      +'あいう'+#10#10      );

  //混在して連続
  CheckFunc(#10#13#13   +'123'+#13#13#10   +'ABC'+#10#10#13   ,
            #13#10#13#10+'123'+#13#10#13#10+'ABC'+#13#10#13#10,
            #13#13      +'123'+#13#13      +'ABC'+#13#13      ,
            #10#10      +'123'+#10#10      +'ABC'+#10#10      );

  CheckFunc(#10#13#13   +'123'+#13#10#13   +'あいう'+#10#13#10   ,
            #13#10#13#10+'123'+#13#10#13#10+'あいう'+#13#10#13#10,
            #13#13      +'123'+#13#13      +'あいう'+#13#13      ,
            #10#10      +'123'+#10#10      +'あいう'+#10#10      );

  //改行数のチェック
  CheckFunc(#10#13#10#13+
            'ABC'+#10#13#10#13#10#13+
            '123'+#10#13#10#13#10#13#10#13+
            'あいうえ'+#10#13#10#13#10#13#10#13#10#13,

            #13#10#13#10+
            'ABC'+#13#10#13#10#13#10+
            '123'+#13#10#13#10#13#10#13#10+
            'あいうえ'+#13#10#13#10#13#10#13#10#13#10,

            #13#13      +
            'ABC'+#13#13#13         +
            '123'+#13#13#13#13            +
            'あいうえ'+#13#13#13#13#13               ,

            #10#10      +
            'ABC'+#10#10#10         +
            '123'+#10#10#10#10            +
            'あいうえ'+#10#10#10#10#10               );
end;
────────────────────
このテストコードを通過する ChangeLineBreakes 関数なら
実装はどんなものでもよいとします。

これで心おきなくリファクタリングできるわけです。

始めは全く高速化など考えずにコーディングしてみました。
AdjustLineBreakesを行って
文字列をCRLFに変換してからCRだけかLFだけを削除するコードです。

────────────────────↓相当遅いコード
function ChangeLineBreakes(const S: String; Style: TTextLineBreakStyleMultiPlatform): String;
const
  CR: Char = #$D; //#13
  LF: Char = #$A; //#10

  //改行コードを削除します
  //LineBreakesには#13か#10を指定
  function DeleteCRLF(S: string; LineBreakes: Char): string;
  var
    Id: Integer;
  begin
    while True do begin
      Id := AnsiPos(LineBreakes, S);
      if Id = 0 then break;
      Delete(S, Id, 1)
    end;
    Result := S;
  end;

  //改行コードがLFCRになっている場合CRLFに変換するコード
  //この関数はまあまあ高速だと思います。
  function LFCRConvertCRLF(S: String): String;
  var
    Id: Integer;
    i: Integer;
  begin
    //↓単独でLFCRが存在するときだけCRLFに変換する
    //  (CRLFCRLFの時LFCRを変換されては困る)
    if ( S[1]= LF ) and
       ( S[2]= CR ) then
    begin
      S[1] := CR;
      S[2] := LF;
    end;

    for i := 2 to Length(S)-2 do
    begin
      if ( S[i-1]<>CR ) and
         ( S[i  ]= LF ) and
         ( S[i+1]= CR ) then
      begin
        S[i  ] := CR;
        S[i+1] := LF;
      end;
    end;

    i := Length(S) -1;
    if ( S[i-1]<>CR ) and
       ( S[i  ]= LF ) and
       ( S[i+1]= CR ) then
    begin
      S[i  ] := CR;
      S[i+1] := LF;
    end;

    Result := S;
  end;

begin
  Result := LFCRConvertCRLF(S);
  Result := AdjustLineBreaks(Result);

  case Style of
    tlbsmpCRLF:
      Result := Result;
    tlbsmpLF:
      Result := DeleteCRLF(Result, CR);
    tlbsmpCR:
      Result := DeleteCRLF(Result, LF);
  else
    Assert(False, '');
  end;
end;
────────────────────↑相当遅いコード

AdjustLineBreaksはLFCRの改行コードを二つの改行と
認識してしまうので先にLFCRをCRLFに変換する処理をいれました。

テストは通過するのですが、とりあえずこのやり方は動作があまりにも遅く
数百KBのテキストを変換するのに、待ち時間がかなりあるので困りました。

文字列に対して1文字削除処理や
新しい文字列を定義してコピーしたりすると
その都度メモリが確保されて遅くなっている
ということがわかりまして、

それを改善して高速化するために
戻り値の文字列に対して
最初に一度SetLengthをして
元の文字列から改行コードを変換したものを
代入していき、処理が終わったら
最後に改めてSetLengthを行って文字列長を調整する
というコードに改善してみました。

────────────────────↓かなり速いコード
function ChangeLineBreakes(const S: String; Style: TTextLineBreakStyleMultiPlatform): String;
const
  CR =#$D; //#13
  LF =#$A; //#10

  //改行がいくつあるかをカウントする
  function CountCRLF(S: String): Integer;
  var
    i: Integer;
  begin
    Result := 0;
    for i := 1 to Length(S)-1 do
    begin
      case S[i] of
        CR: begin
          //CRで次がLFじゃないなら
          if not (S[i+1]=LF) then
            Inc(Result);
        end;
        LF: begin
          //LFで次がCRじゃないなら
          if not (S[i+1]=CR) then
            Inc(Result);
        end;
      else ;
      end;
    end; //for i
  end;

var
  ReadIndex, WriteIndex, i: Integer;
  ReplaceStr: String;
label putCRLF;
begin
  case Style of
    tlbsmpCRLF: ReplaceStr := CR+LF;
    tlbsmpCR  : ReplaceStr := CR;
    tlbsmpLF  : ReplaceStr := LF;
  end;
  ReadIndex:=1;
  WriteIndex:=1;
  //現在の文字列長と最悪改行の数だけバイト数増えた分をメモリ確保
  SetLength(Result,Length(S)+CountCRLF(S));

  while ReadIndex<=Length(S) do begin
    case S[ReadIndex] of
      CR: begin
        //見ているのが最後の文字じゃなくて
        if not (ReadIndex=Length(S)) then
          //次の文字がLFなら
          if S[ReadIndex+1] =LF then inc(ReadIndex);
          //ReadIndexで見る場所を一つ飛ばす
        goto putCRLF;
      end;

      LF: begin
        //見ているのが最後の文字じゃなくて
        if not (ReadIndex=Length(S)) then
          //次の文字がCRなら
          if S[ReadIndex+1] =CR then inc(ReadIndex);
          //ReadIndexで見る場所を一つ飛ばす

        //Resultの文字を改行コードの所だけ入力する
        putCRLF:
        for i:=1 to Length(ReplaceStr) do  Result[WriteIndex+i-1]:=ReplaceStr[i];
        inc(WriteIndex,length(ReplaceStr));
      end;

      else begin
        //CRでもLFでもない文字はそのまま移動
        Result[WriteIndex]:=S[ReadIndex];inc(WriteIndex);
      end;
    end;
    inc(ReadIndex);
  end; //while
  SetLength(Result,WriteIndex-1);
end;
────────────────────↑かなり速いコード

これでいきなり前のものと比べて1000倍近く高速化されました。

それから更に高速化を目指すことが可能です。
教えてもらった事ですが
ポイントとして3つあります。

1、Length(S)  は const S と定義した変数を使ったとしても
	関数に展開されるのでループ内で毎回Length(S)を行うと遅い。
    一度、A:=Length(S)として変数に代入して
    必要ならAの内容を見るべき。

2、文字列への Moji[ i ]:= という代入は内部で関数に置換されてしまう 
    高速化するなら文字列ではなく文字配列にするといい(ポインタでもいい)
    type StrArray = array[1..$10] of char;
    //この場合$10は無視されるので適当な宣言でよいらしい
    var PStr: ^StrArray; 
    PStr := @S[1]; として準備しておく。
    既存コードの S[ReadIndex] を PStr^[ReadIndex] と記述変更するべき

3、ReplaceStr[0]=CR; ReplaceStr[1]=LF;などの時
	ループ内の改行コードを代入するコードの部分が
    for i:=1 to Length(ReplaceStr) do  Result[WriteIndex+i-1] := ReplaceStr[i];
    ここで遅くなっている。
    var ReplaceStr: array[0..1]of char;  と宣言しなおして
    1.Result[WriteIndex  ] := ReplaceStr[0]; 
    2.Result[WriteIndex+1] := ReplaceStr[1]; 
    として改行コードを代入すると高速。
    ReplaceStrがCR単独の時は[2.]の部分は無駄なコピーだけど
    比較して禁止するにもコードが必要なので比較コードも書かない。

ループ中に最終文字列かどうかをチェックする
条件判断コードも取り除く工夫をして

上記を踏まえてより高速化を行ってみました。
これで先ほどのコードより3倍ほど高速化されます。

────────────────────↓ものすごく速いコード
{-------------------------------
//文字列の改行コードをそろえます
機能:       改行コードを
            WinCRLF形式 MacCR形式 UnixLF形式
            で相互変換します
引数説明:   S: 元の文字列
            Style: 変換する形式指定
戻り値:     変換された文字列
備考:
履歴:       2002/03/16
//------------------------------}
function ChangeLineBreakes(const S: String; Style: TTextLineBreakStyleMultiPlatform): String;
type StrArray = array[1..$10] of char;
const
  CR = #$D; //#13
  LF = #$A; //#10

  //CRとLFがいくつあるかをカウントする
  function CountCRLF(const S: String): Integer;
  var
    i: Integer;
    PS: ^StrArray;
  begin
    Result := 0;
    PS := @S[1];
    for i := 1 to Length(S) do
    begin
      case PS^[i] of
        CR: begin
            Inc(Result);
        end;
        LF: begin
            Inc(Result);
        end;
      else ;
      end;
    end; //for i
  end;

var
  ReadIndex, WriteIndex, SourceLength: Integer;
  ReplaceStrLen: Integer;
  ReplaceChar: array[0..1] of Char;
  PResultStr, PSourceStr: ^StrArray;
begin
  if S = '' then begin Result := ''; Exit; end;

  SourceLength := Length(S);
  case Style of
    tlbsmpCRLF:
    begin
      ReplaceChar := CR+LF;
      ReplaceStrLen := 2;
      SetLength(Result, SourceLength+CountCRLF(S));
      //↑最大で改行コードの数だけ
      //  文字列長が増加する可能性があるので
      //  メモリを確保している
    end;

    tlbsmpCR:
    begin
      ReplaceChar := CR+#0;
      ReplaceStrLen := 1;
      SetLength(Result, SourceLength);
    end;

    tlbsmpLF:
    begin
      ReplaceChar := LF+#0;
      ReplaceStrLen := 1;
      SetLength(Result, SourceLength);
    end;
  else
    ReplaceStrLen := 0;
    Assert(False, '');
  end;

  PResultStr := @Result[1];
  PSourceStr := @S[1];

  ReadIndex := 1;
  WriteIndex := 1;
  while (ReadIndex <= SourceLength-1) do
  begin
    case PSourceStr^[ReadIndex] of
      CR: begin
        if PSourceStr^[ReadIndex+1]=LF then
          Inc(ReadIndex);
        //改行コード挿入コードへ
      end;

      LF: begin
        if PSourceStr^[ReadIndex+1]=CR then
          Inc(ReadIndex);
        //改行コード挿入コードへ
      end;

    else
      PResultStr^[WriteIndex] := PSourceStr^[ReadIndex];
      Inc(WriteIndex);
      Inc(ReadIndex);
      Continue;
    end; //case

    //改行コード挿入
    PResultStr^[WriteIndex  ] := ReplaceChar[0];
    PResultStr^[WriteIndex+1] := ReplaceChar[1];
    inc(WriteIndex, ReplaceStrLen);

    Inc(ReadIndex);
  end; //while

  if ReadIndex = SourceLength then
  begin
    case PSourceStr^[SourceLength] of
      CR, LF: begin
        //改行コード挿入
        PResultStr^[WriteIndex  ] := ReplaceChar[0];
        PResultStr^[WriteIndex+1] := ReplaceChar[1];
        Inc(WriteIndex, ReplaceStrLen);
      end;
    else
      PResultStr^[WriteIndex] := PSourceStr^[SourceLength];
      Inc(WriteIndex);
    end; //case
  end;
  //SourceLength < ReadIndex の場合
  //=文字の最後が???CRLFか???LFCRの場合
  //何もしない

  SetLength(Result,WriteIndex-1);
end;
//------------------------------
────────────────────↑ものすごく速いコード

最初に出てくるSetLengthの時に

  SetLength(Result, SourceLength+CountCRLF(S));

このような事をしている所がありCountCRLFを行う事で
少し時間が余計にかかっています
これはメモリをなるべく少なく使うようにしているからなので単純に

  SetLength(Result, SourceLength*2);

などとすると更に少し高速化するかもしれませんが
メモリを多く食い過ぎるので使わない方がいいでしょう。


これをより高速化しようとすると
アセンブラでチューニングするという方法もあるらしいですが
それでもここから10倍も高速化するという事は不可能なようです。

文字列処理を行う時に高速化が必要なら参考にしてください。