Browse Source

fcl-db: sdfdataset: Added support for reading of CSV files, which have embedded CRLF between double-quotes (Added TSDFStringList) + test.

git-svn-id: trunk@31243 -
lacak 10 years ago
parent
commit
6f3da41769
2 changed files with 109 additions and 34 deletions
  1. 89 20
      packages/fcl-db/src/sdf/sdfdata.pp
  2. 20 14
      packages/fcl-db/tests/tcsdfdata.pp

+ 89 - 20
packages/fcl-db/src/sdf/sdfdata.pp

@@ -13,6 +13,9 @@ unit SdfData;
 ---------------
 ---------------
 Modifications
 Modifications
 ---------------
 ---------------
+30/Jul/15 LacaK:
+      Added TSDFStringList to support reading of CSV files, which have embedded
+      CRLF between double-quotes.
 7/Jun/12 BigChimp:
 7/Jun/12 BigChimp:
       Quote fields with delimiters or quotes to match Delphi SDF definition
       Quote fields with delimiters or quotes to match Delphi SDF definition
       (see e.g. help on TStrings.CommaText)
       (see e.g. help on TStrings.CommaText)
@@ -50,7 +53,7 @@ Modifications
            characters.
            characters.
            Altered buffer method to create on constructor and cleared when opened.
            Altered buffer method to create on constructor and cleared when opened.
       New Resource File. Nice Icons
       New Resource File. Nice Icons
-      SavetoStream method included
+      SaveToStream method included
       LoadFromStream method included
       LoadFromStream method included
                 ****** THANKS LESLIE *****
                 ****** THANKS LESLIE *****
 14/Ago/01  Version 2.00 (Orlando Arrocha)
 14/Ago/01  Version 2.00 (Orlando Arrocha)
@@ -141,10 +144,18 @@ type
     BookmarkFlag: TBookmarkFlag;
     BookmarkFlag: TBookmarkFlag;
   end;
   end;
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
-// TBaseTextDataSet
 
 
-  { TFixedFormatDataSet }
+  { TSDFStringList }
 
 
+  TSDFStringList = class(TStringList)
+    protected
+      FMultiLine: boolean;
+      procedure SetTextStr(const Value: string); override;
+  end;
+
+//-----------------------------------------------------------------------------
+// TFixedFormatDataSet
+//-----------------------------------------------------------------------------
   TFixedFormatDataSet = class(TDataSet)
   TFixedFormatDataSet = class(TDataSet)
   private
   private
     FSchema             :TStringList;
     FSchema             :TStringList;
@@ -152,7 +163,7 @@ type
     FFilterBuffer       :TRecordBuffer;
     FFilterBuffer       :TRecordBuffer;
     FFileMustExist      :Boolean;
     FFileMustExist      :Boolean;
     FReadOnly           :Boolean;
     FReadOnly           :Boolean;
-    FLoadfromStream     :Boolean;
+    FLoadFromStream     :Boolean;
     FTrimSpace          :Boolean;
     FTrimSpace          :Boolean;
     procedure SetSchema(const Value: TStringList);
     procedure SetSchema(const Value: TStringList);
     procedure SetFileName(Value : TFileName);
     procedure SetFileName(Value : TFileName);
@@ -164,7 +175,7 @@ type
     function GetActiveRecBuf(out RecBuf: TRecordBuffer): Boolean;
     function GetActiveRecBuf(out RecBuf: TRecordBuffer): Boolean;
     procedure SetFieldOfs(var Buffer : TRecordBuffer; FieldNo : Integer);
     procedure SetFieldOfs(var Buffer : TRecordBuffer; FieldNo : Integer);
   protected
   protected
-    FData               :TStringlist;
+    FData               :TSDFStringList;
     FDataOffset         :Integer;
     FDataOffset         :Integer;
     FCurRec             :Integer;
     FCurRec             :Integer;
     FRecordSize         :Integer;
     FRecordSize         :Integer;
@@ -217,7 +228,7 @@ type
     procedure SaveFileAs(strFileName : String); dynamic;
     procedure SaveFileAs(strFileName : String); dynamic;
     property  CanModify;
     property  CanModify;
     procedure LoadFromStream(Stream :TStream);
     procedure LoadFromStream(Stream :TStream);
