View Issue Details

IDProjectCategoryView StatusLast Update
0028582LazarusLCLpublic2015-09-08 22:09
ReporterwpAssigned ToJesus Reyes 
PrioritynormalSeverityminorReproducibilityalways
Status closedResolutionfixed 
Product Version1.5 (SVN)Product Build 
Target Version1.6Fixed in Version1.5 (SVN) 
Summary0028582: CSV import into StringGrid omits first line
DescriptionAs discussed in the forum (http://forum.lazarus.freepascal.org/index.php/topic,28932.msg181815.html#msg181815) the CSV import into a StringGrid fails to import the first data line if the parameter WithHeaders is false. See attached screenshot of the import of this data file:

1;Smith;Jim;Utah;111-111111
2;Adams;Joshua;New York;222-222222
3;Woodbridge;James;Alabama;333-333333
4;Woodgate;John;North Carolina;444-444444
5;Hamilton;Steven;Louisiana;555-555555

It is seen that the first line is missing in each of the two grid of this demo (the first grid uses predefined columns, while the second one does not).

The provided patch fixes this issue. In addition it takes care of cases when the data file contains text to be ignored above the import lines.
Steps To ReproduceThe attached demo project shows the issue with the current version of Lazaraus, and it can be used to verify the improvements due to this patch.

Several data files are provided for the test; all of them contain 5 data lines like in the example above.

"NoHeader_0.txt" - no header line, data start immediately at the top
"NoHeader_1.txt" - no header line, 1 additional line to be ignored above the first data line
"NoHeader_2.txt" - no header line, 2 additional lines to be ignored above the first data line
"Header_0.txt" - a header line immediately at the top, data lines follow
"Header_1.txt" - one line to be ignored, then the header line, then the data lines
"Header_2.txt" - two lines to be skipped, then the header line, then the data lines

The demo program contains a combobox for selecting one of these input files. The parameter "WithHeaders" of the grid's method "LoadFromCVSFile" is set according to the state of the checkbox "File with headers". In addition, the combobox "Skip..." allows to skip no, one or two data lines above the imported lines (used by the patched version only!)

To reproduce the described behavior and the screenshot, load the file "NoHeader_0.txt" and uncheck the "WithHeaders" box.
In addition, load "No_Header_1.txt" or "NoHeader_2.txt" --> the dummy lines are imported as well which is not expected.
Load "Header_1.txt" with "File with Header" checked --> the predefined headers of the upper grid are partly overwritten by the dummy text, the headers of the lower grid (no predefined headers) are not detected correctly as well.

Apply the patch and repeat the tests. The patch introduces an additonal (optional) parameter "SkipLines" to the "LoadFromCSVFile" method which can be selected in the gui by means of the "Skip..." combobox.

Whenever the parameters selected by "File with header" and "Skip..." match the structure of the file the data are corrected imported.
TagsNo tags attached.
Fixed in Revision49735, 49754
LazTarget1.6
WidgetsetWin32/Win64
Attached Files
  • grids.pas.patch (3,859 bytes)
    Index: lcl/grids.pas
    ===================================================================
    --- lcl/grids.pas	(revision 49699)
    +++ lcl/grids.pas	(working copy)
    @@ -1625,8 +1625,8 @@
           procedure Clean(StartCol,StartRow,EndCol,EndRow: integer; CleanOptions: TGridZoneSet); overload;
           procedure CopyToClipboard(AUseSelection: boolean = false);
           procedure InsertRowWithValues(Index: Integer; Values: array of String);
    -      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true);
    -      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true);
    +      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    +      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
           procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true;
                                     VisibleColumnsOnly: boolean=false);
           procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=','; WithHeader: boolean=true;
    @@ -10853,10 +10853,11 @@
     end;
     
     procedure TCustomStringGrid.LoadFromCSVStream(AStream: TStream;
    -  ADelimiter: Char=','; WithHeader: boolean=true);
    +  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
     var
       MaxCols: Integer = 0;
       MaxRows: Integer = 0;
    +  LineCounter: Integer = -1;
     
       function RowOffset: Integer;
       begin
    @@ -10864,7 +10865,7 @@
         if withHeader then
           result := Max(0, FixedRows-1)  + Max(MaxRows-1, 0)
         else
    -      result := FixedRows + Max(MaxRows-2, 0);
    +      result := FixedRows + Max(MaxRows-1, 0);
       end;
     
       procedure NewRecord(Fields:TStringlist);
    @@ -10871,21 +10872,19 @@
       var
         i, aRow: Integer;
       begin
    +    inc(LineCounter);
    +    if (LineCounter < SkipLines) then
    +      exit;
    +
         if Fields.Count=0 then
           exit;
     
    -    Inc(MaxRows);
    -    if (MaxRows=1) then
    -      // first record
    -      if not WithHeader then
    -        exit; // ... no header wanted
    -
         // make sure we have enough columns
         if MaxCols<Fields.Count then
           MaxCols := Fields.Count;
         if Columns.Enabled then begin
           while Columns.VisibleCount<MaxCols do
    -          Columns.Add;
    +        Columns.Add;
         end
         else begin
           if ColCount<MaxCols then
    @@ -10892,17 +10891,25 @@
             ColCount := MaxCols;
         end;
     
    -    // and rows ...
    +    // setup columns captions of custom columns if they are enabled
    +    if (MaxRows = 0) then
    +      if WithHeader then
    +      begin
    +        if Columns.Enabled then
    +          for i:=0 to Fields.Count-1 do Columns[i].Title.Caption:=Fields[i]
    +        else
    +          for i:=0 to Fields.Count-1 do Cells[i, 0] := Fields[i];
    +        inc(MaxRows);
    +        exit;
    +      end;
    +
    +    // Make sure we have enough rows
    +    Inc(MaxRows);
         aRow := RowOffset;
         if aRow>RowCount-1 then
           RowCount := aRow + 20;
     
    -    // setup columns captions of custom columns if they are enabled
    -    if (MaxRows=1) and withHeader and Columns.Enabled then begin
    -      for i:=0 to Fields.Count-1 do
    -        Columns[i].Title.Caption:=Fields[i];
    -    end;
    -
    +    // Copy line data to cells
         for i:=0 to Fields.Count-1 do
           Cells[i, aRow] := Fields[i];
       end;
    @@ -10927,13 +10934,13 @@
     end;
     
     procedure TCustomStringGrid.LoadFromCSVFile(AFilename: string;
    -  ADelimiter: Char=','; WithHeader: boolean=true);
    +  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
     var
       TheStream: TFileStreamUtf8;
     begin
       TheStream:=TFileStreamUtf8.Create(AFileName,fmOpenRead or fmShareDenyWrite);
       try
    -    LoadFromCSVStream(TheStream, ADelimiter, WithHeader);
    +    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines);
       finally
         TheStream.Free;
       end;
    
    grids.pas.patch (3,859 bytes)
  • StringGrid_CSVImport.zip (4,645 bytes)
  • NoHeader.png (59,167 bytes)
    NoHeader.png (59,167 bytes)
  • lcsvutils.pas.patch (525 bytes)
    Index: components/lazutils/lcsvutils.pas
    ===================================================================
    --- components/lazutils/lcsvutils.pas	(revision 49723)
    +++ components/lazutils/lcsvutils.pas	(working copy)
    @@ -60,7 +60,8 @@
       begin
         Len := Length(curWord);
         SetLength(curWord, Len+(leadPtr-wordPtr));
    -    Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr));
    +    if Length(curWord) > 0 then
    +      Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr));
         Inc(leadPtr);
         wordPtr := leadPtr;
       end;
    
    lcsvutils.pas.patch (525 bytes)
  • StringGrid_CSVImport_Crash.zip (1,398 bytes)
  • StringGrid_CSV_SkipEmptyLines.zip (4,725 bytes)
  • grids-skip-empty-lines.patch (2,809 bytes)
    Index: lcl/grids.pas
    ===================================================================
    --- lcl/grids.pas	(revision 49735)
    +++ lcl/grids.pas	(working copy)
    @@ -1625,8 +1625,10 @@
           procedure Clean(StartCol,StartRow,EndCol,EndRow: integer; CleanOptions: TGridZoneSet); overload;
           procedure CopyToClipboard(AUseSelection: boolean = false);
           procedure InsertRowWithValues(Index: Integer; Values: array of String);
    -      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    -      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    +      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=',';
    +        WithHeader: boolean=true; SkipLines: Integer=0; SkipEmptyLines: Boolean=true);
    +      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=',';
    +        WithHeader: boolean=true; SkipLines: Integer=0; SkipEmptyLines: Boolean=true);
           procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true;
                                     VisibleColumnsOnly: boolean=false);
           procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=','; WithHeader: boolean=true;
    @@ -10853,7 +10855,8 @@
     end;
     
     procedure TCustomStringGrid.LoadFromCSVStream(AStream: TStream;
    -  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    +  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0;
    +  SkipEmptyLines: Boolean=true);
     var
       MaxCols: Integer = 0;
       MaxRows: Integer = 0;
    @@ -10871,6 +10874,7 @@
       procedure NewRecord(Fields:TStringlist);
       var
         i, aRow: Integer;
    +    empty: Boolean;
       begin
         inc(LineCounter);
         if (LineCounter < SkipLines) then
    @@ -10879,6 +10883,16 @@
         if Fields.Count=0 then
           exit;
     
    +    if SkipEmptyLines then begin
    +      empty := true;
    +      for i:=0 to Fields.Count-1 do
    +        if Fields[i] <> '' then begin
    +          empty := false;
    +          break;
    +        end;
    +      if empty then exit;
    +    end;
    +
         // make sure we have enough columns
         if MaxCols<Fields.Count then
           MaxCols := Fields.Count;
    @@ -10934,13 +10948,14 @@
     end;
     
     procedure TCustomStringGrid.LoadFromCSVFile(AFilename: string;
    -  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    +  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0;
    +  SkipEmptyLines: Boolean=true);
     var
       TheStream: TFileStreamUtf8;
     begin
       TheStream:=TFileStreamUtf8.Create(AFileName,fmOpenRead or fmShareDenyWrite);
       try
    -    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines);
    +    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines, SkipEmptyLines);
       finally
         TheStream.Free;
       end;
    
  • grids-v3.pas.patch (5,755 bytes)
    Index: components/lazutils/lcsvutils.pas
    ===================================================================
    --- components/lazutils/lcsvutils.pas	(revision 49739)
    +++ components/lazutils/lcsvutils.pas	(working copy)
    @@ -57,8 +57,10 @@
       procedure StorePart;
       var
         Len: Integer;
    +    x: Integer;
       begin
         Len := Length(curWord);
    +    x := leadptr - wordptr;
         SetLength(curWord, Len+(leadPtr-wordPtr));
         if Length(curWord) > 0 then
           Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr));
    Index: lcl/grids.pas
    ===================================================================
    --- lcl/grids.pas	(revision 49739)
    +++ lcl/grids.pas	(working copy)
    @@ -1625,12 +1625,14 @@
           procedure Clean(StartCol,StartRow,EndCol,EndRow: integer; CleanOptions: TGridZoneSet); overload;
           procedure CopyToClipboard(AUseSelection: boolean = false);
           procedure InsertRowWithValues(Index: Integer; Values: array of String);
    -      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    -      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    -      procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true;
    -                                VisibleColumnsOnly: boolean=false);
    -      procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=','; WithHeader: boolean=true;
    -                                VisibleColumnsOnly: boolean=false);
    +      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=',';
    +        UseTitles: boolean=true; FromLine: Integer=0; SkipEmptyLines: Boolean=true);
    +      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=',';
    +        UseTitles: boolean=true; FromLine: Integer=0; SkipEmptyLines: Boolean=true);
    +      procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=',';
    +        WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
    +      procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=',';
    +        WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
     
           property Cells[ACol, ARow: Integer]: string read GetCells write SetCells;
           property Cols[index: Integer]: TStrings read GetCols write SetCols;
    @@ -10853,7 +10855,8 @@
     end;
     
     procedure TCustomStringGrid.LoadFromCSVStream(AStream: TStream;
    -  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    +  ADelimiter: Char=','; UseTitles: boolean=true; FromLine: Integer=0;
    +  SkipEmptyLines: Boolean=true);
     var
       MaxCols: Integer = 0;
       MaxRows: Integer = 0;
    @@ -10862,7 +10865,7 @@
       function RowOffset: Integer;
       begin
         // return row offset of current CSV record (MaxRows) which is 1 based
    -    if withHeader then
    +    if UseTitles then
           result := Max(0, FixedRows-1)  + Max(MaxRows-1, 0)
         else
           result := FixedRows + Max(MaxRows-1, 0);
    @@ -10871,14 +10874,27 @@
       procedure NewRecord(Fields:TStringlist);
       var
         i, aRow: Integer;
    +    empty: Boolean;
       begin
         inc(LineCounter);
    -    if (LineCounter < SkipLines) then
    +    if (LineCounter < FromLine) then
           exit;
     
         if Fields.Count=0 then
           exit;
     
    +    if SkipEmptyLines then begin
    +      if (MaxCols > 1) then begin   // Ignore "SkipLines" for file with single column
    +        empty := true;
    +        for i:=0 to Fields.Count-1 do
    +          if Fields[i] <> '' then begin
    +            empty := false;
    +            break;
    +          end;
    +        if empty then exit;
    +      end;
    +    end;
    +
         // make sure we have enough columns
         if MaxCols<Fields.Count then
           MaxCols := Fields.Count;
    @@ -10891,9 +10907,9 @@
             ColCount := MaxCols;
         end;
     
    -    // setup columns captions of custom columns if they are enabled
    +    // setup columns captions if enabled by UseTitles
         if (MaxRows = 0) then
    -      if WithHeader then
    +      if UseTitles then
           begin
             if Columns.Enabled then
               for i:=0 to Fields.Count-1 do Columns[i].Title.Caption:=Fields[i]
    @@ -10934,13 +10950,14 @@
     end;
     
     procedure TCustomStringGrid.LoadFromCSVFile(AFilename: string;
    -  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
    +  ADelimiter: Char=','; UseTitles: boolean=true; FromLine: Integer=0;
    +  SkipEmptyLines: Boolean=true);
     var
       TheStream: TFileStreamUtf8;
     begin
       TheStream:=TFileStreamUtf8.Create(AFileName,fmOpenRead or fmShareDenyWrite);
       try
    -    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines);
    +    LoadFromCSVStream(TheStream, ADelimiter, UseTitles, FromLine, SkipEmptyLines);
       finally
         TheStream.Free;
       end;
    @@ -10947,7 +10964,7 @@
     end;
     
     procedure TCustomStringGrid.SaveToCSVStream(AStream: TStream; ADelimiter: Char;
    -  WithHeader: boolean=true; VisibleColumnsOnly: boolean=false);
    +  WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
     var
       i,j,StartRow: Integer;
       HeaderL, Lines: TStringList;
    @@ -10957,7 +10974,7 @@
         exit;
       Lines := TStringList.Create;
       try
    -    if WithHeader then begin
    +    if WriteTitles then begin
           if Columns.Enabled then begin
             if FixedRows>0 then begin
               HeaderL := TStringList.Create;
    @@ -11019,13 +11036,13 @@
     end;
     
     procedure TCustomStringGrid.SaveToCSVFile(AFileName: string; ADelimiter: Char;
    -  WithHeader: boolean=true; VisibleColumnsOnly: boolean=false);
    +  WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
     var
       TheStream: TFileStreamUtf8;
     begin
       TheStream:=TFileStreamUtf8.Create(AFileName,fmCreate);
       try
    -    SaveToCSVStream(TheStream, ADelimiter, WithHeader);
    +    SaveToCSVStream(TheStream, ADelimiter, WriteTitles, VisibleColumnsOnly);
       finally
         TheStream.Free;
       end;
    
    grids-v3.pas.patch (5,755 bytes)

