Forum > LCL

unit Grids.pas SaveToCSVStream bug?

(1/2) > >>

KodeZwerg:
Hello there, I do actually not know if this is on purpose or a bug, here is what I found out.
I made a basic Grid and entered in the cells (beside fixed cells) test data.
When I did exported a file, it somehow had strange content.
The SaveToCSVStream mechanism add infront of each line a delimiter char when you turned WriteTitles off.
here is the original:

--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---procedure TCustomStringGrid.SaveToCSVStream(AStream: TStream; ADelimiter: Char;  WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);var  i,j,StartRow: Integer;  HeaderL, Lines: TStringList;  C: TGridColumn;begin  if (RowCount=0) or (ColCount=0) then    exit;  Lines := TStringList.Create;  try    if WriteTitles then begin      if Columns.Enabled then begin        if FixedRows>0 then begin          HeaderL := TStringList.Create;          try            // Collect header column names to a temporary StringList            for i := 0 to ColCount-1 do begin              c := ColumnFromGridColumn(i);              if (c <> nil) then begin                if c.Visible or not VisibleColumnsOnly then                  HeaderL.Add(c.Title.Caption);              end              else              if not VisibleColumnsOnly then                HeaderL.Add(Cells[i, 0]);            end;            HeaderL.Delimiter:=ADelimiter;            Headerl.StrictDelimiter := False; //force quoting of strings that contain whitespace or Delimiter            Lines.Add(HeaderL.DelimitedText); // Add as a first row in Lines          finally            HeaderL.Free;          end;        end;        StartRow := FixedRows;      end else      if FixedRows>0 then        StartRow := FixedRows-1      else        StartRow := 0;    end else      StartRow := FixedRows;    for i:=StartRow to RowCount-1 do begin      if Columns.Enabled and VisibleColumnsOnly then begin        HeaderL := TStringList.Create;        try        for j := 0 to ColCount-1 do begin          c := ColumnFromGridColumn(j);          if c=nil then Continue;          if c.Visible then            HeaderL.Add(Cells[j,i]);        end;        HeaderL.Delimiter:=ADelimiter;        HeaderL.StrictDelimiter := False; //force quoting of strings that contain whitespace or Delimiter        Lines.Add(HeaderL.DelimitedText); // Add the row in Lines        finally          HeaderL.Free;        end;      end      else      begin      Rows[i].StrictDelimiter := False; //force quoting of strings that contain whitespace or Delimiter      Rows[i].Delimiter:=ADelimiter;      Lines.Add(Rows[i].DelimitedText);    end;    end;    Lines.SaveToStream(AStream);  finally    Lines.Free;  end;end;
here is my patch:

--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---procedure TCustomStringGrid.SaveToCSVStream(AStream: TStream; ADelimiter: Char;  WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);var  i,j,StartRow: Integer;  HeaderL, Lines: TStringList;  C: TGridColumn;begin  if (RowCount=0) or (ColCount=0) then    exit;  Lines := TStringList.Create;  try    if WriteTitles then begin      if Columns.Enabled then begin        if FixedRows>0 then begin          HeaderL := TStringList.Create;          try            // Collect header column names to a temporary StringList            for i := 0 to ColCount-1 do begin              c := ColumnFromGridColumn(i);              if (c <> nil) then begin                if c.Visible or not VisibleColumnsOnly then                  HeaderL.Add(c.Title.Caption);              end              else              if not VisibleColumnsOnly then                HeaderL.Add(Cells[i, 0]);            end;            HeaderL.Delimiter:=ADelimiter;            Headerl.StrictDelimiter := False; //force quoting of strings that contain whitespace or Delimiter            Lines.Add(HeaderL.DelimitedText); // Add as a first row in Lines          finally            HeaderL.Free;          end;        end;        StartRow := FixedRows;      end else      if FixedRows>0 then        StartRow := FixedRows-1      else        StartRow := 0;    end else      StartRow := FixedRows;    for i:=StartRow to RowCount-1 do begin      if Columns.Enabled and VisibleColumnsOnly then begin        HeaderL := TStringList.Create;        try        for j := 0 to ColCount-1 do begin          c := ColumnFromGridColumn(j);          if c=nil then Continue;          if c.Visible then            HeaderL.Add(Cells[j,i]);        end;        HeaderL.Delimiter:=ADelimiter;        HeaderL.StrictDelimiter := False; //force quoting of strings that contain whitespace or Delimiter        Lines.Add(HeaderL.DelimitedText); // Add the row in Lines        finally          HeaderL.Free;        end;      end      else      begin        HeaderL := TStringList.Create;        try        for j := FixedCols to ColCount-1 do begin            HeaderL.Add(Cells[j,i]);            HeaderL.Delimiter:=ADelimiter;            HeaderL.StrictDelimiter := False; //force quoting of strings that contain whitespace or Delimiter        end;        Lines.Add(HeaderL.DelimitedText); // Add the row in Lines        finally          HeaderL.Free;        end;    end;    end;    Lines.SaveToStream(AStream);  finally    Lines.Free;  end;end;
If you think its useful feel free to update Grids.pas with my patch.
If you think it is wrong, please enlight me the truth to let me see my mistake.