-    procedure SavetoStream(Stream :TStream);
+    procedure SaveToStream(Stream :TStream);
   published
   published
     property FileMustExist: Boolean read FFileMustExist write SetFileMustExist;
     property FileMustExist: Boolean read FFileMustExist write SetFileMustExist;
     property ReadOnly: Boolean read FReadOnly write SetReadOnly;
     property ReadOnly: Boolean read FReadOnly write SetReadOnly;
@@ -256,6 +267,7 @@ type
 
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 // TSdfDataSet
 // TSdfDataSet
+//-----------------------------------------------------------------------------
   TSdfDataSet = class(TFixedFormatDataSet)
   TSdfDataSet = class(TFixedFormatDataSet)
   private
   private
     FDelimiter : Char;
     FDelimiter : Char;
@@ -285,6 +297,7 @@ type
 procedure Register;
 procedure Register;
 
 
 implementation
 implementation
+
 //{$R *.Res}
 //{$R *.Res}
 
 
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -294,11 +307,11 @@ constructor TFixedFormatDataSet.Create(AOwner : TComponent);
 begin
 begin
   FDefaultRecordLength := 250;
   FDefaultRecordLength := 250;
   FFileMustExist  := TRUE;
   FFileMustExist  := TRUE;
-  FLoadfromStream := False;
+  FLoadFromStream := False;
   FRecordSize   := 0;
   FRecordSize   := 0;
   FTrimSpace    := TRUE;
   FTrimSpace    := TRUE;
   FSchema       := TStringList.Create;
   FSchema       := TStringList.Create;
-  FData         := TStringList.Create;  // Load the textfile into a StringList
+  FData         := TSDFStringList.Create;  // Load the textfile into a StringList
   inherited Create(AOwner);
   inherited Create(AOwner);
 end;
 end;
 
 
@@ -344,8 +357,7 @@ var
   i, Len, MaxLen :Integer;
   i, Len, MaxLen :Integer;
   LstFields      :TStrings;
   LstFields      :TStrings;
 begin
 begin
-  if not Assigned(FData) then
-    exit;
+  if not Assigned(FData) then Exit;
 
 
   MaxLen := 0;
   MaxLen := 0;
   FieldDefs.Clear;
   FieldDefs.Clear;
@@ -382,15 +394,15 @@ procedure TFixedFormatDataSet.InternalOpen;
 var
 var
   Stream : TStream;
   Stream : TStream;
 begin
 begin
+  if not Assigned(FData) then Exit;
+
   FSaveChanges := FALSE;
   FSaveChanges := FALSE;
-  if not Assigned(FData) then
-    FData := TStringList.Create;
   if (not FileMustExist) and (not FileExists(FileName)) then
   if (not FileMustExist) and (not FileExists(FileName)) then
   begin
   begin
     Stream := TFileStream.Create(FileName, fmCreate);
     Stream := TFileStream.Create(FileName, fmCreate);
     Stream.Free;
     Stream.Free;
   end;
   end;
-  if not FLoadfromStream then
+  if not FLoadFromStream then
     FData.LoadFromFile(FileName);
     FData.LoadFromFile(FileName);
   FRecordSize := FDefaultRecordLength;
   FRecordSize := FDefaultRecordLength;
   InternalInitFieldDefs;
   InternalInitFieldDefs;
@@ -413,7 +425,7 @@ procedure TFixedFormatDataSet.InternalClose;
 begin
 begin
   if (not FReadOnly) and (FSaveChanges) then  // Write any edits to disk
   if (not FReadOnly) and (FSaveChanges) then  // Write any edits to disk
     FData.SaveToFile(FileName);
     FData.SaveToFile(FileName);
-  FLoadfromStream := False;
+  FLoadFromStream := False;
   FData.Clear;          // Clear data
   FData.Clear;          // Clear data
   BindFields(FALSE);
   BindFields(FALSE);
   if DefaultFields then // Destroy the TField
   if DefaultFields then // Destroy the TField
@@ -444,9 +456,9 @@ begin
   begin
   begin
     Active          := False; //Make sure the Dataset is Closed.
     Active          := False; //Make sure the Dataset is Closed.
     Stream.Position := 0;     //Make sure you are at the top of the Stream.
     Stream.Position := 0;     //Make sure you are at the top of the Stream.