Activities

wp

2015-08-28 22:44

developer  

grids.pas.patch (3,859 bytes)
Index: lcl/grids.pas
===================================================================
--- lcl/grids.pas	(revision 49699)
+++ lcl/grids.pas	(working copy)
@@ -1625,8 +1625,8 @@
       procedure Clean(StartCol,StartRow,EndCol,EndRow: integer; CleanOptions: TGridZoneSet); overload;
       procedure CopyToClipboard(AUseSelection: boolean = false);
       procedure InsertRowWithValues(Index: Integer; Values: array of String);
-      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true);
-      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true);
+      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
+      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
       procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true;
                                 VisibleColumnsOnly: boolean=false);
       procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=','; WithHeader: boolean=true;
@@ -10853,10 +10853,11 @@
 end;
 
 procedure TCustomStringGrid.LoadFromCSVStream(AStream: TStream;
-  ADelimiter: Char=','; WithHeader: boolean=true);
+  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
 var
   MaxCols: Integer = 0;
   MaxRows: Integer = 0;
+  LineCounter: Integer = -1;
 
   function RowOffset: Integer;
   begin
@@ -10864,7 +10865,7 @@
     if withHeader then
       result := Max(0, FixedRows-1)  + Max(MaxRows-1, 0)
     else
-      result := FixedRows + Max(MaxRows-2, 0);
+      result := FixedRows + Max(MaxRows-1, 0);
   end;
 
   procedure NewRecord(Fields:TStringlist);
