diff --git a/components/fpspreadsheet/examples/excel2demo/excel2write.lpr b/components/fpspreadsheet/examples/excel2demo/excel2write.lpr index 7f5c692f0..09c279c48 100644 --- a/components/fpspreadsheet/examples/excel2demo/excel2write.lpr +++ b/components/fpspreadsheet/examples/excel2demo/excel2write.lpr @@ -55,8 +55,11 @@ begin MyWorksheet.WriteUTF8Text(1, 2, 'Third'); MyWorksheet.WriteUTF8Text(1, 3, 'Fourth'); + // Write current date/time + MyWorksheet.WriteDateTime(2, 0, now); + // Save the spreadsheet to a file - MyWorkbook.WriteToFile(MyDir + 'test' + STR_EXCEL_EXTENSION, sfExcel2); + MyWorkbook.WriteToFile(MyDir + 'test' + STR_EXCEL_EXTENSION, sfExcel2, true); MyWorkbook.Free; end. diff --git a/components/fpspreadsheet/examples/excel5demo/excel5write.lpr b/components/fpspreadsheet/examples/excel5demo/excel5write.lpr index f8e55b209..e259271d0 100644 --- a/components/fpspreadsheet/examples/excel5demo/excel5write.lpr +++ b/components/fpspreadsheet/examples/excel5demo/excel5write.lpr @@ -10,8 +10,7 @@ program excel5write; {$mode delphi}{$H+} uses - Classes, SysUtils, fpspreadsheet, xlsbiff5, - laz_fpspreadsheet, fpsconvencoding; + Classes, SysUtils, fpspreadsheet, xlsbiff5; const Str_First = 'First'; @@ -40,9 +39,12 @@ begin MyWorksheet.WriteNumber(0, 2, 3.0);// C1 MyWorksheet.WriteNumber(0, 3, 4.0);// D1 MyWorksheet.WriteUTF8Text(4, 2, Str_Total);// C5 - MyWorksheet.WriteNumber(4, 3, 10); // D5 + MyWorksheet.WriteNumber(4, 3, 10); // D5 -{ Uncommend this to test large XLS files + // Write current date/time + MyWorksheet.WriteDateTime(5, 0, now); + +{ Uncomment this to test large XLS files for i := 2 to 20 do begin MyWorksheet.WriteAnsiText(i, 0, ParamStr(0)); @@ -83,7 +85,7 @@ begin MyWorksheet.WriteUTF8Text(0, 3, Str_Fourth); // Save the spreadsheet to a file - MyWorkbook.WriteToFile(MyDir + 'test.xls', sfExcel5, False); + MyWorkbook.WriteToFile(MyDir + 'test.xls', sfExcel5, true); MyWorkbook.Free; end. diff --git a/components/fpspreadsheet/examples/excel8demo/excel8write.lpr b/components/fpspreadsheet/examples/excel8demo/excel8write.lpr index 247ddf4f9..4eb10de41 100644 --- a/components/fpspreadsheet/examples/excel8demo/excel8write.lpr +++ b/components/fpspreadsheet/examples/excel8demo/excel8write.lpr @@ -28,6 +28,7 @@ var MyDir: string; i: Integer; lCell: PCell; + number: Double; begin MyDir := ExtractFilePath(ParamStr(0)); @@ -41,7 +42,7 @@ begin MyWorksheet.WriteNumber(0, 2, 3.0);// C1 MyWorksheet.WriteNumber(0, 3, 4.0);// D1 MyWorksheet.WriteUTF8Text(4, 2, Str_Total);// C5 - MyWorksheet.WriteNumber(4, 3, 10); // D5 + MyWorksheet.WriteNumber(4, 3, 10); // D5 // D6 number with background color MyWorksheet.WriteNumber(5, 3, 10); @@ -49,7 +50,7 @@ begin lCell^.BackgroundColor := scPURPLE; lCell^.UsedFormattingFields := [uffBackgroundColor]; -{ Uncommend this to test large XLS files +{ Uncomment this to test large XLS files for i := 2 to 20 do begin MyWorksheet.WriteAnsiText(i, 0, ParamStr(0)); @@ -80,6 +81,63 @@ begin //MyFormula.FormulaStr := ''; + // Write current date/time to cells B11:B16 + MyWorksheet.WriteUTF8Text(10, 0, 'nfShortDate'); + MyWorksheet.WriteDateTime(10, 1, now, nfShortDate); + MyWorksheet.WriteUTF8Text(11, 0, 'nfShortTime'); + MyWorksheet.WriteDateTime(11, 1, now, nfShortTime); + MyWorksheet.WriteUTF8Text(12, 0, 'nfLongTime'); + MyWorksheet.WriteDateTime(12, 1, now, nfLongTime); + MyWorksheet.WriteUTF8Text(13, 0, 'nfShortDateTime'); + MyWorksheet.WriteDateTime(13, 1, now, nfShortDateTime); + MyWorksheet.WriteUTF8Text(14, 0, 'nfFmtDateTime, DM'); + MyWorksheet.WriteDateTime(14, 1, now, nfFmtDateTime, 'DM'); + MyWorksheet.WriteUTF8Text(15, 0, 'nfFmtDateTime, MY'); + MyWorksheet.WriteDateTime(15, 1, now, nfFmtDateTime, 'MY'); + MyWorksheet.WriteUTF8Text(16, 0, 'nfShortTimeAM'); + MyWorksheet.WriteDateTime(16, 1, now, nfShortTimeAM); + MyWorksheet.WriteUTF8Text(17, 0, 'nfLongTimeAM'); + MyWorksheet.WriteDateTime(17, 1, now, nfLongTimeAM); + MyWorksheet.WriteUTF8Text(18, 0, 'nfFmtDateTime, MS'); + MyWorksheet.WriteDateTime(18, 1, now, nfFmtDateTime, 'MS'); + MyWorksheet.WriteUTF8Text(19, 0, 'nfFmtDateTime, MSZ'); + MyWorksheet.WriteDateTime(19, 1, now, nfFmtDateTime, 'MSZ'); + + // Write formatted numbers + number := 12345.67890123456789; + MyWorksheet.WriteUTF8Text(24, 1, '12345.67890123456789'); + MyWorksheet.WriteUTF8Text(24, 2, '-12345.67890123456789'); + MyWorksheet.WriteUTF8Text(25, 0, 'nfFixed, 0 decs'); + MyWorksheet.WriteNumber(25, 1, number, nfFixed, 0); + MyWorksheet.WriteNumber(25, 2, -number, nfFixed, 0); + MyWorksheet.WriteUTF8Text(26, 0, 'nfFixed, 2 decs'); + MyWorksheet.WriteNumber(26, 1, number, nfFixed, 2); + MyWorksheet.WriteNumber(26, 2, -number, nfFixed, 2); + MyWorksheet.WriteUTF8Text(27, 0, 'nfFixedTh, 0 decs'); + MyWorksheet.WriteNumber(27, 1, number, nfFixedTh, 0); + MyWorksheet.WriteNumber(27, 2, -number, nfFixedTh, 0); + MyWorksheet.WriteUTF8Text(28, 0, 'nfFixedTh, 2 decs'); + MyWorksheet.WriteNumber(28, 1, number, nfFixedTh, 2); + MyWorksheet.WriteNumber(28, 2, -number, nfFixedTh, 2); + MyWorksheet.WriteUTF8Text(29, 0, 'nfSci, 1 dec'); + MyWorksheet.WriteNumber(29, 1, number, nfSci); + MyWorksheet.WriteNumber(29, 2, -number, nfSci); + MyWorksheet.WriteNumber(29, 3, 1.0/number, nfSci); + MyWorksheet.WriteNumber(29, 4, -1.0/number, nfSci); + MyWorksheet.WriteUTF8Text(30, 0, 'nfExp, 2 decs'); + MyWorksheet.WriteNumber(30, 1, number, nfExp, 2); + MyWorksheet.WriteNumber(30, 2, -number, nfExp, 2); + MyWorksheet.WriteNumber(30, 3, 1.0/number, nfExp, 2); + MyWorksheet.WriteNumber(30, 4, -1.0/number, nfExp, 2); + + number := 1.333333333; + MyWorksheet.WriteUTF8Text(35, 0, 'nfPercent, 0 decs'); + MyWorksheet.WriteNumber(35, 1, number, nfPercent, 0); + MyWorksheet.WriteUTF8Text(36, 0, 'nfPercent, 2 decs'); + MyWorksheet.WriteNumber(36, 1, number, nfPercent, 2); + MyWorksheet.WriteUTF8Text(37, 0, 'nfTimeInterval'); + MyWorksheet.WriteDateTime(37, 1, number, nfTimeInterval); + // Creates a new worksheet MyWorksheet := MyWorkbook.AddWorksheet(Str_Worksheet2); @@ -92,7 +150,7 @@ begin MyWorksheet.WriteUsedFormatting(0, 1, [uffBold]); // Save the spreadsheet to a file - MyWorkbook.WriteToFile(MyDir + 'test2.xls', sfExcel8, False); + MyWorkbook.WriteToFile(MyDir + 'test.xls', sfExcel8, true); MyWorkbook.Free; end. diff --git a/components/fpspreadsheet/examples/ooxmldemo/ooxmlwrite.lpr b/components/fpspreadsheet/examples/ooxmldemo/ooxmlwrite.lpr index 35c000716..9f455347a 100644 --- a/components/fpspreadsheet/examples/ooxmldemo/ooxmlwrite.lpr +++ b/components/fpspreadsheet/examples/ooxmldemo/ooxmlwrite.lpr @@ -33,7 +33,7 @@ begin MyWorksheet.WriteNumber(0, 2, 3.0); MyWorksheet.WriteNumber(0, 3, 4.0); -// Uncommend this to test large XLS files +// Uncomment this to test large XLS files for i := 2 to 2{20} do begin MyWorksheet.WriteUTF8Text(i, 0, ParamStr(0)); @@ -61,6 +61,9 @@ begin MyWorksheet.WriteUTF8Text(0, 2, 'Third'); MyWorksheet.WriteUTF8Text(0, 3, 'Fourth'); + // Write current date/time + MyWorksheet.WriteDateTime(0, 5, now); + // Save the spreadsheet to a file MyWorkbook.WriteToFile(MyDir + 'test.xlsx', sfOOXML); MyWorkbook.Free; diff --git a/components/fpspreadsheet/examples/opendocdemo/opendocwrite.lpr b/components/fpspreadsheet/examples/opendocdemo/opendocwrite.lpr index 19752efe6..23885550b 100644 --- a/components/fpspreadsheet/examples/opendocdemo/opendocwrite.lpr +++ b/components/fpspreadsheet/examples/opendocdemo/opendocwrite.lpr @@ -31,6 +31,7 @@ begin MyWorksheet.WriteNumber(0, 3, 4.0);// D1 MyWorksheet.WriteUTF8Text(4, 2, 'Total:');// C5 MyWorksheet.WriteNumber(4, 3, 10); // D5 + MyWorksheet.WriteDateTime(5, 0, now); // Add some formatting MyWorksheet.WriteUsedFormatting(0, 0, [uffBold]); diff --git a/components/fpspreadsheet/fpsopendocument.pas b/components/fpspreadsheet/fpsopendocument.pas index d8b27f5ee..bb6d56446 100755 --- a/components/fpspreadsheet/fpsopendocument.pas +++ b/components/fpspreadsheet/fpsopendocument.pas @@ -89,6 +89,7 @@ type procedure WriteFormula(AStream: TStream; const ARow, ACol: Cardinal; const AFormula: TsFormula; ACell: PCell); override; procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); override; procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal; const AValue: double; ACell: PCell); override; + procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); override; end; implementation @@ -709,6 +710,16 @@ begin ' ' + LineEnding; end; +{ + Writes the date/time as a text to the sheet. + Currently, no formatting code is written. +} +procedure TsSpreadOpenDocWriter.WriteDateTime(AStream: TStream; + const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); +begin + WriteLabel(AStream, ARow, ACol, FormatDateTime('c', AValue), ACell); +end; + { Registers this reader / writer on fpSpreadsheet } diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index 8ad39045b..962f83321 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -102,7 +102,9 @@ type enough to be able to read/write date values. } - TsNumberFormat = (nfGeneral, nfShortDate, nfShortDateTime); + TsNumberFormat = (nfGeneral, nfFixed, nfFixedTh, nfExp, nfSci, nfPercentage, + nfShortDateTime, nfFmtDateTime, nfShortDate, nfShortTime, nfLongTime, + nfShortTimeAM, nfLongTimeAM, nfTimeInterval); {@@ Text rotation formatting. The text is rotated relative to the standard orientation, which is from left to right horizontal: ---> @@ -187,6 +189,8 @@ type Border: TsCellBorders; BackgroundColor: TsColor; NumberFormat: TsNumberFormat; + NumberFormatStr: String; + NumberDecimals: Word; RGBBackgroundColor: TFPColor; // only valid if BackgroundColor=scRGBCOLOR end; @@ -242,8 +246,10 @@ type function ReadBackgroundColor(ARow, ACol: Cardinal): TsColor; procedure RemoveAllCells; procedure WriteUTF8Text(ARow, ACol: Cardinal; AText: ansistring); - procedure WriteNumber(ARow, ACol: Cardinal; ANumber: double); - procedure WriteDateTime(ARow, ACol: Cardinal; AValue: TDateTime); + procedure WriteNumber(ARow, ACol: Cardinal; ANumber: double; + AFormat: TsNumberFormat = nfGeneral; ADecimals: Word = 2); + procedure WriteDateTime(ARow, ACol: Cardinal; AValue: TDateTime; + AFormat: TsNumberFormat = nfShortDateTime; AFormatStr: String = ''); procedure WriteFormula(ARow, ACol: Cardinal; AFormula: TsFormula); procedure WriteNumberFormat(ARow, ACol: Cardinal; ANumberFormat: TsNumberFormat); procedure WriteRPNFormula(ARow, ACol: Cardinal; AFormula: TsRPNFormula); @@ -378,10 +384,14 @@ procedure RegisterSpreadFormat( AWriterClass: TsSpreadWriterClass; AFormat: TsSpreadsheetFormat); +function SciFloat(AValue: Double; ADecimals: Word): String; +function TimeIntervalToString(AValue: TDateTime): String; + + implementation uses - Math; + Math, StrUtils; var { Translatable strings } @@ -405,6 +415,51 @@ begin GsSpreadFormats[len].Format := AFormat; end; +{@@ + Formats the number AValue in "scientific" format with the given number of + decimals. "Scientific" is the same as "exponential", but with exponents rounded + to multiples of 3. +} +function SciFloat(AValue: Double; ADecimals: Word): String; +var + m: Double; + ex: Integer; +begin + if AValue = 0 then + Result := '0.0' + else begin + ex := floor(log10(abs(AValue))); // exponent + // round exponent to multiples of 3 + ex := (ex div 3) * 3; + if ex < 0 then dec(ex, 3); + m := AValue * Power(10, -ex); // mantisse + Result := Format('%.*fE%d', [ADecimals, m, ex]); + end; +end; + +{@@ + Formats the number AValue as a time string with hours, minutes and seconds. + Unlike TimeToStr there can be more than 24 hours. +} +function TimeIntervalToString(AValue: TDateTime): String; +var + hrs: Integer; + diff: Double; + h,m,s,z: Word; + ts: String; +begin + ts := DefaultFormatSettings.TimeSeparator; + DecodeTime(frac(abs(AValue)), h, m, s, z); + hrs := h + trunc(abs(AValue))*24; + if z > 499 then inc(s); + if hrs > 0 then + Result := Format('%d%s%.2d%s%.2d', [hrs, ts, m, ts, s]) + else + Result := Format('%d%s%.2d', [m, ts, s]); + if AValue < 0.0 then Result := '-' + Result; +end; + + { TsWorksheet } {@@ @@ -673,12 +728,42 @@ end; @return The text representation of the cell } function TsWorksheet.ReadAsUTF8Text(ARow, ACol: Cardinal): ansistring; + + function FloatToStrNoNaN(const Value: Double; + ANumberFormat: TsNumberFormat; ANumberFormatStr: ansistring): ansistring; + begin + if IsNan(Value) then + Result := '' + else + if ANumberFormat = nfSci then + Result := SciFloat(Value, 1) + else + if (ANumberFormat = nfGeneral) or (ANumberFormatStr = '') then + Result := FloatToStr(Value) + else + if (ANumberFormat = nfPercentage) then + Result := FormatFloat(ANumberFormatStr, Value*100) + '%' + else + Result := FormatFloat(ANumberFormatStr, Value); + end; + + function DateTimeToStrNoNaN(const Value: Double; + ANumberFormat: TsNumberFormat; ANumberFormatStr: String): ansistring; + begin + Result := ''; + if not IsNaN(Value) then begin + if ANumberFormat = nfTimeInterval then + Result := TimeIntervalToString(Value) + else + if ANumberFormatStr = '' then + Result := FormatDateTime('c', Value) + else + Result := FormatDateTime(ANumberFormatStr, Value); + end; + end; + var ACell: PCell; - function FloatToStrNoNaN(const Value: Double): ansistring; - begin - if IsNan(Value) then Result:='' else Result:=FloatToStr(Value); - end; begin ACell := FindCell(ARow, ACol); @@ -690,17 +775,9 @@ begin case ACell^.ContentType of //cctFormula - cctNumber: Result := FloatToStrNoNaN(ACell^.NumberValue); + cctNumber: Result := FloatToStrNoNaN(ACell^.NumberValue, ACell^.NumberFormat, ACell^.NumberFormatStr); cctUTF8String: Result := ACell^.UTF8StringValue; - cctDateTime: - begin - Result := SysUtils.DateToStr(ACell^.DateTimeValue); - // User can have specified custom date/time format or one of the other built - // in formats that include time. We can't parse all of them so just return - // time as well unless absolutely sure we only want a date - if ACell^.NumberFormat<>nfShortDate then - Result := Result+' ' + SysUtils.TimeToStr(ACell^.DateTimeValue); - end; + cctDateTime: Result := DateTimeToStrNoNaN(ACell^.DateTimeValue, ACell^.NumberFormat, ACell^.NumberFormatStr); else Result := ''; end; @@ -839,35 +916,63 @@ end; @param ARow The row of the cell @param ACol The column of the cell @param ANumber The number to be written + @param AFormat The format identifier, e.g. nfFixed (optional) + @param ADecimals The number of decimals used for formatting (optional) } -procedure TsWorksheet.WriteNumber(ARow, ACol: Cardinal; ANumber: double); +procedure TsWorksheet.WriteNumber(ARow, ACol: Cardinal; ANumber: double; + AFormat: TsNumberFormat = nfGeneral; ADecimals: Word = 2); var ACell: PCell; + decs: String; begin ACell := GetCell(ARow, ACol); ACell^.ContentType := cctNumber; ACell^.NumberValue := ANumber; + ACell^.NumberDecimals := ADecimals; + if AFormat <> nfGeneral then begin + Include(ACell^.UsedFormattingFields, uffNumberFormat); + ACell^.NumberFormat := AFormat; + decs := DupeString('0', ADecimals); + if ADecimals > 0 then decs := '.' + decs; + case AFormat of + nfFixed: + ACell^.NumberFormatStr := '0' + decs; + nfFixedTh: + ACell^.NumberFormatStr := '#,##0' + decs; + nfExp: + ACell^.NumberFormatStr := '0' + decs + 'E+00'; + nfSci: + ACell^.NumberFormatStr := ''; + nfPercentage: + ACell^.NumberFormatStr := '0' + decs; + end; + end; end; {@@ Writes a date/time value to a determined cell - @param ARow The row of the cell - @param ACol The column of the cell - @param AValue The date/time/datetime to be written + @param ARow The row of the cell + @param ACol The column of the cell + @param AValue The date/time/datetime to be written + @param AFormat The format specifier, e.g. nfShortDate (optional) + @param AFormatStr Format string, used only for nfFmtDateTime. + Must follow the rules for "FormatDateTime", or use + "dm" as abbreviation for "d/mmm", "my" for "mmm/yy", + "ms" for "nn:ss", "msz" for "nn:ss.z" (optional) Note: at least Excel xls does not recognize a separate datetime cell type: a datetime is stored as a (floating point) Number, and the cell is formatted as a date (either built-in or a custom format). - This procedure automatically sets the cell format to short date/time. You may - change this format to another date/time format, but changing it to another - format (e.g. General) will likely lead to the cell being written out as a - plain number. + + Note: custom formats are currently not supported by the writer. } -procedure TsWorksheet.WriteDateTime(ARow, ACol: Cardinal; AValue: TDateTime); +procedure TsWorksheet.WriteDateTime(ARow, ACol: Cardinal; AValue: TDateTime; + AFormat: TsNumberFormat = nfShortDateTime; AFormatStr: String = ''); var ACell: PCell; + fmt: String; begin ACell := GetCell(ARow, ACol); @@ -876,11 +981,32 @@ begin // Date/time is actually a number field in Excel. // To make sure it gets saved correctly, set a date format (instead of General). // The user can choose another date format if he wants to - if not(uffNumberFormat in ACell^.UsedFormattingFields) or - ((uffNumberFormat in ACell^.UsedFormattingFields) and (ACell^.NumberFormat = nfGeneral)) then - begin - Include(ACell^.UsedFormattingFields, uffNumberFormat); - ACell^.NumberFormat := nfShortDateTime; + Include(ACell^.UsedFormattingFields, uffNumberFormat); + ACell^.NumberFormat := AFormat; + case AFormat of + nfShortDateTime: + ACell^.NumberFormatStr := FormatSettings.ShortDateFormat + ' ' + FormatSettings.ShortTimeFormat; + nfShortDate: + ACell^.NumberFormatStr := FormatSettings.ShortDateFormat; + nfShortTime: + ACell^.NumberFormatStr := 't'; + nfLongTime: + ACell^.NumberFormatStr := 'tt'; + nfShortTimeAM: + ACell^.NumberFormatStr := 't am/pm'; + nfLongTimeAM: + ACell^.NumberFormatStr := 'tt am/pm'; + nfFmtDateTime: + begin + fmt := lowercase(AFormatStr); + if fmt = 'dm' then ACell^.NumberFormatStr := 'd/mmm' + else if fmt = 'my' then ACell^.NumberFormatSTr := 'mmm/yy' + else if fmt = 'ms' then ACell^.NumberFormatStr := 'nn:ss' + else if fmt = 'msz' then ACell^.NumberFormatStr := 'nn:ss.z' + else ACell^.NumberFormatStr := AFormatStr; + end; + nfTimeInterval: + ACell^.NumberFormatStr := ''; end; end; @@ -1503,8 +1629,16 @@ begin if uffBackgroundColor in AFormat^.UsedFormattingFields then if (FFormattingStyles[i].BackgroundColor <> AFormat^.BackgroundColor) then Continue; - if uffNumberFormat in AFormat^.UsedFormattingFields then + if uffNumberFormat in AFormat^.UsedFormattingFields then begin if (FFormattingStyles[i].NumberFormat <> AFormat^.NumberFormat) then Continue; + case AFormat^.NumberFormat of + nfFixed, nfFixedTh, nfPercentage, nfExp: + if (FFormattingStyles[i].NumberDecimals <> AFormat^.NumberDecimals) then Continue; + nfShortDate, nfShortDateTime, nfShortTime, nfLongTime, nfShortTimeAM, + nfLongTimeAM, nfFmtDateTime, nfTimeInterval: + if (FFormattingstyles[i].NumberFormatStr <> AFormat^.NumberFormatStr) then Continue; + end; + end; // If we arrived here it means that the styles match Exit(i); @@ -1735,6 +1869,7 @@ begin // Silently dump the formula; child classes should implement their own support end; + finalization SetLength(GsSpreadFormats, 0); diff --git a/components/fpspreadsheet/tests/datetests.pas b/components/fpspreadsheet/tests/datetests.pas index b565b702a..186e21c84 100644 --- a/components/fpspreadsheet/tests/datetests.pas +++ b/components/fpspreadsheet/tests/datetests.pas @@ -22,7 +22,7 @@ uses var // Norm to test against - list of dates/times that should occur in spreadsheet - SollDates: array[0..11] of TDateTime; //"Soll" is a German word in Dutch accountancy circles meaning "normative value to check against". There ;) + SollDates: array[0..37] of TDateTime; //"Soll" is a German word in Dutch accountancy jargon meaning "normative value to check against". There ;) // Initializes Soll*/normative variables. // Useful in test setup procedures to make sure the norm is correct. procedure InitSollDates; @@ -53,6 +53,32 @@ type procedure TestReadDate9; procedure TestReadDate10; procedure TestReadDate11; + procedure TestReadDate12; + procedure TestReadDate13; + procedure TestReadDate14; + procedure TestReadDate15; + procedure TestReadDate16; + procedure TestReadDate17; + procedure TestReadDate18; + procedure TestReadDate19; + procedure TestReadDate20; + procedure TestReadDate21; + procedure TestReadDate22; + procedure TestReadDate23; + procedure TestReadDate24; + procedure TestReadDate25; + procedure TestReadDate26; + procedure TestReadDate27; + procedure TestReadDate28; + procedure TestReadDate29; + procedure TestReadDate30; + procedure TestReadDate31; + procedure TestReadDate32; + procedure TestReadDate33; + procedure TestReadDate34; + procedure TestReadDate35; + procedure TestReadDate36; + procedure TestReadDate37; procedure TestReadDate1899_0; //same as above except with the 1899/1900 date system set procedure TestReadDate1899_1; procedure TestReadDate1899_2; @@ -65,6 +91,32 @@ type procedure TestReadDate1899_9; procedure TestReadDate1899_10; procedure TestReadDate1899_11; + procedure TestReadDate1899_12; + procedure TestReadDate1899_13; + procedure TestReadDate1899_14; + procedure TestReadDate1899_15; + procedure TestReadDate1899_16; + procedure TestReadDate1899_17; + procedure TestReadDate1899_18; + procedure TestReadDate1899_19; + procedure TestReadDate1899_20; + procedure TestReadDate1899_21; + procedure TestReadDate1899_22; + procedure TestReadDate1899_23; + procedure TestReadDate1899_24; + procedure TestReadDate1899_25; + procedure TestReadDate1899_26; + procedure TestReadDate1899_27; + procedure TestReadDate1899_28; + procedure TestReadDate1899_29; + procedure TestReadDate1899_30; + procedure TestReadDate1899_31; + procedure TestReadDate1899_32; + procedure TestReadDate1899_33; + procedure TestReadDate1899_34; + procedure TestReadDate1899_35; + procedure TestReadDate1899_36; + procedure TestReadDate1899_37; end; { TSpreadWriteReadDateTests } @@ -109,6 +161,36 @@ begin SollDates[9]:=EncodeTime(18,0,0,0); SollDates[10]:=EncodeTime(23,59,0,0); SollDates[11]:=EncodeTime(23,59,59,0); + + SollDates[12]:=SollDates[1]; // #1 formatted as nfShortDateTime + SollDates[13]:=SollDates[1]; // #1 formatted as nfShortTime + SollDates[14]:=SollDates[1]; // #1 formatted as nfLongTime + SollDates[15]:=SollDates[1]; // #1 formatted as nfShortTimeAM + SollDates[16]:=SollDates[1]; // #1 formatted as nfLongTimeAM + SollDates[17]:=SollDates[1]; // #1 formatted as nfFmtDateTime dm + SollDates[18]:=SollDates[1]; // #1 formatted as nfFmtDateTime my + SollDates[19]:=SollDates[1]; // #1 formatted as nfFmtDateTime ms + + SollDates[20]:=SollDates[5]; // #5 formatted as nfShortDateTime + SollDates[21]:=SollDates[5]; // #5 formatted as nfShortTime + SollDates[22]:=SollDates[5]; // #5 formatted as nfLongTime + SollDates[23]:=SollDates[5]; // #5 formatted as nfShortTimeAM + SollDates[24]:=SollDates[5]; // #5 formatted as nfLongTimeAM + SollDates[25]:=SollDates[5]; // #5 formatted as nfFmtDateTime dm + SollDates[26]:=SollDates[5]; // #5 formatted as nfFmtDateTime my + SollDates[27]:=SollDates[5]; // #5 formatted as nfFmtDateTime ms + + SollDates[28]:=SollDates[11]; // #11 formatted as nfShortDateTime + SollDates[29]:=SollDates[11]; // #11 formatted as nfShortTime + SollDates[30]:=SollDates[11]; // #11 formatted as nfLongTime + SollDates[31]:=SollDates[11]; // #11 formatted as nfShortTimeAM + SollDates[32]:=SollDates[11]; // #11 formatted as nfLongTimeAM + SollDates[33]:=SollDates[11]; // #11 formatted as nfFmtDateTime dm + SollDates[34]:=SollDates[11]; // #11 formatted as nfFmtDateTime my + SollDates[35]:=SollDates[11]; // #11 formatted as nfFmtDateTime ms + + SollDates[36]:=EncodeTime(3,45,12,0); // formatted as nfTimeDuration + SollDates[37]:=EncodeTime(3,45,12,0) + 1 // formatted as nfTimeDuration end; { TSpreadWriteReadDateTests } @@ -270,6 +352,136 @@ begin TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,11); end; +procedure TSpreadReadDateTests.TestReadDate12; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,12); +end; + +procedure TSpreadReadDateTests.TestReadDate13; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,13); +end; + +procedure TSpreadReadDateTests.TestReadDate14; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,14); +end; + +procedure TSpreadReadDateTests.TestReadDate15; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,15); +end; + +procedure TSpreadReadDateTests.TestReadDate16; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,16); +end; + +procedure TSpreadReadDateTests.TestReadDate17; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,17); +end; + +procedure TSpreadReadDateTests.TestReadDate18; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,18); +end; + +procedure TSpreadReadDateTests.TestReadDate19; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,19); +end; + +procedure TSpreadReadDateTests.TestReadDate20; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,20); +end; + +procedure TSpreadReadDateTests.TestReadDate21; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,21); +end; + +procedure TSpreadReadDateTests.TestReadDate22; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,22); +end; + +procedure TSpreadReadDateTests.TestReadDate23; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,23); +end; + +procedure TSpreadReadDateTests.TestReadDate24; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,24); +end; + +procedure TSpreadReadDateTests.TestReadDate25; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,25); +end; + +procedure TSpreadReadDateTests.TestReadDate26; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,26); +end; + +procedure TSpreadReadDateTests.TestReadDate27; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,27); +end; + +procedure TSpreadReadDateTests.TestReadDate28; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,28); +end; + +procedure TSpreadReadDateTests.TestReadDate29; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,29); +end; + +procedure TSpreadReadDateTests.TestReadDate30; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,30); +end; + +procedure TSpreadReadDateTests.TestReadDate31; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,31); +end; + +procedure TSpreadReadDateTests.TestReadDate32; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,32); +end; + +procedure TSpreadReadDateTests.TestReadDate33; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,33); +end; + +procedure TSpreadReadDateTests.TestReadDate34; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,34); +end; + +procedure TSpreadReadDateTests.TestReadDate35; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,35); +end; + +procedure TSpreadReadDateTests.TestReadDate36; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,36); +end; + +procedure TSpreadReadDateTests.TestReadDate37; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,37); +end; + procedure TSpreadReadDateTests.TestReadDate1899_0; begin TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,0); @@ -330,6 +542,136 @@ begin TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,11); end; +procedure TSpreadReadDateTests.TestReadDate1899_12; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,12); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_13; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,13); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_14; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,14); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_15; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,15); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_16; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,16); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_17; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,17); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_18; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,18); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_19; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,19); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_20; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,20); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_21; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,21); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_22; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,22); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_23; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,23); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_24; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,24); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_25; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,25); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_26; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,26); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_27; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,27); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_28; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,28); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_29; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,29); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_30; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,30); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_31; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,31); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_32; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,32); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_33; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,33); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_34; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,34); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_35; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,35); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_36; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,36); +end; + +procedure TSpreadReadDateTests.TestReadDate1899_37; +begin + TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileBIFF8_1899,37); +end; + initialization // Register so these tests are included in a full run diff --git a/components/fpspreadsheet/tests/formattests.pas b/components/fpspreadsheet/tests/formattests.pas new file mode 100644 index 000000000..084804c8b --- /dev/null +++ b/components/fpspreadsheet/tests/formattests.pas @@ -0,0 +1,225 @@ +unit formattests; + +{$mode objfpc}{$H+} + +interface + +uses + // Not using lazarus package as the user may be working with multiple versions + // Instead, add .. to unit search path + Classes, SysUtils, fpcunit, testutils, testregistry, + fpsallformats, fpspreadsheet, xlsbiff8 {and a project requirement for lclbase for utf8 handling}, + testsutility; + +var + // Norm to test against - list of strings that should occur in spreadsheet + SollNumberStrings: array[0..6, 0..8] of string; + SollNumbers: array[0..6] of Double; + SollNumberFormats: array[0..8] of TsNumberFormat; + SollNumberDecimals: array[0..8] of word; + + SollDateTimeStrings: array[0..4, 0..9] of string; + SollDateTimes: array[0..4] of TDateTime; + SollDateTimeFormats: array[0..9] of TsNumberFormat; + SollDateTimeFormatStrings: array[0..9] of String; + + procedure InitSollFmtData; + +type + { TSpreadWriteReadFormatTests } + //Write to xls/xml file and read back + TSpreadWriteReadFormatTests = class(TTestCase) + private + protected + // Set up expected values: + procedure SetUp; override; + procedure TearDown; override; + published + // Writes out numbers & reads back. + // If previous read tests are ok, this effectively tests writing. + procedure TestWriteReadNumberFormats; + // Repeat with date/times + procedure TestWriteReadDateTimeFormats; + end; + +implementation + +const + FmtNumbersSheet = 'Numbers'; + FmtDateTimesSheet = 'Date/Times'; + +// Initialize array with variables that represent the values +// we expect to be in the test spreadsheet files. +// +// When adding tests, add values to this array +// and increase array size in variable declaration +procedure InitSollFmtData; +var + i: Integer; +begin + // Set up norm - MUST match spreadsheet cells exactly + + // Numbers + SollNumbers[0] := 0.0; + SollNumbers[1] := 1.0; + SollNumbers[2] := -1.0; + SollNumbers[3] := 1.2345E6; + SollNumbers[4] := -1.23456E6; + SollNumbers[5] := 1.23456E-6; + SollNumbers[6] := -1.23456E-6; + + SollNumberFormats[0] := nfGeneral; SollNumberDecimals[0] := 0; + SollNumberFormats[1] := nfFixed; SollNumberDecimals[1] := 0; + SollNumberFormats[2] := nfFixed; SollNumberDecimals[2] := 2; + SollNumberFormats[3] := nfFixedTh; SollNumberDecimals[3] := 0; + SollNumberFormats[4] := nfFixedTh; SollNumberDecimals[4] := 2; + SollNumberFormats[5] := nfExp; SollNumberDecimals[5] := 2; + SollNumberFormats[6] := nfSci; SollNumberDecimals[6] := 1; + SollNumberFormats[7] := nfPercentage; SollNumberDecimals[7] := 0; + SollNumberFormats[8] := nfPercentage; SollNumberDecimals[8] := 2; + + for i:=Low(SollNumbers) to High(SollNumbers) do begin + SollNumberStrings[i, 0] := FloatToStr(SollNumbers[i]); + SollNumberStrings[i, 1] := FormatFloat('0', SollNumbers[i]); + SollNumberStrings[i, 2] := FormatFloat('0.00', SollNumbers[i]); + SollNumberStrings[i, 3] := FormatFloat('#,##0', SollNumbers[i]); + SollNumberStrings[i, 4] := FormatFloat('#,##0.00', SollNumbers[i]); + SollNumberStrings[i, 5] := FormatFloat('0.00E+00', SollNumbers[i]); + SollNumberStrings[i, 6] := SciFloat(SollNumbers[i], 1); + SollNumberStrings[i, 7] := FormatFloat('0', SollNumbers[i]*100) + '%'; + SollNumberStrings[i, 8] := FormatFloat('0.00', SollNumbers[i]*100) + '%'; + end; + + // Date/time values + SollDateTimes[0] := EncodeDate(2012, 1, 12) + EncodeTime(13, 14, 15, 567); + SolLDateTimes[1] := EncodeDate(2012, 2, 29) + EncodeTime(0, 0, 0, 1); + SollDateTimes[2] := EncodeDate(2040, 12, 31) + EncodeTime(12, 0, 0, 0); + SollDateTimes[3] := 1 + EncodeTime(3,45, 0, 0); + SollDateTimes[4] := EncodeTime(12, 0, 0, 0); + + SollDateTimeFormats[0] := nfShortDateTime; SollDateTimeFormatStrings[0] := ''; + SollDateTimeFormats[1] := nfShortDate; SollDateTimeFormatStrings[1] := ''; + SollDateTimeFormats[2] := nfShortTime; SollDateTimeFormatStrings[2] := ''; + SollDateTimeFormats[3] := nfLongTime; SollDateTimeFormatStrings[3] := ''; + SollDateTimeFormats[4] := nfShortTimeAM; SollDateTimeFormatStrings[4] := ''; + SollDateTimeFormats[5] := nfLongTimeAM; SollDateTimeFormatStrings[5] := ''; + SollDateTimeFormats[6] := nfFmtDateTime; SollDateTimeFormatStrings[6] := 'dm'; + SolLDateTimeFormats[7] := nfFmtDateTime; SollDateTimeFormatStrings[7] := 'my'; + SollDateTimeFormats[8] := nfFmtDateTime; SollDateTimeFormatStrings[8] := 'ms'; + SollDateTimeFormats[9] := nfTimeInterval; SollDateTimeFormatStrings[9] := ''; + + for i:=Low(SollDateTimes) to High(SollDateTimes) do begin + SollDateTimeStrings[i, 0] := DateToStr(SollDateTimes[i]) + ' ' + FormatDateTime('t', SollDateTimes[i]); + SollDateTimeStrings[i, 1] := DateToStr(SollDateTimes[i]); + SollDateTimeStrings[i, 2] := FormatDateTime('t', SollDateTimes[i]); + SolLDateTimeStrings[i, 3] := FormatDateTime('tt', SollDateTimes[i]); + SollDateTimeStrings[i, 4] := FormatDateTime('t am/pm', SollDateTimes[i]); + SollDateTimeStrings[i, 5] := FormatDateTime('tt am/pm', SollDateTimes[i]); + SollDateTimeStrings[i, 6] := FormatDateTime('dd/mmm', SollDateTimes[i]); + SollDateTimeStrings[i, 7] := FormatDateTime('mmm/yy', SollDateTimes[i]); + SollDateTimeStrings[i, 8] := FormatDateTime('nn:ss', SollDateTimes[i]); + SollDateTimeStrings[i, 9] := TimeIntervalToString(SollDateTimes[i]); + end; +end; + +{ TSpreadWriteReadFormatTests } + +procedure TSpreadWriteReadFormatTests.SetUp; +begin + inherited SetUp; + InitSollFmtData; //just for security: make sure the variables are reset to default +end; + +procedure TSpreadWriteReadFormatTests.TearDown; +begin + inherited TearDown; +end; + +procedure TSpreadWriteReadFormatTests.TestWriteReadNumberFormats; +var + MyWorksheet: TsWorksheet; + MyWorkbook: TsWorkbook; + ActualString: String; + Row, Col: Integer; + TempFile: string; //write xls/xml to this file and read back from it +begin + TempFile:=GetTempFileName; + {// Not needed: use workbook.writetofile with overwrite=true + if fileexists(TempFile) then + DeleteFile(TempFile); + } + // Write out all test values + MyWorkbook := TsWorkbook.Create; + MyWorkSheet:= MyWorkBook.AddWorksheet(FmtNumbersSheet); + for Row := Low(SollNumbers) to High(SollNumbers) do + for Col := ord(Low(SollNumberFormats)) to ord(High(SollNumberFormats)) do begin + MyWorksheet.WriteNumber(Row, Col, SollNumbers[Row], SollNumberFormats[Col], SollNumberDecimals[Col]); + ActualString := MyWorksheet.ReadAsUTF8Text(Row, Col); + CheckEquals(SollNumberStrings[Row, Col], ActualString, 'Test unsaved string mismatch cell ' + CellNotation(MyWorksheet,Row,Col)); + end; + MyWorkBook.WriteToFile(TempFile,sfExcel8,true); + MyWorkbook.Free; + + // Open the spreadsheet, as biff8 + MyWorkbook := TsWorkbook.Create; + MyWorkbook.ReadFromFile(TempFile, sfExcel8); + MyWorksheet:=GetWorksheetByName(MyWorkBook, FmtNumbersSheet); + if MyWorksheet=nil then + fail('Error in test code. Failed to get named worksheet'); + for Row := Low(SollNumbers) to High(SollNumbers) do + for Col := Low(SollNumberFormats) to High(SollNumberFormats) do begin + ActualString := MyWorkSheet.ReadAsUTF8Text(Row,Col); + CheckEquals(SollNumberStrings[Row,Col],ActualString,'Test saved string mismatch cell '+CellNotation(MyWorkSheet,Row,Col)); + end; + + // Finalization + MyWorkbook.Free; + + DeleteFile(TempFile); +end; + +procedure TSpreadWriteReadFormatTests.TestWriteReadDateTimeFormats; +var + MyWorksheet: TsWorksheet; + MyWorkbook: TsWorkbook; + ActualString: String; + Row,Col: Integer; + TempFile: string; //write xls/xml to this file and read back from it +begin + TempFile:=GetTempFileName; + // Write out all test values + MyWorkbook := TsWorkbook.Create; + MyWorksheet := MyWorkbook.AddWorksheet(FmtDateTimesSheet); + for Row := Low(SollDateTimes) to High(SollDateTimes) do + for Col := Low(SollDateTimeFormats) to High(SollDateTimeFormats) do begin + MyWorksheet.WriteDateTime(Row, Col, SollDateTimes[Row], SollDateTimeFormats[Col], SollDateTimeFormatStrings[Col]); + ActualString := MyWorksheet.ReadAsUTF8Text(Row, Col); + CheckEquals(SollDateTimeStrings[Row, Col], ActualString, 'Test unsaved string mismatch cell ' + CellNotation(MyWorksheet,Row,Col)); + end; + MyWorkBook.WriteToFile(TempFile,sfExcel8,true); + MyWorkbook.Free; + + // Open the spreadsheet, as biff8 + MyWorkbook := TsWorkbook.Create; + MyWorkbook.ReadFromFile(TempFile, sfExcel8); + MyWorksheet := GetWorksheetByName(MyWorkbook, FmtDateTimesSheet); + if MyWorksheet = nil then + fail('Error in test code. Failed to get named worksheet'); + for Row := Low(SollDateTimes) to High(SollDateTimes) do + for Col := Low(SollDateTimeFormats) to High(SollDateTimeFormats) do begin + ActualString := myWorksheet.ReadAsUTF8Text(Row,Col); + CheckEquals(SollDateTimeStrings[Row, Col], ActualString, 'Test saved string mismatch cell '+CellNotation(MyWorksheet,Row,Col)); + end; + + // Finalization + MyWorkbook.Free; + + DeleteFile(TempFile); +end; + +initialization + RegisterTest(TSpreadWriteReadFormatTests); + InitSollFmtData; + +end. + diff --git a/components/fpspreadsheet/tests/manualtests.pas b/components/fpspreadsheet/tests/manualtests.pas index b614213c3..0e5a47724 100644 --- a/components/fpspreadsheet/tests/manualtests.pas +++ b/components/fpspreadsheet/tests/manualtests.pas @@ -26,7 +26,7 @@ uses var // Norm to test against - list of dates/times that should occur in spreadsheet - SollColors: array[0..22] of tsColor; //"Soll" is a German word in Dutch accountancy circles meaning "normative value to check against". There ;) + SollColors: array[0..22] of tsColor; //"Soll" is a German word in Dutch accountancy jargon meaning "normative value to check against". There ;) SollColorNames: array[0..22] of string; //matching names for SollColors // Initializes Soll*/normative variables. // Useful in test setup procedures to make sure the norm is correct. diff --git a/components/fpspreadsheet/tests/numberstests.pas b/components/fpspreadsheet/tests/numberstests.pas index a500c869b..00bac7627 100644 --- a/components/fpspreadsheet/tests/numberstests.pas +++ b/components/fpspreadsheet/tests/numberstests.pas @@ -22,7 +22,7 @@ uses var // Norm to test against - list of numbers/times that should occur in spreadsheet - SollNumbers: array[0..12] of double; //"Soll" is a German word in Dutch accountancy circles meaning "normative value to check against". There ;) + SollNumbers: array[0..20] of double; //"Soll" is a German word in Dutch accountancy jargon meaning "normative value to check against". There ;) // Initializes Soll*/normative variables. // Useful in test setup procedures to make sure the norm is correct. procedure InitSollNumbers; @@ -53,6 +53,14 @@ type procedure TestReadNumber9; procedure TestReadNumber10; procedure TestReadNumber11; + procedure TestReadNumber12; + procedure TestReadNumber13; + procedure TestReadNumber14; + procedure TestReadNumber15; + procedure TestReadNumber16; + procedure TestReadNumber17; + procedure TestReadNumber18; + procedure TestReadNumber19; end; { TSpreadWriteReadNumberTests } @@ -98,6 +106,14 @@ begin SollNumbers[10]:=3.14159265358979; //some parts of pi SollNumbers[11]:=59000000; //59 million SollNumbers[12]:=59000000.1; //same + a tenth + SollNumbers[13]:=0.3536; // 0.3536 formatted as percentage, no decimals + SollNumbers[14]:=0.3536; // 0.3536 formatted as percentage, 2 decimals + SollNumbers[15]:=59000000.1234; // 59 million + 0.1234 formatted with thousand separator, no decimals + SollNumbers[16]:=59000000.1234; // 59 million + 0.1234 formatted with thousand separator, 2 decimals + SollNumbers[17]:=-59000000.1234; // minus 59 million + 0.1234, formatted as "scientific" with 1 decimal + SollNumbers[18]:=-59000000.1234; // minus 59 million + 0.1234, formatted as "exp" with 2 decimals + SollNumbers[19]:=59000000.1234; // 59 million + 0.1234 formatted as currrency (EUROs), 2 decimals + SollNumbers[20]:=59000000.1234; // 59 million + 0.1234 formatted as currrency (Dollars), 2 decimals end; { TSpreadWriteReadNumberTests } @@ -252,6 +268,46 @@ begin TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,11); end; +procedure TSpreadReadNumberTests.TestReadNumber12; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,12); +end; + +procedure TSpreadReadNumberTests.TestReadNumber13; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,13); +end; + +procedure TSpreadReadNumberTests.TestReadNumber14; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,14); +end; + +procedure TSpreadReadNumberTests.TestReadNumber15; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,15); +end; + +procedure TSpreadReadNumberTests.TestReadNumber16; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,16); +end; + +procedure TSpreadReadNumberTests.TestReadNumber17; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,17); +end; + +procedure TSpreadReadNumberTests.TestReadNumber18; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,16); +end; + +procedure TSpreadReadNumberTests.TestReadNumber19; +begin + TestReadNumber(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,17); +end; + initialization // Register so these tests are included in a full run diff --git a/components/fpspreadsheet/tests/spreadtestcli.lpr b/components/fpspreadsheet/tests/spreadtestcli.lpr index 2c5561030..6ba2e93f8 100644 --- a/components/fpspreadsheet/tests/spreadtestcli.lpr +++ b/components/fpspreadsheet/tests/spreadtestcli.lpr @@ -10,7 +10,7 @@ uses testregistry, testdbwriter {used to get results into db}, datetests, manualtests, numberstests, stringtests, internaltests, - testsutility, testutils {the actual tests}; + testsutility, testutils, formattests {the actual tests}; const ShortOpts = 'ac:dhlpr:x'; diff --git a/components/fpspreadsheet/tests/spreadtestgui.lpi b/components/fpspreadsheet/tests/spreadtestgui.lpi index faead4edf..775053d77 100644 --- a/components/fpspreadsheet/tests/spreadtestgui.lpi +++ b/components/fpspreadsheet/tests/spreadtestgui.lpi @@ -1,4 +1,4 @@ - + @@ -21,9 +21,13 @@ + + + + @@ -75,7 +79,7 @@ - + @@ -111,6 +115,11 @@ + + + + + @@ -140,7 +149,7 @@ - + @@ -150,6 +159,9 @@ + + + diff --git a/components/fpspreadsheet/tests/spreadtestgui.lpr b/components/fpspreadsheet/tests/spreadtestgui.lpr index 0c5ab37c3..acf3b5dba 100644 --- a/components/fpspreadsheet/tests/spreadtestgui.lpr +++ b/components/fpspreadsheet/tests/spreadtestgui.lpr @@ -5,7 +5,7 @@ program spreadtestgui; uses Interfaces, Forms, GuiTestRunner, datetests, stringtests, - numberstests, manualtests, testsutility, internaltests; + numberstests, manualtests, testsutility, internaltests, formattests; begin Application.Initialize; diff --git a/components/fpspreadsheet/tests/stringtests.pas b/components/fpspreadsheet/tests/stringtests.pas index 1fe7def2b..e3148ac64 100644 --- a/components/fpspreadsheet/tests/stringtests.pas +++ b/components/fpspreadsheet/tests/stringtests.pas @@ -22,7 +22,7 @@ uses var // Norm to test against - list of strings that should occur in spreadsheet - SollStrings: array[0..6] of string; //"Soll" is a German word in Dutch accountancy circles meaning "normative value to check against". There ;) + SollStrings: array[0..12] of string; //"Soll" is a German word in Dutch accountancy jargon meaning "normative value to check against". There ;) // Initializes Soll*/normative variables. // Useful in test setup procedures to make sure the norm is correct. procedure InitSollStrings; @@ -48,6 +48,12 @@ type procedure TestReadString4; procedure TestReadString5; procedure TestReadString6; + procedure TestReadString7; + procedure TestReadString8; + procedure TestReadString9; + procedure TestReadString10; + procedure TestReadString11; + procedure TestReadString12; end; { TSpreadWriteReadStringTests } @@ -84,6 +90,12 @@ begin SollStrings[4]:='café au lait'; //accent aigue on the e SollStrings[5]:='водка'; //cyrillic SollStrings[6]:='wódka'; //Polish o accent aigue + SollStrings[7]:='35%'; // 0.3536 formatted as percentage, no decimals + SollStrings[8]:=FormatFloat('0.00', 35.36)+'%'; // 0.3536 formatted as percentage, 2 decimals + SollStrings[9]:=FormatFloat('#,##0', 59000000.1234); // 59 million + 0.1234 formatted with thousand separator, no decimals + SollStrings[10]:=FormatFloat('#,##0.00', 59000000.1234); // 59 million + 0.1234 formatted with thousand separator, 2 decimals + SollStrings[11]:=SciFloat(-59000000.1234, 1); // minus 59 million + 0.1234, formatted as "scientific" with 1 decimal + SollStrings[12]:=FormatFloat('0.00E+00', -59000000.1234); // minus 59 million + 0.1234, formatted as "exp" with 2 decimals end; { TSpreadWriteReadStringTests } @@ -318,6 +330,37 @@ begin TestReadString(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,6); end; +procedure TSpreadReadStringTests.TestReadString7; +begin + TestReadString(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,7); +end; + +procedure TSpreadReadStringTests.TestReadString8; +begin + TestReadString(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,8); +end; + +procedure TSpreadReadStringTests.TestReadString9; +begin + TestReadString(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,9); +end; + +procedure TSpreadReadStringTests.TestReadString10; +begin + TestReadString(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,10); +end; + +procedure TSpreadReadStringTests.TestReadString11; +begin + TestReadString(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,11); +end; + +procedure TSpreadReadStringTests.TestReadString12; +begin + TestReadString(ExtractFilePath(ParamStr(0)) + TestFileBIFF8,12); +end; + + initialization // Register so these tests are included in a full run RegisterTest(TSpreadReadStringTests); diff --git a/components/fpspreadsheet/tests/testbiff8.xls b/components/fpspreadsheet/tests/testbiff8.xls index 3c0cb9b76..5093f294b 100644 Binary files a/components/fpspreadsheet/tests/testbiff8.xls and b/components/fpspreadsheet/tests/testbiff8.xls differ diff --git a/components/fpspreadsheet/tests/testbiff8_1899.xls b/components/fpspreadsheet/tests/testbiff8_1899.xls index e39646655..73ba50eae 100644 Binary files a/components/fpspreadsheet/tests/testbiff8_1899.xls and b/components/fpspreadsheet/tests/testbiff8_1899.xls differ diff --git a/components/fpspreadsheet/xlsbiff2.pas b/components/fpspreadsheet/xlsbiff2.pas index 3d5b67e63..0b6faca0d 100755 --- a/components/fpspreadsheet/xlsbiff2.pas +++ b/components/fpspreadsheet/xlsbiff2.pas @@ -70,6 +70,7 @@ type procedure WriteRPNFormula(AStream: TStream; const ARow, ACol: Cardinal; const AFormula: TsRPNFormula; ACell: PCell); override; procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); override; procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal; const AValue: double; ACell: PCell); override; + procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); override; end; implementation @@ -407,6 +408,21 @@ begin AStream.WriteBuffer(AValue, 8); end; +{******************************************************************* +* TsSpreadBIFF2Writer.WriteDateTime () +* +* DESCRIPTION: Writes a date/time value as a text +* +* No further formatting applied. +* +*******************************************************************} +procedure TsSpreadBIFF2Writer.WriteDateTime(AStream: TStream; + const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); +begin + WriteLabel(AStream, ARow, ACol, FormatDateTime('c', AValue), ACell); +end; + + { TsSpreadBIFF2Reader } procedure TsSpreadBIFF2Reader.ReadFromStream(AStream: TStream; AData: TsWorkbook); diff --git a/components/fpspreadsheet/xlsbiff5.pas b/components/fpspreadsheet/xlsbiff5.pas index 743bf1ed8..3056885d8 100755 --- a/components/fpspreadsheet/xlsbiff5.pas +++ b/components/fpspreadsheet/xlsbiff5.pas @@ -118,6 +118,7 @@ type procedure WriteBOF(AStream: TStream; ADataType: Word); function WriteBoundsheet(AStream: TStream; ASheetName: string): Int64; //procedure WriteCodepage(AStream: TStream; AEncoding: TsEncoding); this is in xlscommon + procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); override; procedure WriteDimensions(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteEOF(AStream: TStream); procedure WriteFont(AStream: TStream; AFont: TFPCustomFont); @@ -875,6 +876,20 @@ begin AStream.WriteBuffer(AValue, 8); end; +{******************************************************************* +* TsSpreadBIFF5Writer.WriteDateTime () +* +* DESCRIPTION: Writes a date/time value as a string +* +* No further formatting of the date +* +*******************************************************************} +procedure TsSpreadBIFF5Writer.WriteDateTime(AStream: TStream; + const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); +begin + WriteLabel(AStream, ARow, ACol, FormatDateTime('c', AValue), ACell); +end; + {******************************************************************* * TsSpreadBIFF5Writer.WriteStyle () * diff --git a/components/fpspreadsheet/xlsbiff8.pas b/components/fpspreadsheet/xlsbiff8.pas index c0a51a00b..02d4ce208 100755 --- a/components/fpspreadsheet/xlsbiff8.pas +++ b/components/fpspreadsheet/xlsbiff8.pas @@ -87,10 +87,12 @@ type FXFList: TFPList; // of TXFRecordData FFormatList: TFPList; // of TFormatRecordData function DecodeRKValue(const ARK: DWORD): Double; + procedure ExtractNumberFormat(AXFIndex: WORD; + out ANumberFormat: TsNumberFormat; out ADecimals: Word; + out ANumberFormatStr: String); // Tries to find if a number cell is actually a date/datetime/time cell // and retrieve the value - function IsDate(Number: Double; ARow: WORD; - ACol: WORD; AXFIndex: WORD; var ADateTime: TDateTime): boolean; + function IsDateTime(Number: Double; ANumberFormat: TsNumberFormat; var ADateTime: TDateTime): Boolean; function ReadWideString(const AStream: TStream; const ALength: WORD): WideString; overload; function ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; overload; procedure ReadWorkbookGlobals(AStream: TStream; AData: TsWorkbook); @@ -350,6 +352,7 @@ var lBorders: TsCellBorders; lAddBackground: Boolean; lBackgroundColor: TsColor; + fmt: String; begin // The first 4 styles were already added for i := 4 to Length(FFormattingStyles) - 1 do @@ -364,13 +367,63 @@ begin // Now apply the modifications. if uffNumberFormat in FFormattingStyles[i].UsedFormattingFields then - begin case FFormattingStyles[i].NumberFormat of - nfGeneral: lFormatIndex := FORMAT_GENERAL; - nfShortDate: lFormatIndex := FORMAT_SHORT_DATE; - nfShortDateTime: lFormatIndex := FORMAT_SHORT_DATETIME; + nfFixed: + case FFormattingStyles[i].NumberDecimals of + 0: lFormatIndex := FORMAT_FIXED_0_DECIMALS; + 2: lFormatIndex := FORMAT_FIXED_2_DECIMALS; + end; + nfFixedTh: + case FFormattingStyles[i].NumberDecimals of + 0: lFormatIndex := FORMAT_FIXED_THOUSANDS_0_DECIMALS; + 2: lFormatIndex := FORMAT_FIXED_THOUSANDS_2_DECIMALS; + end; + nfExp: + lFormatIndex := FORMAT_EXP_2_DECIMALS; + nfSci: + lFormatIndex := FORMAT_SCI_1_DECIMAL; + nfPercentage: + case FFormattingStyles[i].NumberDecimals of + 0: lFormatIndex := FORMAT_PERCENT_0_DECIMALS; + 2: lFormatIndex := FORMAT_PERCENT_2_DECIMALS; + end; + { + nfCurrency: + case FFormattingStyles[i].NumberDecimals of + 0: lFormatIndex := FORMAT_CURRENCY_0_DECIMALS; + 2: lFormatIndex := FORMAT_CURRENCY_2_DECIMALS; + end; + } + nfShortDate: + lFormatIndex := FORMAT_SHORT_DATE; + nfShortTime: + lFormatIndex := FORMAT_SHORT_TIME; + nfLongTime: + lFormatIndex := FORMAT_LONG_TIME; + nfShortTimeAM: + lFormatIndex := FORMAT_SHORT_TIME_AM; + nfLongTimeAM: + lFormatIndex := FORMAT_LONG_TIME_AM; + nfShortDateTime: + lFormatIndex := FORMAT_SHORT_DATETIME; + nfFmtDateTime: + begin + fmt := lowercase(FFormattingStyles[i].NumberFormatStr); + if (fmt = 'dm') or (fmt = 'd-mmm') or (fmt = 'd mmm') or (fmt = 'd. mmm') or (fmt = 'd/mmm') then + lFormatIndex := FORMAT_DATE_DM + else + if (fmt = 'my') or (fmt = 'mmm-yy') or (fmt = 'mmm yy') or (fmt = 'mmm/yy') then + lFormatIndex := FORMAT_DATE_MY + else + if (fmt = 'ms') or (fmt = 'nn:ss') or (fmt = 'mm:ss') then + lFormatIndex := FORMAT_TIME_MS + else + if (fmt = 'msz') or (fmt = 'nn:ss.zzz') or (fmt = 'mm:ss.zzz') or (fmt = 'mm:ss.0') or (fmt = 'mm:ss.z') or (fmt = 'nn:ss.z') then + lFormatIndex := FORMAT_TIME_MSZ + end; + nfTimeInterval: + lFormatIndex := FORMAT_TIME_INTERVAL; end; - end; if uffBorder in FFormattingStyles[i].UsedFormattingFields then lBorders := FFormattingStyles[i].Border; @@ -1451,47 +1504,127 @@ begin Result:=Number; end; -function TsSpreadBIFF8Reader.IsDate(Number: Double; - ARow: WORD; ACol: WORD; AXFIndex: WORD; var ADateTime: TDateTime): boolean; -// Try to find out if a cell has a date/time and return -// TheDate if it is +procedure TsSpreadBIFF8Reader.ExtractNumberFormat(AXFIndex: WORD; + out ANumberFormat: TsNumberFormat; out ADecimals: Word; + out ANumberFormatStr: String); +const + { see ➜ 5.49 } + NOT_USED = nfGeneral; + fmts: array[1..58] of TsNumberFormat = ( + nfFixed, nfFixed, nfFixedTh, nfFixedTh, nfFixedTh, // 1..5 + nfFixedTh, nfFixedTh, nfFixedTh, nfPercentage, nfPercentage, // 6..10 + nfExp, NOT_USED, NOT_USED, nfShortDate, nfShortDate, // 11..15 + nfFmtDateTime, nfFmtDateTime, nfShortTimeAM, nfLongTimeAM, nfShortTime, // 16..20 + nfLongTime, nfShortDateTime, NOT_USED, NOT_USED, NOT_USED, // 21..25 + NOT_USED, NOT_USED, NOT_USED, NOT_USED, NOT_USED, // 26..30 + NOT_USED, NOT_USED, NOT_USED, NOT_USED, NOT_USED, // 31..35 + NOT_USED, nfFixedTh, nfFixedTh, nfFixedTh, nfFixedTh, // 36..40 + nfFixedTh, nfFixedTh, nfFixedTh, nfFixedTh, nfFmtDateTime, // 41..45 + nfTimeInterval, nfFmtDateTime, nfSci, NOT_USED, NOT_USED, // 46..50 + NOT_USED, NOT_USED, NOT_USED, NOT_USED, NOT_USED, // 51..55 + NOT_USED, NOT_USED, NOT_USED // 56..58 + ); + decs: array[1..58] of word = ( + 0, 2, 0, 2, 0, 0, 2, 2, 0, 2, // 1..10 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 11..20 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21..30 + 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, // 31..40 + 0, 0, 2, 2, 0, 3, 0, 1, 0, 0, // 41..50 #48 is "scientific", use "exponential" instead + 0, 0, 0, 0, 0, 0, 0, 0); // 51..58 var lFormatData: TFormatRecordData; lXFData: TXFRecordData; + isAMPM: Boolean; + isLongTime: Boolean; + isMilliSec: Boolean; + t,d: Boolean; begin - result := false; - // Try to figure out if the number is really a number of a date or time value - // See: http://www.gaia-gis.it/FreeXL/freexl-1.0.0a-doxy-doc/Format.html - // Unfornately Excel doesnt give us a direct way to find this, - // we need to guess by the FORMAT field - // Note FindFormatRecordForCell will not retrieve default format numbers + ANumberFormat := nfGeneral; + ANumberFormatStr := ''; + ADecimals := 0; + lFormatData := FindFormatRecordForCell(AXFIndex); {Record FORMAT, BIFF8 (5.49): Offset Size Contents 0 2 Format index used in other records } - if lFormatData=nil then - begin - // No custom format, so first test for default formats - lXFData := TXFRecordData(FXFList.Items[AXFIndex]); - if (lXFData.FormatIndex in [14..22, 27..36, 45, 46, 47, 50..58]) then - begin - ADateTime := ConvertExcelDateTimeToDateTime(Number, FDateMode); - Exit(true); + if lFormatData = nil then begin + // no custom format, so first test for default formats + lXFData := TXFRecordData (FXFList.Items[AXFIndex]); + case lXFData.FormatIndex of + FORMAT_DATE_DM: + begin ANumberFormat := nfFmtDateTime; ANumberFormatStr := 'DM'; end; + FORMAT_DATE_MY: + begin ANumberFormat := nfFmtDateTime; ANumberFormatStr := 'MY'; end; + FORMAT_TIME_MS: + begin ANumberFormat := nfFmtDateTime; ANumberFormatStr := 'MS'; end; + FORMAT_TIME_MSZ: + begin ANumberFormat := nfFmtDateTime; ANumberFormatStr := 'MSZ'; end; + else + if (lXFData.FormatIndex > 0) and (lXFData.FormatIndex <= 58) then begin + ANumberFormat := fmts[lXFData.FormatIndex]; + ADecimals := decs[lXFData.FormatIndex]; + end; end; - end + end else + // Check custom formats if they have / in format string (this can fail for + // custom text formats) + if IsPercentNumberFormat(lFormatData.FormatString, ADecimals) then + ANumberFormat := nfPercentage else - begin - // Check custom formats if they - // have / in format string (this can fail for custom text formats) - if (Pos('/', lFormatData.FormatString) > 0) then - begin - ADateTime := ConvertExcelDateTimeToDateTime(Number, FDateMode); - Exit(true); + if IsExpNumberFormat(lFormatData.Formatstring, ADecimals) then + ANumberFormat := nfExp + else + if IsThousandSepNumberFormat(lFormatData.FormatString, ADecimals) then + ANumberFormat := nfFixedTh + else + if IsFixedNumberFormat(lFormatData.FormatString, ADecimals) then + ANumberFormat := nfFixed + else begin + t := IsTimeFormat(lFormatData.FormatString, isLongTime, isAMPM, isMilliSec); + d := IsDateFormat(lFormatData.FormatString); + if d and t then + ANumberFormat := nfShortDateTime + else + if d then + ANumberFormat := nfShortDate + else + if t then begin + if isAMPM then begin + if isLongTime then + ANumberFormat := nfLongTimeAM + else + ANumberFormat := nfShortTimeAM; + end else begin + if isLongTime then + ANumberFormat := nfLongTime + else + ANumberFormat := nfShortTime; + end; end; end; - ADateTime := 0; +end; + +function TsSpreadBIFF8Reader.IsDateTime(Number: Double; + ANumberFormat: TsNumberFormat; var ADateTime: TDateTime): boolean; +// Convert the number to a date/time and return that if it is +begin + if ANumberFormat in [ + nfShortDateTime, nfFmtDateTime, nfShortDate, + nfShortTime, nfLongTime, nfShortTimeAM, nfLongTimeAM] then + begin + ADateTime := ConvertExcelDateTimeToDateTime(Number, FDateMode); + Result := true; + end else + if ANumberFormat = nfTimeInterval then begin + ADateTime := Number; + Result := true; + end else + begin + ADateTime := 0; + Result := false; + end; end; function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; @@ -1718,6 +1851,9 @@ var ARow, ACol, XF: WORD; lDateTime: TDateTime; Number: Double; + nf: TsNumberFormat; // Number format + nd: word; // decimals + nfs: String; // Number format string begin {Retrieve XF record, row and column} ReadRowColXF(AStream,ARow,ACol,XF); @@ -1729,10 +1865,11 @@ begin Number:=DecodeRKValue(RK); {Find out what cell type, set contenttype and value} - if IsDate(Number, ARow, ACol, XF, lDateTime) then - FWorksheet.WriteDateTime(ARow, ACol, lDateTime) + ExtractNumberFormat(XF, nf, nd, nfs); + if IsDateTime(Number, nf, lDateTime) then + FWorksheet.WriteDateTime(ARow, ACol, lDateTime, nf, nfs) else - FWorksheet.WriteNumber(ARow,ACol,Number); + FWorksheet.WriteNumber(ARow, ACol, Number, nf); end; procedure TsSpreadBIFF8Reader.ReadMulRKValues(const AStream: TStream); @@ -1742,6 +1879,9 @@ var Pending: integer; RK: DWORD; Number: Double; + nf: TsNumberFormat; + nd: word; + nfs: String; begin ARow:=WordLEtoN(AStream.ReadWord); fc:=WordLEtoN(AStream.ReadWord); @@ -1751,10 +1891,11 @@ begin RK:=DWordLEtoN(AStream.ReadDWord); Number:=DecodeRKValue(RK); {Find out what cell type, set contenttype and value} - if IsDate(Number, ARow, fc, XF, lDateTime) then - FWorksheet.WriteDateTime(ARow, fc, lDateTime) + ExtractNumberFormat(XF, nf, nd, nfs); + if IsDateTime(Number, nf, lDateTime) then + FWorksheet.WriteDateTime(ARow, fc, lDateTime, nf, nfs) else - FWorksheet.WriteNumber(ARow,fc,Number); + FWorksheet.WriteNumber(ARow, fc, Number, nf, nd); inc(fc); dec(Pending,(sizeof(XF)+sizeof(RK))); end; @@ -1941,6 +2082,9 @@ var ARow, ACol, XF: Word; AValue: Double; lDateTime: TDateTime; + nf: TsNumberFormat; + nd: word; + nfs: String; begin {Retrieve XF record, row and column} ReadRowColXF(AStream,ARow,ACol,XF); @@ -1949,10 +2093,11 @@ begin AStream.ReadBuffer(AValue, 8); {Find out what cell type, set contenttype and value} - if IsDate(AValue, ARow, ACol, XF, lDateTime) then - FWorksheet.WriteDateTime(ARow, ACol, lDateTime) + ExtractNumberFormat(XF, nf, nd, nfs); + if IsDateTime(AValue, nf, lDateTime) then + FWorksheet.WriteDateTime(ARow, ACol, lDateTime, nf, nfs) else - FWorksheet.WriteNumber(ARow,ACol,AValue); + FWorksheet.WriteNumber(ARow,ACol,AValue,nf,nd); end; procedure TsSpreadBIFF8Reader.ReadRichString(const AStream: TStream); diff --git a/components/fpspreadsheet/xlscommon.pas b/components/fpspreadsheet/xlscommon.pas index 51232c182..db8cdc105 100644 --- a/components/fpspreadsheet/xlscommon.pas +++ b/components/fpspreadsheet/xlscommon.pas @@ -137,10 +137,30 @@ const DATEMODE_1904_BASE=1462; //1/1/1904 in FPC TDateTime { FORMAT record constants } - // Just a subset; needed for output to date/time records - FORMAT_GENERAL = 0; //general/default format - FORMAT_SHORT_DATE = 14; //short date - FORMAT_SHORT_DATETIME = 22; //short date+time + // Subset of the built-in formats for US Excel, + // including those needed for date/time output + FORMAT_GENERAL = 0; //general/default format + FORMAT_FIXED_0_DECIMALS = 1; //fixed, 0 decimals + FORMAT_FIXED_2_DECIMALS = 2; //fixed, 2 decimals + FORMAT_FIXED_THOUSANDS_0_DECIMALS = 3; //fixed, w/ thousand separator, 0 decs + FORMAT_FIXED_THOUSANDS_2_DECIMALS = 4; //fixed, w/ thousand separator, 2 decs + FORMAT_CURRENCY_0_DECIMALS = 5; //currency (with currency symbol), 0 decs + FORMAT_CURRENCY_2_DECIMALS = 7; //currency (with currency symbol), 2 decs + FORMAT_PERCENT_0_DECIMALS = 9; //percent, 0 decimals + FORMAT_PERCENT_2_DECIMALS = 10; //percent, 2 decimals + FORMAT_EXP_2_DECIMALS = 11; //exponent, 2 decimals + FORMAT_SCI_1_DECIMAL = 48; //scientific, 1 decimal + FORMAT_SHORT_DATE = 14; //short date + FORMAT_DATE_DM = 16; //date D-MMM + FORMAT_DATE_MY = 17; //date MMM-YYYY + FORMAT_SHORT_TIME_AM = 18; //short time H:MM with AM + FORMAT_LONG_TIME_AM = 19; //long time H:MM:SS with AM + FORMAT_SHORT_TIME = 20; //short time H:MM + FORMAT_LONG_TIME = 21; //long time H:MM:SS + FORMAT_SHORT_DATETIME = 22; //short date+time + FORMAT_TIME_MS = 45; //time MM:SS + FORMAT_TIME_MSZ = 47; //time MM:SS.0 + FORMAT_TIME_INTERVAL = 46; //time [hh]:mm:ss, hh can be >24 type @@ -191,6 +211,15 @@ type constructor Create; override; end; +function IsExpNumberFormat(s: String; out Decimals: Word): Boolean; +function IsFixedNumberFormat(s: String; out Decimals: Word): Boolean; +function IsPercentNumberFormat(s: String; out Decimals: Word): Boolean; +function IsThousandSepNumberFormat(s: String; out Decimals: Word): Boolean; + +function IsDateFormat(s: String): Boolean; +function IsTimeFormat(s: String; out isLong, isAMPM, isMillisec: Boolean): Boolean; + + implementation function ConvertExcelDateTimeToDateTime( @@ -467,5 +496,201 @@ begin FDateMode := dm1900; end; + +{ Format checking procedures } + +{ This simple parsing procedure of the Excel format string checks for a fixed + float format s, i.e. s can be '0', '0.00', '000', '0,000', and returns the + number of decimals, i.e. number of zeros behind the decimal point } +function IsFixedNumberFormat(s: String; out Decimals: Word): Boolean; +var + i: Integer; + p: Integer; + decs: String; +begin + Decimals := 0; + + // Check if s is a valid format mask. + try + FormatFloat(s, 1.0); + except + on EConvertError do begin + Result := false; + exit; + end; + end; + + // If it is count the zeros - each one is a decimal. + if s = '0' then + Result := true + else begin + p := pos('.', s); // position of decimal point; + if p = 0 then begin + Result := false; + end else begin + Result := true; + for i:= p+1 to Length(s) do + if s[i] = '0' then begin + inc(Decimals) + end + else + exit; // ignore characters after the last 0 + end; + end; +end; + +{ This function checks whether the format string corresponds to a thousand + separator format like "#,##0.000' and returns the number of fixed decimals + (i.e. zeros after the decimal point) } +function IsThousandSepNumberFormat(s: String; out Decimals: Word): Boolean; +var + i, p: Integer; +begin + Decimals := 0; + + // Check if s is a valid format string + try + FormatFloat(s, 1.0); + except + on EConvertError do begin + Result := false; + exit; + end; + end; + + // If it is look for the thousand separator. If found count decimals. + Result := (Pos(',', s) > 0); + if Result then begin + p := pos('.', s); + if p > 0 then + for i := p+1 to Length(s) do + if s[i] = '0' then + inc(Decimals) + else + exit; // ignore format characters after the last 0 + end; +end; + + +{ This function checks whether the format string corresponds to percent + formatting and determines the number of decimals } +function IsPercentNumberFormat(s: String; out Decimals: Word): Boolean; +var + i, p: Integer; +begin + Decimals := 0; + // The signature of the percent format is a percent sign at the end of the + // format string. + Result := (s <> '') and (s[Length(s)] = '%'); + if Result then begin + // Check for a valid format string + Delete(s, Length(s), 1); + try + FormatDateTime(s, 1.0); + except + on EConvertError do begin + Result := false; + exit; + end; + end; + // Count decimals + p := pos('.', s); + if p > 0 then + for i := p+1 to Length(s)-1 do + if s[i] = '0' then + inc(Decimals) + else + exit; // ignore characters after last 0 + end; +end; + +{ This function checks whether the format string corresponds to exponential + formatting and determines the number decimals } +function IsExpNumberFormat(s: String; out Decimals: Word): Boolean; +var + i, p, pe: Integer; +begin + Decimals := 0; + + // Check for a valid format string + try + FormatDateTime(s, 1.0); + except + on EConvertError do begin + Result := false; + exit; + end; + end; + + // Count decimals + pe := pos('e', lowercase(s)); + result := pe > 0; + if Result then begin + p := pos('.', s); + if (p > 0) then begin + if p < pe then + for i:=1 to pe-1 do + if s[i] = '0' then + inc(Decimals) + else + exit; // ignore characters after last 0 + end; + end; +end; + +{ IsDateFormat checks if the format string s corresponds to a date format } +function IsDateFormat(s: String): Boolean; +begin + // Day, month, year are separated by a slash + Result := (pos('/', s) > 0); + if Result then + // Check validity of format string + try + FormatDateTime(s, now); + except on EConvertError do + Result := false; + end; +end; + +{ IsTimeFormat checks if the format string s is a time format. isLong is + true if the string contains hours, minutes and seconds (two colons). + isAMPM is true if the string contains "AM/PM", "A/P" or "AMPM". + isMilliSec is true if the string ends with a "z". } +function IsTimeFormat(s: String; out isLong, isAMPM, isMillisec: Boolean): Boolean; +var + p, i, count: Integer; +begin + // Time parts are separated by a colon + p := pos(':', s); + isLong := false; + isAMPM := false; + result := p > 0; + + if Result then begin + count := 1; + s := Uppercase(s); + + // If there are is a second colon s is a "long" time format + for i:=p+1 to Length(s) do + if s[i] = ':' then begin + isLong := true; + break; + end; + + // Seek for "AM/PM" etc to detect that specific format + isAMPM := (pos('AM/PM', s) > 0) or (pos('A/P', s) > 0) or (pos('AMPM', s) > 0); + + // Look for the "milliseconds" character z + isMilliSec := (s[Length(s)] = 'Z'); + + // Check validity of format string + try + FormatDateTime(s, now); + except on EConvertError do + Result := false; + end; + end; +end; + end. diff --git a/components/fpspreadsheet/xlsxooxml.pas b/components/fpspreadsheet/xlsxooxml.pas index d11053ce3..2e1e00a06 100755 --- a/components/fpspreadsheet/xlsxooxml.pas +++ b/components/fpspreadsheet/xlsxooxml.pas @@ -75,6 +75,7 @@ type //todo: add WriteDate procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); override; procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal; const AValue: double; ACell: PCell); override; + procedure WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); override; end; implementation @@ -517,6 +518,16 @@ begin Format(' %s', [CellPosText, CellValueText]) + LineEnding; end; +{ + Writes the date/time as a text to the sheet. + No further formatting applied. +} +procedure TsSpreadOOXMLWriter.WriteDateTime(AStream: TStream; + const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); +begin + WriteLabel(AStream, ARow, ACol, FormatDateTime('c', AValue), ACell); +end; + { Registers this reader / writer on fpSpreadsheet }