-    FLoadfromStream := True;
+    FLoadFromStream := True;
     if not Assigned(FData) then
     if not Assigned(FData) then
-     raise Exception.Create('Data buffer unassigned');
+      raise Exception.Create('Data buffer unassigned');
     FData.LoadFromStream(Stream);
     FData.LoadFromStream(Stream);
     Active := True;
     Active := True;
   end
   end
@@ -455,7 +467,7 @@ begin
 end;
 end;
 
 
 // Saves Data as text to a stream.
 // Saves Data as text to a stream.
-procedure TFixedFormatDataSet.SavetoStream(Stream: TStream);
+procedure TFixedFormatDataSet.SaveToStream(Stream: TStream);
 begin
 begin
   if assigned(stream) then
   if assigned(stream) then
     FData.SaveToStream(Stream)
     FData.SaveToStream(Stream)
@@ -886,6 +898,62 @@ begin
   end;
   end;
 end;
 end;
 
 
+
+//-----------------------------------------------------------------------------
+// TSDFStringList
+//-----------------------------------------------------------------------------
+
+procedure TSDFStringList.SetTextStr(const Value: string);
+var
+  S: string;
+  P: integer;
+
+  function GetNextLine(const Value: string; out S: string; var P: Integer): Boolean;
+  const
+    CR: char = #13;
+    LF: char = #10;
+    DQ: char = '"';
+  var
+    L, P1: integer;
+    InDQ: boolean;
+  begin
+    // RFC 4180:
+    //  Each record is located on a separate line, delimited by a line break (CRLF)
+    //  Fields containing line breaks (CRLF), double quotes, and commas should be enclosed in double-quotes.
+    Result := False;
+    L := Length(Value);
+    if P > L then Exit;
+    P1 := P;
+    InDQ := False;
+    while (P <= L) and (not(Value[P] in [CR,LF]) or InDQ) do
+    begin
+      if Value[P] = DQ then InDQ := not InDQ;
+      inc(P);
+    end;
+    S := Copy(Value, P1, P-P1);
+    if (P <= L) and (Value[P] = CR) then
+      inc(P);
+    if (P <= L) and (Value[P] = LF) then
+      inc(P);
+    Result := True;
+  end;
+
+begin
+  if FMultiLine then // CRLF can be enclosed between double-quotes
+    try
+      BeginUpdate;
+      Clear;
+      P:=1;
+      while GetNextLine(Value,S,P) do
+        Add(S);
+    finally
+      EndUpdate;
+    end
+  else
+    inherited;
+end;
+
+
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
 // TSdfDataSet
 // TSdfDataSet
 //-----------------------------------------------------------------------------
 //-----------------------------------------------------------------------------
@@ -919,7 +987,7 @@ begin
   if (S[Pos] = DQ) then
   if (S[Pos] = DQ) then
     // quoted field
     // quoted field
     begin
     begin
-    // skip leading quote
+    // skip leading double-quote
     Inc(Pos);
     Inc(Pos);
     // allocate output buffer
     // allocate output buffer
     SetLength(Result, Len-P1+1);
     SetLength(Result, Len-P1+1);
@@ -931,7 +999,7 @@ begin
         begin
         begin
         if (pSrc[1] = DQ) then // doubled DQ
         if (pSrc[1] = DQ) then // doubled DQ
           begin
           begin