@@ -10871,21 +10872,19 @@
   var
     i, aRow: Integer;
   begin
+    inc(LineCounter);
+    if (LineCounter < SkipLines) then
+      exit;
+
     if Fields.Count=0 then
       exit;
 
-    Inc(MaxRows);
-    if (MaxRows=1) then
-      // first record
-      if not WithHeader then
-        exit; // ... no header wanted
-
     // make sure we have enough columns
     if MaxCols<Fields.Count then
       MaxCols := Fields.Count;
     if Columns.Enabled then begin
       while Columns.VisibleCount<MaxCols do
-          Columns.Add;
+        Columns.Add;
     end
     else begin
       if ColCount<MaxCols then
@@ -10892,17 +10891,25 @@
         ColCount := MaxCols;
     end;
 
-    // and rows ...
+    // setup columns captions of custom columns if they are enabled
+    if (MaxRows = 0) then
+      if WithHeader then
+      begin
+        if Columns.Enabled then
+          for i:=0 to Fields.Count-1 do Columns[i].Title.Caption:=Fields[i]
+        else
+          for i:=0 to Fields.Count-1 do Cells[i, 0] := Fields[i];
+        inc(MaxRows);
+        exit;
+      end;
+
+    // Make sure we have enough rows
+    Inc(MaxRows);
     aRow := RowOffset;
     if aRow>RowCount-1 then
       RowCount := aRow + 20;
 
