Delphi 10.3 Rio で CSV を処理する

# はじめに

これは [Delphi Advent Calendar 2018](https://qiita.com/advent-calendar/2018/delphi) の 12 日目の記事です。

先日、Delphi 10.3 Rio がリリースされ、型推論可能なインライン変数宣言ができるようになりました。

 - [インライン変数宣言 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/Rio/ja/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E5%A4%89%E6%95%B0%E5%AE%A3%E8%A8%80)
 - [型推論 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/Rio/ja/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E5%A4%89%E6%95%B0%E5%AE%A3%E8%A8%80#.E3.82.A4.E3.83.B3.E3.83.A9.E3.82.A4.E3.83.B3.E5.A4.89.E6.95.B0.E3.81.AE.E5.9E.8B.E6.8E.A8.E8.AB.96)

これを使って CSV を処理してみようと思います。


# コード

## CSV ファイルの読み込み

適当な CSV ファイルがなかったので、Delphi のサンプルデータベースをコンバートして CSV を作りました。
一行目はヘッダです。

```text:animals.csv
"NAME","SIZE","WEIGHT","AREA"
"Angel Fish",2,2,"Computer Aquariums"
"Boa",10,8,"South America"
"Critters",30,20,"Screen Savers"
"House Cat",10,5,"New Orleans"
"Ocelot",40,35,"Africa and Asia"
"Parrot",5,5,"South America"
"Tetras",2,2,"Fish Bowls"
```

Delphi のサンプルデータベースは 10.3 Rio だと
``C:\Users\Public\Documents\Embarcadero\Studio\20.0\Samples\Data``
に格納されています。

10.3 Rio っぽい (?) CSV ファイルの読み込みはこんなコードになりました。

```pascal:ReadCSV.dpr
program ReadCSV;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.Classes, System.SysUtils;

begin
  var Reader := TStreamReader.Create('animals.csv', TEncoding.Default, True); // 'animals.csv' を開く。BOM を調べてエンコーディングを自動選択する。BOM がなかったら TEncoding.Default (つまりは Shift_JIS) で開く。
  try
    Reader.ReadLine;                                                          // ヘッダ行を読み飛ばす
    while not Reader.EndOfStream do                                           // EOF になるまで読む
      begin
        for var Field in Reader.ReadLine.Split([','], '"') do                 // 読んだ行をカンマ区切りで分割したコレクションにする (クォーテーション考慮)
          Writeln(Field.DeQuotedString('"'));                                 // 読んだフィールドがダブルクォーテーションで括られていたらそれを外す
        Writeln;                                                              // 空の改行
      end;
  finally
    Reader.Free;
  end;
  Readln;                                                                     // 何かキーが押されるまで待つ
end.
```

TStreamReader のコンストラクタには以下のようなバリエーションがあります。

```pascal
constructor Create(Stream: TStream); overload;
constructor Create(Stream: TStream; DetectBOM: Boolean); overload;
constructor Create(Stream: TStream; Encoding: TEncoding;  DetectBOM: Boolean = False; BufferSize: Integer = 4096); overload;
constructor Create(const Filename: string); overload;
constructor Create(const Filename: string; DetectBOM: Boolean); overload;
constructor Create(const Filename: string; Encoding: TEncoding;  DetectBOM: Boolean = False; BufferSize: Integer = 4096); overload;

```

実行すると、

```text
Angel Fish
2
2
Computer Aquariums

Boa
10
8
South America

Critters
30
20
Screen Savers

House Cat
10
5
New Orleans

Ocelot
40
35
Africa and Asia

Parrot
5
5
South America

Tetras
2
2
Fish Bowls
```

こんな感じになります。テキスト分割処理に TStringList を使ったバージョンは以下のようになります。

```pascal
program ReadCSV;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.Classes, System.SysUtils;

begin
  var Reader := TStreamReader.Create('animals.csv', TEncoding.Default, True); // 'animals.csv' を開く。BOM を調べてエンコーディングを自動選択する。BOM がなかったら TEncoding.Default (つまりは Shift_JIS) で開く。
  var Fields := TStringList.Create;
  try
    Fields.StrictDelimiter := True;                                           // デリミタ (TStringList.Delimiter) でのみ文字列を分割する。クォート文字 (TStringList.QuoteChar) は考慮される。
    Reader.ReadLine;                                                          // ヘッダ行を読み飛ばす
    while not Reader.EndOfStream do                                           // EOF になるまで読む
      begin
        Fields.DelimitedText := Reader.ReadLine;                              // 読んだ行をカンマ区切りで分割したコレクションにする (クォーテーション考慮)
        for var Field in Fields do
          Writeln(Field);                                                         
        Writeln;                                                              // 空の改行
      end;
  finally
    Fields.Free;
    Reader.Free;
  end;
  Readln;                                                                     // 何かキーが押されるまで待つ
end.
```

テキスト分割処理は TStringList に任せた方が解りやすいコードになるかと思います。テキスト分割にかかわる TStringList のプロパティは以下の通りです。

|プロパティ|初期値|説明|
|:--|:--|:---|
|Delimiter|, (カンマ)|区切り文字|
|QuoteChar|" (ダブルクォーテーション)|クォート文字|
|StrictDelimiter|False|区切り文字でのみ分割。<br>False だと空白文字でも分割する。|

なお、10.1 Berlin より、一部のプロパティは Options プロパティで指定するようになっており、StrictDelimiter もその一つとなっています。つまり、

```pascal
  Fields.StrictDelimiter := True;
```

は

```pascal
  Fields.Options := Fields.Options + [soStrictDelimiter];
```

このようにも書けるようになっています。

 - [System.Classes.TStrings.Options (DocWiki: en)](http://docwiki.embarcadero.com/Libraries/en/System.Classes.TStrings.Options)

### TStreamReader について

TStreamReader は .NET の StreamReader クラスを~~パク~~...いえ、インスパイアしたクラスです。標準関数の AssignFile() / Reset() / CloseFile() の代わりに使えます。

そういえば 10.3 では Rewind() なんてメソッドが追加されています。10.2 Tokyo 以前では ファイルの先頭に移動したい場合には、

```pascal
  Reader.DiscardBufferedData;
  Reader.BaseStream.Seek(0, TSeekOrigin.soBeginning);
```

なんて事をやる必要がありました (多分) が、10.3 Rio だと、

```pascal
  Reader.Rewind;
```

って書くだけでよくなりました。

 - [System.Classes.TStreamReader (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TStreamReader)
 - [StreamReader Class (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamreader?view=netframework-4.7.2)

## CSV ファイルの書き出し

正直、書き出す方はあまり特殊な記述にはなりません。

```pascal:WriteCSV.dpr
program WriteCSV;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.Classes, System.SysUtils, System.Types;

begin
  var Writer := TStreamWriter.Create('animals2.csv', False, TEncoding.UTF8);
  try
    var StrArr: TStringDynArray;
    SetLength(StrArr, 4);

    // ヘッダ
    StrArr[0] := 'NAME'.QuotedString('"');
    StrArr[1] := 'SIZE'.QuotedString('"');
    StrArr[2] := 'WEIGHT'.QuotedString('"');
    StrArr[3] := 'AREA'.QuotedString('"');
    Writer.WriteLine(String.Join(',', StrArr));

    // データ
    StrArr[0] := 'Angel Fish'.QuotedString('"');
    StrArr[1] := '2';
    StrArr[2] := '2';
    StrArr[3] := 'Computer Aquarium'.QuotedString('"');
    Writer.WriteLine(String.Join(',', StrArr));

  finally
    Writer.Free;
  end;
end.
```

TStreamWriter のコンストラクタには以下のようなバリエーションがあります。

```pascal
constructor Create(Stream: TStream); overload;
constructor Create(Stream: TStream; Encoding: TEncoding; BufferSize: Integer = 4096); overload;
constructor Create(const Filename: string; Append: Boolean = False); overload;
constructor Create(const Filename: string; Append: Boolean; Encoding: TEncoding; BufferSize: Integer = 4096); overload;
```

ファイル名をパラメータとして取るコンストラクタの 2 番目の引数は Append で、ファイルを追記するかどうかのフラグです。False (上書き) がデフォルトです。3 番目の引数はエンコーディングで、指定しないと OS デフォルト (日本語なら Shift_JIS) の設定になります。

QuotedString は String 型へのヘルパーです (System.SysUtils.TStringHelper のオブジェクトメソッド)。

```pascal:
StrArr[0] := '"' + 'Angel Fish' + '"';
```

ではいかんのか？と思われるかもしれませんが、そのロジックだと**文字列にダブルクォーテーションが含まれる場合**に困ります。

 - [Comma-Separated Values (Wikipedia)](https://ja.wikipedia.org/wiki/Comma-Separated_Values)

Join は String 型へのヘルパーです (System.SysUtils.TStringHelper のクラスメソッド)。クラスメソッドなので、**"型名.クラスメソッド"** の形式で呼び出せます。

```pascal
  var LineStr: string := String.Join(',', StrArr);        // OK
  var LineStr: string := TStringHelper.Join(',', StrArr); // NG
```

TStringHelper は String 型のヘルパーなので、**"String.クラスメソッド"** で呼び出します。"TStringHelper.クラスメソッド" では呼び出せません。

余談ですが、10.3 Rio の String 型のクラスメソッド (TStringHelper のクラスメソッド) には以下のようなものがあります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/771f475f-c4b1-c917-0628-03e9ee19d0e9.png)

 - [System.SysUtils.TStringHelper (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.TStringHelper)

### TStreamWriter について

TStreamWriter は .NET の StreamWriter クラスを~~パク~~...いえ、インスパイアしたクラスです。標準関数の AssignFile() / Rewrite(), Append() / CloseFile() の代わりに使えます。

 - [System.Classes.TStreamWriter (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TStreamWriter)
 - [StreamWriter Class (docs.microsoft.com)](https://docs.microsoft.com/ja-jp/dotnet/api/system.io.streamwriter?view=netframework-4.7.2)


# おわりに
特にオチはないです。

TStreamReader / TStreamWriter は ANSI 版 Delphi では使えないため、CSV 処理には TStringList を使う事が多かったですね...お手軽ですし。でも、古い Delphi 用に AssignFile() / Reset() / Rewrite(), Append() / CloseFile() をラッピングした TStreamReader / TStreamWriter 互換クラスを作っておくと何かと便利かもしれませんね...こんな感じで。

```pascal:uStreamReaderWriter.pas
unit uStreamReaderWriter;

interface

type
  TStreamReader = class
  private
    F: TextFile;
    function GetEndOfStream: Boolean;
  public
    constructor Create(const Filename: string);
    destructor Destroy; override;
    procedure Close;
    function Read: Integer;
    function ReadLine: string;
    property EndOfStream: Boolean read GetEndOfStream;
  end;

  TStreamWriter = class
  private
    F: TextFile;
  public
    constructor Create(const Filename: string; Append: Boolean = False);
    destructor Destroy; override;
    procedure Close;
    procedure Flush;
    procedure Write(Value: Char);
    procedure WriteLine(const Value: string);
  end;

implementation

{ TStreamReader }

procedure TStreamReader.Close;
begin
  System.CloseFile(F);
end;

constructor TStreamReader.Create(const Filename: string);
begin
  System.AssignFile(F, Filename);
  System.Reset(F);
end;

destructor TStreamReader.Destroy;
begin
  Self.Close;
end;

function TStreamReader.GetEndOfStream: Boolean;
begin
  result := System.Eof(F)
end;

function TStreamReader.Read: Integer;
var
  c: Char;
begin
  System.Read(F, C);
  result := Ord(C);
end;

function TStreamReader.ReadLine: string;
var
  Buf: String;
begin
  System.Readln(F, Buf);
  result := Buf;
end;

{ TStreamWriter }

procedure TStreamWriter.Close;
begin
  System.CloseFile(F);
end;

constructor TStreamWriter.Create(const Filename: string; Append: Boolean);
begin
  System.AssignFile(F, Filename);
  if Append then
    System.Append(F)
  else
    System.Rewrite(F);
end;

destructor TStreamWriter.Destroy;
begin
  Self.Close;
end;

procedure TStreamWriter.Flush;
begin
  System.Flush(F)
end;

procedure TStreamWriter.Write(Value: Char);
begin
  System.Write(F, Value);
end;

procedure TStreamWriter.WriteLine(const Value: string);
begin
  System.Writeln(F, Value);
end;

end.
```

XE3 よりも前のバージョンには String 型のヘルパーがないので Split() は使えませんが (XE 以降には [SplitString()](http://docwiki.embarcadero.com/Libraries/ja/System.StrUtils.SplitString) があるけれど)、それこそ TStringList を使って処理をすればいいかと思います。

```pascal
program ReadCSV;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes,
  uStreamReaderWriter;

var
  Reader: TStreamReader;
  Fields: TStringList;
  Field: String;
begin
  Reader := TStreamReader.Create('animals.csv');
  Fields := TStringList.Create;
  try
    Fields.StrictDelimiter := True;
    Reader.ReadLine;
    while not Reader.EndOfStream do
      begin
        Fields.DelimitedText := Reader.ReadLine;
        for Field in Fields do
          Writeln(Field);
        Writeln;
      end;
  finally
    Fields.Free;
    Reader.Free;
  end;
  Readln;
end.
```

なんだかんだで TStringList が便利すぎるのですよねぇ...。