In attachment are two images, the first show the unpatched Grids.pas
The second demonstrate the patched varation

My Version: Lazarus 2.2.4 (rev lazarus_2_2_4) FPC 3.2.2 x86_64-win64-win32/win64

rvk:

--- Quote from: KodeZwerg on December 19, 2022, 06:26:07 am ---The SaveToCSVStream mechanism add infront of each line a delimiter char when you turned WriteTitles off.

--- End quote ---
It's not that strange. The first field is the text in the first column (your fixed column).
Setting WriteTitles to false only eliminates the FixedRow rows. Not the FixedCol columns.
You could create a WriteFixed to also eliminate the fixed column (or the WriteTitles should be renamed to WriteHeaders).

Anyway... looking at your code I see a bit of inefficiency (ever so slightly). You do the Delimiter:=ADelimiter and StrictDelimiter := False for EVERY column.
This would be better (this was probably because the original code isn't really properly indented):

--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---HeaderL := TStringList.Create;try  for j := FixedCols to ColCount-1 do HeaderL.Add(Cells[j,i]);  HeaderL.Delimiter:=ADelimiter;  HeaderL.StrictDelimiter := False; //force quoting of strings that contain whitespace or Delimiter  Lines.Add(HeaderL.DelimitedText); // Add the row in Linesfinally  HeaderL.Free;end;
Also... what if I really WANT the FixedCol exported in my CSV? It could contain really important information.
In that case you should look at WriteTitles and only start from FixedCols is that is false.

Something like (not tested):

--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---if WriteTitles then StartCol := 0 else StartCol := FixedCols;for j := StartCol to ColCount-1 do HeaderL.Add(Cells[j,i]);

wp:
The author of the SaveToCSVFile (not me) decided to mean with "Titles" only the headers of the columns because a stringgrid with GridColumns has "Titles" in the top-most row. If a grid contains no gridcolumns the "Titles" are the cells in the fixed rows. The cells in the fixed columns are not understood to be "Titles" in this concept. This is by design, not a bug.

The fixed column containing the row headers thus is not affected by the "WriteTitles" parameter. So what appears to be a leading semicolon to you is just the consequence of the the first column being empty.

Since your fixed column is empty, simply turn it off (Grid.FixedCols := 0), and everything will be fine.

Of course, if you have content in the fixed columns but you do not want to save it you have a problem. In this case I'd recommend to use an own procedure like yours.

It would be possible to extend the procedure by an additional parameter, "WriteFixedCols", to clarify things. This would not be very nice because the original parameters (WriteTitles, VisibleColumnsOnly) must be kept in place for compatibility reasons and therefore WriteTitles and WriteFixedCols would not be listed together which I personally would expect. One could overload the entire procedure and introduce some "CSVOptions = set of (coWriteTitles, coWriteFixedCols, csVisibleColumnsOnly)", but sincerely, I don't like this either because I think that saving to CSV should not be a method of the grids at all. What if somebody wants to save a grid to hmtl, xml, json, xls? For this the user would have to write his own procedures anyway. So why not drop the SaveToCSVFile at all and introduce a more general TGridExporter?

KodeZwerg:
Thanks @rvk for hint with removing the wrong begin/end, yeah I just copied and pasted without seeing it  :'(
My own method does do same and on wish also like it was before i accidently felt over it  ;)

--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---procedure SaveGridToFile(const AStringGrid: TStringGrid; const AFileName: string; const ADelimiter: Char = ','; const AIncludeFixed: Boolean = False; const AIncludeTitle: Boolean = False);var  LRow, LCol,  LRowStart, LColStart: Integer;  sl: TStringList;  s: string;begin  sl := TStringList.Create;  if AIncludeFixed then    begin      LRowStart := 0;      LColStart := 0;    end    else    begin      if AIncludeTitle then        LRowStart := 0        else        LRowStart := AStringGrid.FixedRows;      LColStart := AStringGrid.FixedCols;    end;  try    for LRow := LRowStart to Pred(AStringGrid.RowCount) do      begin        s := '';        for LCol := LColStart to Pred(AStringGrid.ColCount) do          if (LCol < Pred(AStringGrid.ColCount)) then            s := s + AStringGrid.Cells[LCol, LRow] + ADelimiter            else            s := s + AStringGrid.Cells[LCol, LRow];        sl.Add(s);      end;    sl.SaveToFile(AFilename, True);  finally    sl.Free;  end;end;

rvk:

--- Quote from: KodeZwerg on December 19, 2022, 11:45:04 am ---
--- Code: Pascal  [+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i < x.length; i++) { x[i].style.maxHeight='none'; x[i].style.height = Math.min(x[i].clientHeight+15,306)+'px'; x[i].style.resize = "vertical";}};} ---s := s + AStringGrid.Cells[LCol, LRow] + ADelimiter
--- End quote ---
You definitely want to fix these lines  ::)
Using a TStringList.DelimitedText, there is some logic which adds protection for a delimiter character in a string.
In your code you don't.

So if one of you cells contain a ADelimiter character, the whole CSV-line would be corrupt.

Navigation

[0] Message Index

[#] Next page

Go to full version