-    // setup columns captions of custom columns if they are enabled
-    if (MaxRows=1) and withHeader and Columns.Enabled then begin
-      for i:=0 to Fields.Count-1 do
-        Columns[i].Title.Caption:=Fields[i];
-    end;
-
+    // Copy line data to cells
     for i:=0 to Fields.Count-1 do
       Cells[i, aRow] := Fields[i];
   end;
@@ -10927,13 +10934,13 @@
 end;
 
 procedure TCustomStringGrid.LoadFromCSVFile(AFilename: string;
-  ADelimiter: Char=','; WithHeader: boolean=true);
+  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
 var
   TheStream: TFileStreamUtf8;
 begin
   TheStream:=TFileStreamUtf8.Create(AFileName,fmOpenRead or fmShareDenyWrite);
   try
-    LoadFromCSVStream(TheStream, ADelimiter, WithHeader);
+    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines);
   finally
     TheStream.Free;
   end;
grids.pas.patch (3,859 bytes)

wp

2015-08-28 22:45

developer  

StringGrid_CSVImport.zip (4,645 bytes)

wp

2015-08-28 22:46

developer  

NoHeader.png (59,167 bytes)
NoHeader.png (59,167 bytes)

Bart Broersma

2015-08-29 00:30

developer   ~0085571

Observed current behviour:
- if WithHeaders is False and FixedRows <> 0 then the lines will be loaded starting at the first non-fixed row.
- if WithHeaders is True and FixedRows <> 0 then header will be displayed at last fixed row and rest of data follows.
- if FixedRows = 0 then data is loaded with/without headers as expected.

Regardless of the value of WithHeaders all data is loaded and displayed as expected, including the first _data_ line.

Tested with Lazarus 1.5 r49723 FPC 2.6.4 i386-win32-win32/win64.

Bart Broersma

2015-08-29 00:38

developer   ~0085572

I don't think we should try to accomodate skipping empty lines, AFAIK headers need to be on the first line in proper csv files.

wp

2015-08-29 00:49

developer   ~0085573

>> all data is loaded and displayed as expected

No - the case of the screenshot misses the first data line: "NoHeader_0.txt", "File with headers" unchecked (I think that's the combination the program starts with). The first data line visible must have a "1" in the "No" column - in the mentioned case there is a "2", however.

I do agree that data are loaded into fixed or non-fixed rows as expected.

wp

2015-08-29 01:21

developer   ~0085574

>> I don't think we should try to accomodate skipping empty lines, AFAIK headers need to be on the first line in proper csv files.

I've seen lots of such files, mostly from measurement equipment writing a header with the measurement parameters in front of the data.

wp

2015-08-29 01:27

developer  

lcsvutils.pas.patch (525 bytes)
Index: components/lazutils/lcsvutils.pas
===================================================================
--- components/lazutils/lcsvutils.pas	(revision 49723)
+++ components/lazutils/lcsvutils.pas	(working copy)
@@ -60,7 +60,8 @@
   begin
     Len := Length(curWord);
     SetLength(curWord, Len+(leadPtr-wordPtr));
-    Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr));
+    if Length(curWord) > 0 then
+      Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr));
     Inc(leadPtr);
     wordPtr := leadPtr;
   end;
lcsvutils.pas.patch (525 bytes)

wp

2015-08-29 01:34

developer   ~0085575

Last edited: 2015-08-29 01:38

View 2 revisions

Just found another bug: Bart mentioning "empty" lines lead me to look what happens if the file contains empty lines -> Crash: RunError(201), line 64 of lcsvutils.pas.

    SetLength(curWord, Len+(leadPtr-wordPtr)); <-- 0 in case of an empty line
    Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr)); <-- crash here