-          Inc(pSrc);
+          Inc(pSrc);           // dequote double-quote
           Inc(Pos);
           Inc(Pos);
           end
           end
         else if (pSrc[1] in [Delimiter,' ',CR,LF,#0]) then // DQ followed by delimiter or end of record
         else if (pSrc[1] in [Delimiter,' ',CR,LF,#0]) then // DQ followed by delimiter or end of record
@@ -950,7 +1018,7 @@ begin
       Inc(Pos);
       Inc(Pos);
     end
     end
   else
   else
-    // unquoted field name
+    // unquoted field
     begin
     begin
     while (Pos <= Len) and not(S[Pos] in [Delimiter,CR,LF,#0]) do
     while (Pos <= Len) and not(S[Pos] in [Delimiter,CR,LF,#0]) do
       Inc(Pos);
       Inc(Pos);
@@ -1118,6 +1186,7 @@ end;
 procedure TSdfDataSet.SetMultiLine(const Value: Boolean);
 procedure TSdfDataSet.SetMultiLine(const Value: Boolean);
 begin
 begin
   FMultiLine:=Value;
   FMultiLine:=Value;
+  FData.FMultiLine:=Value;
 end;
 end;
 
 
 
 

+ 20 - 14
packages/fcl-db/tests/tcsdfdata.pp

@@ -228,9 +228,7 @@ end;
 
 
 procedure Ttestsdfspecific.TestDelimitedTextOutput;
 procedure Ttestsdfspecific.TestDelimitedTextOutput;
 // Test if saving and loading data keeps the original values.
 // Test if saving and loading data keeps the original values.
-
-// Mainly check if writing & reading quotes works.
-// to do: more fully test RFC4180
+// Mainly check if writing & reading embedded quotes and CRLF works.
 const
 const
   Value1='Delimiter,"and";quote';
   Value1='Delimiter,"and";quote';
   Value2='J"T"';
   Value2='J"T"';
@@ -241,6 +239,7 @@ const
   Value7='Some "random" quotes';
   Value7='Some "random" quotes';
 Var
 Var
   F : Text;
   F : Text;
+  i : integer;
 begin
 begin
   // with Schema, with Header line
   // with Schema, with Header line
   TestDataset.Close;
   TestDataset.Close;
@@ -250,21 +249,28 @@ begin
   Assign(F, TestDataset.FileName);
   Assign(F, TestDataset.FileName);
   Rewrite(F);
   Rewrite(F);
   Writeln(F,'Field1,Field2,Field3,Field4,Field5,Field6,Field7');
   Writeln(F,'Field1,Field2,Field3,Field4,Field5,Field6,Field7');
-  Writeln(F,'"Delimiter,""and"";quote","J""T""",Just a long line,"Just a quoted long line","multi');
-  Writeln(F,'line","Delimiter,and;done","Some ""random"" quotes"');
+  for i:=1 to 3 do
+  begin
+    Writeln(F,'"Delimiter,""and"";quote","J""T""",Just a long line,"Just a quoted long line","multi');
+    Writeln(F,'line","Delimiter,and;done","Some ""random"" quotes"');
+  end;
   Close(F);
   Close(F);
   // Load our dataset
   // Load our dataset
   TestDataset.Open;
   TestDataset.Open;
-//  AssertEquals('Field count',7,TestDataset.FieldDefs.Count);
-//  AssertEquals('Record count',1,TestDataset.RecordCount);
+  AssertEquals('FieldDefs.Count', 7, TestDataset.FieldDefs.Count);
+  AssertEquals('RecordCount', 3, TestDataset.RecordCount);
   TestDataset.First;
   TestDataset.First;
-  AssertEquals('Field1', Value1, TestDataSet.Fields[0].AsString);
-  AssertEquals('Field2', Value2, TestDataSet.Fields[1].AsString);
-  AssertEquals('Field3', Value3, TestDataSet.Fields[2].AsString);
-  AssertEquals('Field4', Value4, TestDataSet.Fields[3].AsString);
-  AssertEquals('Field5', Value5, TestDataSet.Fields[4].AsString);
-  AssertEquals('Field6', Value6, TestDataSet.Fields[5].AsString);
-  AssertEquals('Field7' ,Value7, TestDataSet.Fields[6].AsString);
+  for i:=1 to 3 do
+  begin
+    AssertEquals('Field1', Value1, TestDataSet.Fields[0].AsString);
+    AssertEquals('Field2', Value2, TestDataSet.Fields[1].AsString);
+    AssertEquals('Field3', Value3, TestDataSet.Fields[2].AsString);
+    AssertEquals('Field4', Value4, TestDataSet.Fields[3].AsString);
+    AssertEquals('Field5', Value5, TestDataSet.Fields[4].AsString);
+    AssertEquals('Field6', Value6, TestDataSet.Fields[5].AsString);
+    AssertEquals('Field7' ,Value7, TestDataSet.Fields[6].AsString);
+    TestDataSet.Next;
+  end;
 end;
 end;
 
 
 procedure Ttestsdfspecific.TestEmptyFieldContents;
 procedure Ttestsdfspecific.TestEmptyFieldContents;