diff --git a/components/fpspreadsheet/source/common/fpsnumformat.pas b/components/fpspreadsheet/source/common/fpsnumformat.pas
index 3222168f1..6d99ddf52 100644
--- a/components/fpspreadsheet/source/common/fpsnumformat.pas
+++ b/components/fpspreadsheet/source/common/fpsnumformat.pas
@@ -1080,9 +1080,9 @@ begin
nftMilliseconds:
case section.Elements[el].IntValue of
- 1: Result := Result + IntToStr(ms div 100);
- 2: Result := Result + Format('%02d', [ms div 10]);
- 3: Result := Result + Format('%03d', [ms]);
+ 1: Result := Result + IntToStr(round(ms/100));
+ 2: Result := Result + Format('%.2d', [round(ms/10)]);
+ 3: Result := Result + Format('%.3d', [ms]);
end;
nftAMPM:
@@ -2902,6 +2902,10 @@ begin
section^.Kind := section^.Kind + [nfkTime];
if section^.Elements[el].IntValue < 0 then
section^.Kind := section^.Kind + [nfkTimeInterval];
+ if section^.Elements[el].Token = nftMilliseconds then
+ section^.Decimals := section^.Elements[el].IntValue
+ else
+ section^.Decimals := 0;
end;
nftMonthMinute:
isMonthMinute := true;
@@ -3690,12 +3694,25 @@ begin
end;
'.':
begin
- token := NextToken;
- if token in ['z', '0'] then begin
- AddElement(nftDecSep, FToken);
- FToken := NextToken;
+ {
+ AddElement(nftDecSep, FToken);
+ FToken := NextToken;
+ if FToken in ['z', 'Z', '0'] then
+ begin
ScanAndCount(FToken, n);
AddElement(nftMilliseconds, n);
+ end;
+ }
+
+ token := NextToken;
+ if token in ['z', 'Z', '0'] then begin
+ AddElement(nftDecSep, FToken);
+ FToken := NextToken;
+ if FToken in ['z', 'Z', '0'] then
+ ScanAndCount(FToken, n)
+ else
+ n := 0;
+ AddElement(nftMilliseconds, n+1);
end else begin
AddElement(nftDateTimeSep, FToken);
FToken := token;
diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas
index e53806466..0c4740e95 100644
--- a/components/fpspreadsheet/source/common/fpsopendocument.pas
+++ b/components/fpspreadsheet/source/common/fpsopendocument.pas
@@ -689,6 +689,7 @@ var
ns: Integer;
clr: TsColor;
mask: String;
+ s: String;
timeIntervalStr: String;
styleMapStr: String;
int,num,denom: Integer;
@@ -954,9 +955,14 @@ begin
nftSecond:
begin
+ s := '';
+ if (el < nel - 2) and (Elements[el+1].Token = nftDecSep) and
+ (Elements[el+2].Token = nftMilliseconds)
+ then
+ s := Format('number:decimal-places="%d"', [Elements[el+2].IntValue]);
case abs(Elements[el].IntValue) of
1: Result := Result + '';
- 2: Result := Result + '';
+ 2: Result := Result + '';
end;
if Elements[el].IntValue < 0 then
timeIntervalStr := ' number:truncate-on-overflow="false"';
@@ -7898,6 +7904,7 @@ var
fmt: TsCellFormat;
numFmtParams: TsNumFormatParams;
h,m,s,ms: Word;
+ mask: String;
begin
Unused(ARow, ACol);
@@ -7949,7 +7956,22 @@ begin
isTimeOnly := Assigned(numFmtParams) and (numFmtParams.Sections[0].Kind * [nfkDate, nfkTime] = [nfkTime])
else
isTimeOnly := false;
+ // ODS wants the date/time in the ISO format.
strValue := FormatDateTime(DATE_FMT[isTimeOnly], AValue);
+ // Add milliseconds; they must be appended as decimals to the seconds.
+ if Assigned(numFmtParams) and (nfkTime in numFmtParams.Sections[0].Kind) and
+ (numFmtParams.Sections[0].Decimals > 0) then
+ begin
+ strValue[Length(strValue)] := '.'; // replace trailing 'S' by '.'
+ // add value of milliseconds, rounded to required decimal places
+ DecodeTime(AValue, h,m,s,ms);
+ case numFmtParams.Sections[0].Decimals of
+ 1: strValue := strValue + FormatFloat('0', round(ms/100));
+ 2: strValue := strValue + FormatFloat('00', round(ms/10));
+ 3: strValue := strValue + FormatFloat('000', ms);
+ end;
+ strValue := strValue + 'S';
+ end;
displayStr := FWorksheet.ReadAsText(ACell);
AppendToStream(AStream, Format(
'' +
diff --git a/components/fpspreadsheet/tests/datetests.pas b/components/fpspreadsheet/tests/datetests.pas
index 5c0794d41..246e0fcb3 100644
--- a/components/fpspreadsheet/tests/datetests.pas
+++ b/components/fpspreadsheet/tests/datetests.pas
@@ -289,12 +289,20 @@ type
// Reads dates, date/time and time values from spreadsheet and checks against list
// One cell per test so some tests can fail and those further below may still work
procedure TestWriteReadDates(AFormat: TsSpreadsheetFormat);
+ procedure TestWriteReadMilliseconds(AFormat: TsSpreadsheetFormat);
+
published
procedure TestWriteReadDates_BIFF2;
procedure TestWriteReadDates_BIFF5;
procedure TestWriteReadDates_BIFF8;
procedure TestWriteReadDates_ODS;
procedure TestWriteReadDates_OOXML;
+
+ procedure TestWriteReadMilliseconds_BIFF2;
+ procedure TestWriteReadMilliseconds_BIFF5;
+ procedure TestWriteReadMilliseconds_BIFF8;
+ procedure TestWriteReadMilliseconds_ODS;
+ procedure TestWriteReadMilliseconds_OOXML;
end;
@@ -407,7 +415,7 @@ begin
MyWorkbook.Free;
end;
- // Open the spreadsheet, as biff8
+ // Open the spreadsheet
MyWorkbook := TsWorkbook.Create;
try
MyWorkbook.ReadFromFile(TempFile, AFormat);
@@ -432,6 +440,106 @@ begin
end;
end;
+procedure TSpreadWriteReadDateTests.TestWriteReadMilliseconds(
+ AFormat: TsSpreadsheetFormat);
+type
+ TMillisecondTestParam = record
+ h, m, s, ms: word;
+ str1, str2, str3: String;
+ end;
+const
+ SOLL_TIMES: array[0..2] of TMillisecondTestParam = (
+ (h:12; m: 0; s: 0; ms: 0; str1:'12:00:00.0'; str2:'12:00:00.00'; str3:'12:00:00.000'),
+ (h:23; m:59; s:59; ms: 10; str1:'23:59:59.0'; str2:'23:59:59.01'; str3:'23:59:59.010'),
+ (h:23; m:59; s:59; ms:191; str1:'23:59:59.2'; str2:'23:59:59.19'; str3:'23:59:59.191')
+ );
+ FORMAT_STRINGS: array[1..3] of string = (
+ 'hh:nn:ss.z', 'hh:nn:ss.zz', 'hh:nn:ss.zzz');
+ EPS = 0.0005*60*60*24; // 0.5 ms
+var
+ MyWorkbook: TsWorkbook;
+ MyWorksheet: TsWorksheet;
+ actualDateTime: TDateTime;
+ actualStr: String;
+ r, c: Cardinal;
+ h, m, s, ms: Word;
+ t: TTime;
+ tempFile: String;
+begin
+ tempFile := NewTempFile;
+
+ // Write out all test values
+ MyWorkbook := TsWorkbook.Create;
+ try
+ MyWorkbook.FormatSettings.DecimalSeparator := '.';
+ MyWorkSheet := MyWorkBook.AddWorksheet(DatesSheet);
+ for r := Low(SOLL_TIMES) to High(SOLL_TIMES) do
+ begin
+ with SOLL_TIMES[r] do t := EncodeTime(h, m, s, ms);
+ for c := Low(FORMAT_STRINGS) to High(FORMAT_STRINGS) do
+ begin
+ MyWorkSheet.WriteDateTime(r, c, t, FORMAT_STRINGS[c]);
+
+ // Some checks inside worksheet itself, before writing
+ if not(MyWorkSheet.ReadAsDateTime(r, c, actualDateTime)) then
+ Fail('Failed writing date time for cell '+CellNotation(MyWorkSheet, r, c));
+ CheckEquals(t, actualDateTime, EPS,
+ 'Test date/time value mismatch cell '+CellNotation(MyWorksheet, r, c));
+ actualStr := MyWorksheet.ReadAsText(r, c);
+ case c of
+ 1: CheckEquals(SOLL_TIMES[r].str1, actualstr,
+ 'Cell string mismatch, cell '+CellNotation(Myworksheet, r, c));
+ 2: CheckEquals(SOLL_TIMES[r].str2, actualstr,
+ 'Cell string mismatch, cell '+CellNotation(Myworksheet, r, c));
+ 3: CheckEquals(SOLL_TIMES[r].str3, actualstr,
+ 'Cell string mismatch, cell '+CellNotation(Myworksheet, r, c));
+ end;
+ end;
+ end;
+ MyWorkBook.WriteToFile(TempFile, AFormat, true);
+ finally
+ MyWorkbook.Free;
+ end;
+
+ // Open the spreadsheet
+ MyWorkbook := TsWorkbook.Create;
+ try
+ MyWorkbook.FormatSettings.DecimalSeparator := '.';
+ MyWorkbook.ReadFromFile(TempFile, AFormat);
+ if AFormat = sfExcel2 then
+ MyWorksheet := MyWorkbook.GetFirstWorksheet
+ else
+ MyWorksheet := GetWorksheetByName(MyWorkBook,DatesSheet);
+ if MyWorksheet=nil then
+ fail('Error in test code. Failed to get named worksheet');
+
+ // Read test data from A column & compare if written=original
+ for r := Low(SOLL_TIMES) to High(SOLL_TIMES) do
+ begin
+ with SOLL_TIMES[r] do t := EncodeTime(h, m, s, ms);
+ for c := Low(FORMAT_STRINGS) to High(FORMAT_STRINGS) do begin
+ if not(MyWorkSheet.ReadAsDateTime(r, c, actualDateTime)) then
+ Fail('Could not read date time for cell '+CellNotation(MyWorkSheet, r, c));
+ CheckEquals(r, actualDateTime, EPS,
+ 'Test date/time value mismatch cell '+CellNotation(MyWorkSheet, r, c));
+ actualStr := MyWorksheet.ReadAsText(r, c);
+ case c of
+ 1: CheckEquals(SOLL_TIMES[r].str1, actualstr,
+ 'Cell string mismatch, cell '+CellNotation(Myworksheet, r, c));
+ 2: CheckEquals(SOLL_TIMES[r].str2, actualstr,
+ 'Cell string mismatch, cell '+CellNotation(Myworksheet, r, c));
+ 3: CheckEquals(SOLL_TIMES[r].str3, actualstr,
+ 'Cell string mismatch, cell '+CellNotation(Myworksheet, r, c));
+ end;
+ end;
+ end;
+ finally
+ MyWorkbook.Free;
+ DeleteFile(TempFile);
+ end;
+
+end;
+
procedure TSpreadWriteReadDateTests.TestWriteReadDates_BIFF2;
begin
TestWriteReadDates(sfExcel2);
@@ -1680,6 +1788,33 @@ begin
TestReadDate(ExtractFilePath(ParamStr(0)) + TestFileOOXML_1899,37);
end;
+//------------------------------------------------------------------------------
+
+procedure TSpreadWriteReadDateTests.TestWriteReadMilliseconds_BIFF2;
+begin
+ TestWriteReadMilliseconds(sfExcel2);
+end;
+
+procedure TSpreadWriteReadDateTests.TestWriteReadMilliseconds_BIFF5;
+begin
+ TestWriteReadMilliseconds(sfExcel5);
+end;
+
+procedure TSpreadWriteReadDateTests.TestWriteReadMilliseconds_BIFF8;
+begin
+ TestWriteReadMilliseconds(sfExcel8);
+end;
+
+procedure TSpreadWriteReadDateTests.TestWriteReadMilliseconds_ODS;
+begin
+ TestWriteReadMilliseconds(sfOpenDocument);
+end;
+
+procedure TSpreadWriteReadDateTests.TestWriteReadMilliseconds_OOXML;
+begin
+ TestWriteReadMilliseconds(sfOOXML);
+end;
+
initialization
// Register so these tests are included in a full run