It could be argued that empty lines do not follow the csv standard, but the application should not crash in any way.

The attached "lcsvutils.pas.patch" fixes the bug.

Jesus Reyes

2015-08-30 19:59

developer   ~0085588

@wp: The check on empty lines looks OK, but I was not able to reproduce the problem on files with empty lines (without any patching), I tried empty lines at start, in the middle or at the end of file. Can you please attach a sample file that reproduce the problem?.

wp

2015-08-30 23:14

developer  

StringGrid_CSVImport_Crash.zip (1,398 bytes)

wp

2015-08-30 23:16

developer   ~0085590

Run new demo "StringGrid_CSVImport_Crash" to see it happen in FormCreate when the malformed file is loaded; the malformed file is contained in the zip.

Jesus Reyes

2015-08-31 18:43

developer   ~0085603

Last edited: 2015-08-31 18:44

View 2 revisions

Strange, here it doesn't crash. Tested on Linux and on Mac (using FPC trunk in both installs, probably different revisions though)

wp

2015-08-31 19:05

developer   ~0085604

Strange indeed. My first observation was on Win7 with fpc 2.6.4/Laz trunk. Now I tested also on Linux Mint (fpc 2.6.4 / Laz trunk) and Win7 fpc trunk / Laz trunk). Always crashing...

Jesus Reyes

2015-08-31 19:36

developer   ~0085605

I just tried Lazarus Trunk using FPC 2.6.4 and it works fine here, also tried 1.4 fixes with FPC 2.6.4 works fine too, tried also an old 1.2.5 with FPC 2.6.5 no problem too.

Maybe it's time to check some things.

1. The lpi included in the zip file (crash) it's zero bytes, I reconstructed it.
2. I'm testing without any patch (no grids.pas.patch and no lcsvutils.pas.patch)

let's compare because this is becoming weird.

wp

2015-08-31 20:00

developer   ~0085606

1. Sorry for the 0-byte file, no idea how this happened. I had to reconstruct it also today for the tests mentioned above.

2. Yes, also no patch which I reverted on my normal development system (Laz trunk /fpc 2.6.4). The Mint and fpc trunk installations have never seen the patch.

BUT:
Tried Laz 1.42 / fpc 2.6.4 --> NO PROBLEM. But I see that this has the csv parsing inside the LoadFromCSVStream. Some time ago this has been moved into a separate unit, lcsvutils. This is where the crash happens. Could you check where the csv parsing occurs in your tests?

wp

2015-08-31 22:28

developer   ~0085608

lcsvutils came into Laz trunk in r48780, the buggy line is already in the very first commit.

Bart Broersma

2015-08-31 23:03

developer   ~0085609

I cannot get it to crash.
I used the file in StringGrid_CSVImport_Crash.zip (and other files with multiple empty lines at start, in middle and at end of that file).
Lazarus 1.5 r49723 FPC 2.6.4 i386-win32-win32/win64 on Win7.

wp

2015-09-01 00:31

developer   ~0085610

It's incredible...

Did a clean install of Laz trunk with FPC 2.6.4. --> NO CRASH

Then I saw that Lazarus is compiled as "Normal IDE", but I normally have "Debug IDE". Recompiled the new Lazarus with "Debug IDE" --> CRASH

Switched back to my old Laz trunk / FPC 2.6.4, recompiled as "Normal IDE" --> NO CRASH
Going back to "Debug IDE" --> CRASH again.
Remove the -Cr from the "Debug IDE" --> NO CRASH.

Now it is clear: Rangecheck error! I could have seen this much earlier if I had looked at the error message RangeError(201).

    SetLength(curWord, Len+(leadPtr-wordPtr));
    Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr)); <-- crash here

When the "crash" occurs, Len + lenPtr - wordPtr = 0. i.e. the Move operation writes to the first byte of a string of 0 length --> Range check error.

Jesus Reyes

2015-09-01 02:19

developer   ~0085611

This means len=0 and leadPtr=wordPtr, which makes move() write 0 bytes.

It is IMO questionable that writing 0 bytes should raise any error.

As things are as they are, a check should not matter, I will shortly commit the patches.

Thanks for solving the mistery.

Jesus Reyes

2015-09-01 02:24

developer   ~0085612

Forgot to write, the reason for testing with an older version (a version that doesn't use lcsvutils) is because in older version empty lines are ignored and in the unpatched version they are not, empty rows are inserted instead, this raise the question of what should be the right behaviour?.

wp

2015-09-01 11:14

developer   ~0085614

>> what should be the right behaviour

Maybe ignore them for consistency?

>> This means len=0 and leadPtr=wordPtr, which makes move() write 0 bytes. It is IMO questionable that writing 0 bytes should raise any error.

Shouldn't it depend on the order in which the compiler handles the parameters? If is sees the 0-length string first maybe it does not check the byte count at all.

Jesus Reyes

2015-09-01 21:26

developer   ~0085637

I applied the patch as supplied.

I think a little more work is needed, perhaps a better naming on arguments and adding another one for controlling whether empty lines should be reflected or not (ommiting them should be the default).

wp

2015-09-01 22:46

developer   ~0085640

I am attaching another patch which adds a boolean parameter "SkipEmptyLines" to the parameter list. It defaults to "true" to keep the old behavior.

For testing, I am attaching also the modified demo of the original report. The data files provided contain empty lines now. You can toggle the "SkipEmptyLines" by means of the corresponding checkbox.

wp

2015-09-01 22:47

developer  

StringGrid_CSV_SkipEmptyLines.zip (4,725 bytes)

wp

2015-09-01 22:47

developer  

grids-skip-empty-lines.patch (2,809 bytes)
Index: lcl/grids.pas
===================================================================
--- lcl/grids.pas	(revision 49735)
+++ lcl/grids.pas	(working copy)
@@ -1625,8 +1625,10 @@
       procedure Clean(StartCol,StartRow,EndCol,EndRow: integer; CleanOptions: TGridZoneSet); overload;
       procedure CopyToClipboard(AUseSelection: boolean = false);
       procedure InsertRowWithValues(Index: Integer; Values: array of String);
-      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
-      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
+      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=',';
+        WithHeader: boolean=true; SkipLines: Integer=0; SkipEmptyLines: Boolean=true);
+      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=',';
+        WithHeader: boolean=true; SkipLines: Integer=0; SkipEmptyLines: Boolean=true);
       procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true;
                                 VisibleColumnsOnly: boolean=false);
       procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=','; WithHeader: boolean=true;
@@ -10853,7 +10855,8 @@
 end;
 
 procedure TCustomStringGrid.LoadFromCSVStream(AStream: TStream;
-  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
+  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0;
+  SkipEmptyLines: Boolean=true);
 var
   MaxCols: Integer = 0;
   MaxRows: Integer = 0;
@@ -10871,6 +10874,7 @@
   procedure NewRecord(Fields:TStringlist);
   var
     i, aRow: Integer;
+    empty: Boolean;
   begin
     inc(LineCounter);
     if (LineCounter < SkipLines) then
@@ -10879,6 +10883,16 @@
     if Fields.Count=0 then
       exit;
 
+    if SkipEmptyLines then begin
+      empty := true;
+      for i:=0 to Fields.Count-1 do
+        if Fields[i] <> '' then begin
+          empty := false;
+          break;
+        end;
+      if empty then exit;
+    end;
+
     // make sure we have enough columns
     if MaxCols<Fields.Count then
       MaxCols := Fields.Count;
@@ -10934,13 +10948,14 @@
 end;
 
 procedure TCustomStringGrid.LoadFromCSVFile(AFilename: string;
-  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
+  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0;
+  SkipEmptyLines: Boolean=true);
 var
   TheStream: TFileStreamUtf8;
 begin
   TheStream:=TFileStreamUtf8.Create(AFileName,fmOpenRead or fmShareDenyWrite);
   try
-    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines);
+    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines, SkipEmptyLines);
   finally
     TheStream.Free;
   end;

LacaK

2015-09-02 07:43

developer   ~0085645

Last edited: 2015-09-02 07:44

View 2 revisions

IMO according to RFC4180 file with empty line can be valid if there is only one field:
  header<CRLF>
  value1<CRLF>
  <CRLF>
  value3<CRLF>

while if there are more than one fields:
  headerA,headerB<CRLF>
  valueA1,valueB1<CRLF>
  <CRLF> //<--INVALID
  ,<CRLF> //<--VALID
  valueA3,valueB3<CRLF>

if invalid line is detected then simplest would be silently ignore it or raise exception (but not load such line as with empty fileds).

wp

2015-09-02 12:21

developer   ~0085651

I hate reading these rfc docs...

If we want to be consistent with rfc4180 then we should remove also the new "SkipLines" argument which allows to begin reading somewhere in the file, not only at the top, as Bart had suggested above. This would be against my practical experience, though. But anyway, we cannot cover all cases in this simple add-on method of the grid (my personal opinion has always been that the grid should not have its own csv import/export, this task should be done by other classes).

I could modify the patch in the following way:
- remove the "SkipLines" parameter to enforce reading at the top
- remove the "SkipEmptyLines" parameter; empty lines will raise an exception (unless there is only one field).
- varying field count from line to line will raise an exception (maybe already done somewhere in lcsvutils?)

In this case, docs of TStringGrid should state something like "LoadFromCSVFile and LoadFromCSVStream support only files being consistent with rfc4180. An exception is raised otherwise."

Jesus Reyes

2015-09-02 19:31

developer   ~0085676

I think we should make it flexible enough to be practical. If we do it strictly rfc 4180 compliant, it will be in my opinion less flexible. By making it flexible, we could even achieve a rfc 4180 compliant mode.

It seems to me that LibreOffice (LO) calc CSV support is flexible enough for most practical cases (though I don't know if the LO CSV support is claimed to be rfc4180 compliant), so I implemented the lcsvutils parser a little like that, of course I have not looked at LO source code so I don't claim that the current grid support is identically.

I would like continue to keep it that way, in this sense, wp SkipLines resemble the LO "from Row" property and I think it's a good step towards this goal.

BTW, I think fromRow or (start|from)(Line|Row) it's a little more clear than SkipLines (integer) because the name clash with the next parameter SkipEmptyLines (Boolean). I would suggest naming the arguments this way:
  WithHeaders->useTitles
  SkipLines->(fromLine|fromRow) (though the semantics will change a little) or alternatively skipHeaderLinesCount
  SkipEmptyLines->ignoreEmptyLines (this would clarify, in my opinion, that the ignored lines are not part of the header lines or at least they count after "from row").

wp

2015-09-02 21:09

developer  

grids-v3.pas.patch (5,755 bytes)
Index: components/lazutils/lcsvutils.pas
===================================================================
--- components/lazutils/lcsvutils.pas	(revision 49739)
+++ components/lazutils/lcsvutils.pas	(working copy)
@@ -57,8 +57,10 @@
   procedure StorePart;
   var
     Len: Integer;
+    x: Integer;
   begin
     Len := Length(curWord);
+    x := leadptr - wordptr;
     SetLength(curWord, Len+(leadPtr-wordPtr));
     if Length(curWord) > 0 then
       Move(wordPtr^, curWord[Len+1], (leadPtr-wordPtr));
Index: lcl/grids.pas
===================================================================
--- lcl/grids.pas	(revision 49739)
+++ lcl/grids.pas	(working copy)
@@ -1625,12 +1625,14 @@
       procedure Clean(StartCol,StartRow,EndCol,EndRow: integer; CleanOptions: TGridZoneSet); overload;
       procedure CopyToClipboard(AUseSelection: boolean = false);
       procedure InsertRowWithValues(Index: Integer; Values: array of String);
-      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
-      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
-      procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=','; WithHeader: boolean=true;
-                                VisibleColumnsOnly: boolean=false);
-      procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=','; WithHeader: boolean=true;
-                                VisibleColumnsOnly: boolean=false);
+      procedure LoadFromCSVStream(AStream: TStream; ADelimiter: Char=',';
+        UseTitles: boolean=true; FromLine: Integer=0; SkipEmptyLines: Boolean=true);
+      procedure LoadFromCSVFile(AFilename: string; ADelimiter: Char=',';
+        UseTitles: boolean=true; FromLine: Integer=0; SkipEmptyLines: Boolean=true);
+      procedure SaveToCSVStream(AStream: TStream; ADelimiter: Char=',';
+        WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
+      procedure SaveToCSVFile(AFileName: string; ADelimiter: Char=',';
+        WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
 
       property Cells[ACol, ARow: Integer]: string read GetCells write SetCells;
       property Cols[index: Integer]: TStrings read GetCols write SetCols;
@@ -10853,7 +10855,8 @@
 end;
 
 procedure TCustomStringGrid.LoadFromCSVStream(AStream: TStream;
-  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
+  ADelimiter: Char=','; UseTitles: boolean=true; FromLine: Integer=0;
+  SkipEmptyLines: Boolean=true);
 var
   MaxCols: Integer = 0;
   MaxRows: Integer = 0;
@@ -10862,7 +10865,7 @@
   function RowOffset: Integer;
   begin
     // return row offset of current CSV record (MaxRows) which is 1 based
-    if withHeader then
+    if UseTitles then
       result := Max(0, FixedRows-1)  + Max(MaxRows-1, 0)
     else
       result := FixedRows + Max(MaxRows-1, 0);
@@ -10871,14 +10874,27 @@
   procedure NewRecord(Fields:TStringlist);
   var
     i, aRow: Integer;
+    empty: Boolean;
   begin
     inc(LineCounter);
-    if (LineCounter < SkipLines) then
+    if (LineCounter < FromLine) then
       exit;
 
     if Fields.Count=0 then
       exit;
 
+    if SkipEmptyLines then begin
+      if (MaxCols > 1) then begin   // Ignore "SkipLines" for file with single column
+        empty := true;
+        for i:=0 to Fields.Count-1 do
+          if Fields[i] <> '' then begin
+            empty := false;
+            break;
+          end;
+        if empty then exit;
+      end;
+    end;
+
     // make sure we have enough columns
     if MaxCols<Fields.Count then
       MaxCols := Fields.Count;
@@ -10891,9 +10907,9 @@
         ColCount := MaxCols;
     end;
 
-    // setup columns captions of custom columns if they are enabled
+    // setup columns captions if enabled by UseTitles
     if (MaxRows = 0) then
-      if WithHeader then
+      if UseTitles then
       begin
         if Columns.Enabled then
           for i:=0 to Fields.Count-1 do Columns[i].Title.Caption:=Fields[i]
@@ -10934,13 +10950,14 @@
 end;
 
 procedure TCustomStringGrid.LoadFromCSVFile(AFilename: string;
-  ADelimiter: Char=','; WithHeader: boolean=true; SkipLines: Integer=0);
+  ADelimiter: Char=','; UseTitles: boolean=true; FromLine: Integer=0;
+  SkipEmptyLines: Boolean=true);
 var
   TheStream: TFileStreamUtf8;
 begin
   TheStream:=TFileStreamUtf8.Create(AFileName,fmOpenRead or fmShareDenyWrite);
   try
-    LoadFromCSVStream(TheStream, ADelimiter, WithHeader, SkipLines);
+    LoadFromCSVStream(TheStream, ADelimiter, UseTitles, FromLine, SkipEmptyLines);
   finally
     TheStream.Free;
   end;
@@ -10947,7 +10964,7 @@
 end;
 
 procedure TCustomStringGrid.SaveToCSVStream(AStream: TStream; ADelimiter: Char;
-  WithHeader: boolean=true; VisibleColumnsOnly: boolean=false);
+  WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
 var
   i,j,StartRow: Integer;
   HeaderL, Lines: TStringList;
@@ -10957,7 +10974,7 @@
     exit;
   Lines := TStringList.Create;
   try
-    if WithHeader then begin
+    if WriteTitles then begin
       if Columns.Enabled then begin
         if FixedRows>0 then begin
           HeaderL := TStringList.Create;
@@ -11019,13 +11036,13 @@
 end;
 
 procedure TCustomStringGrid.SaveToCSVFile(AFileName: string; ADelimiter: Char;
-  WithHeader: boolean=true; VisibleColumnsOnly: boolean=false);
+  WriteTitles: boolean=true; VisibleColumnsOnly: boolean=false);
 var
   TheStream: TFileStreamUtf8;
 begin
   TheStream:=TFileStreamUtf8.Create(AFileName,fmCreate);
   try
-    SaveToCSVStream(TheStream, ADelimiter, WithHeader);
+    SaveToCSVStream(TheStream, ADelimiter, WriteTitles, VisibleColumnsOnly);
   finally
     TheStream.Free;
   end;
grids-v3.pas.patch (5,755 bytes)

wp

2015-09-02 21:14

developer   ~0085677

Last edited: 2015-09-02 21:21

View 2 revisions

Patch "grids-v3.pas.patch" implements your suggestion.

In addition I used the optional parameter "VisibleColumnsOnly" in "SaveToCSVFile" which seems to be missing for no reason.

Jesus Reyes

2015-09-04 01:59

developer   ~0085696

Thanks, Applied with changes. I think "empty lines" should mean lines with only line ending separators and not lines with empty fields. The latter should always be accepted (IMO).

Also as mentioned before I think the parameter fromRow does not have the same meaning as the previous skipLines argument. One, by default, start reading the csv file from line number 1 which, yes, it is equivalent to reading the file skipping 0 lines but is not the same thing regarding the default value and the implemented logic.

wp

2015-09-04 13:50

developer   ~0085703

Last edited: 2015-09-04 13:51

View 2 revisions

>> "empty lines" should mean lines with only line ending separators and not lines with empty fields

Oh - I had not thought of this case...

>>One, by default, start reading the csv file from line number 1 which, yes, it is equivalent to reading the file skipping 0 lines but is not the same thing regarding the default value and the implemented logic.

Yes, text editors, such as NotePad++ or the Lazarus IDE, name the first line "1", but from a programmer's point of view I think it makes more sense to index it as "0", such as in StringList[0], Memo.Lines[0] etc. In fact, this ambiguity had motivated me to call the parameter "SkipLines" in the initial version (but, of course, the similarity with "SkipEmptyLines" is very confusiong...).

Anyway, to me this issue is solved.

Issue History

Date Modified Username Field Change
2015-08-28 22:44 wp New Issue
2015-08-28 22:44 wp File Added: grids.pas.patch
2015-08-28 22:45 wp File Added: StringGrid_CSVImport.zip
2015-08-28 22:46 wp File Added: NoHeader.png
2015-08-28 22:49 wp Description Updated View Revisions
2015-08-28 22:49 wp Steps to Reproduce Updated View Revisions
2015-08-29 00:30 Bart Broersma Note Added: 0085571
2015-08-29 00:38 Bart Broersma Note Added: 0085572
2015-08-29 00:49 wp Note Added: 0085573
2015-08-29 01:21 wp Note Added: 0085574
2015-08-29 01:27 wp File Added: lcsvutils.pas.patch
2015-08-29 01:34 wp Note Added: 0085575
2015-08-29 01:38 wp Note Edited: 0085575 View Revisions
2015-08-29 07:34 Jesus Reyes Assigned To => Jesus Reyes
2015-08-29 07:34 Jesus Reyes Status new => assigned
2015-08-30 19:59 Jesus Reyes Note Added: 0085588
2015-08-30 19:59 Jesus Reyes Status assigned => feedback
2015-08-30 23:14 wp File Added: StringGrid_CSVImport_Crash.zip
2015-08-30 23:16 wp Note Added: 0085590
2015-08-30 23:16 wp Status feedback => assigned
2015-08-31 18:43 Jesus Reyes Note Added: 0085603
2015-08-31 18:44 Jesus Reyes Note Edited: 0085603 View Revisions
2015-08-31 19:05 wp Note Added: 0085604
2015-08-31 19:36 Jesus Reyes Note Added: 0085605
2015-08-31 20:00 wp Note Added: 0085606
2015-08-31 22:28 wp Note Added: 0085608
2015-08-31 23:03 Bart Broersma Note Added: 0085609
2015-09-01 00:31 wp Note Added: 0085610
2015-09-01 02:19 Jesus Reyes Note Added: 0085611
2015-09-01 02:24 Jesus Reyes Note Added: 0085612
2015-09-01 11:14 wp Note Added: 0085614
2015-09-01 21:26 Jesus Reyes Fixed in Revision => 49735
2015-09-01 21:26 Jesus Reyes LazTarget - => 1.6
2015-09-01 21:26 Jesus Reyes Note Added: 0085637
2015-09-01 21:26 Jesus Reyes Status assigned => resolved
2015-09-01 21:26 Jesus Reyes Fixed in Version => 1.5 (SVN)
2015-09-01 21:26 Jesus Reyes Resolution open => fixed
2015-09-01 21:26 Jesus Reyes Target Version => 1.6
2015-09-01 22:46 wp Note Added: 0085640
2015-09-01 22:46 wp Status resolved => assigned
2015-09-01 22:46 wp Resolution fixed => reopened
2015-09-01 22:47 wp File Added: StringGrid_CSV_SkipEmptyLines.zip
2015-09-01 22:47 wp File Added: grids-skip-empty-lines.patch
2015-09-02 07:43 LacaK Note Added: 0085645
2015-09-02 07:44 LacaK Note Edited: 0085645 View Revisions
2015-09-02 12:21 wp Note Added: 0085651
2015-09-02 19:31 Jesus Reyes Note Added: 0085676
2015-09-02 21:09 wp File Added: grids-v3.pas.patch
2015-09-02 21:14 wp Note Added: 0085677
2015-09-02 21:21 wp Note Edited: 0085677 View Revisions
2015-09-04 01:59 Jesus Reyes Fixed in Revision 49735 => 49735, 49754
2015-09-04 01:59 Jesus Reyes Note Added: 0085696
2015-09-04 13:50 wp Note Added: 0085703
2015-09-04 13:51 wp Note Edited: 0085703 View Revisions
2015-09-08 22:09 wp Status assigned => resolved
2015-09-08 22:09 wp Resolution reopened => fixed
2015-09-08 22:09 wp Status resolved => closed