diff --git a/components/fpspreadsheet/fpsnumformatparser.pas b/components/fpspreadsheet/fpsnumformatparser.pas index 778b382d8..f2b15c74d 100644 --- a/components/fpspreadsheet/fpsnumformatparser.pas +++ b/components/fpspreadsheet/fpsnumformatparser.pas @@ -147,6 +147,7 @@ type procedure ClearAll; function GetDateTimeCode(ASection: Integer): String; function IsDateTimeFormat: Boolean; + function IsTimeFormat: Boolean; procedure LimitDecimals; procedure Localize; @@ -1184,6 +1185,26 @@ begin Result := false; end; +{ Returns true if the format elements contain only time, no date tokens. } +function TsNumFormatParser.IsTimeFormat: Boolean; +var + section: Integer; + elem: Integer; +begin + Result := false; + for section := 0 to High(FSections) do + for elem := 0 to High(FSections[section].Elements) do + if FSections[section].Elements[elem].Token in [nftHour, nftMinute, nftSecond] + then begin + Result := true; + end else + if FSections[section].Elements[elem].Token in [nftYear, nftMonth, nftDay, nftExpChar, nftCurrSymbol] + then begin + Result := false; + exit; + end; +end; + function TsNumFormatParser.IsTokenAt(AToken: TsNumFormatToken; ASection, AIndex: Integer): Boolean; begin diff --git a/components/fpspreadsheet/fpsopendocument.pas b/components/fpspreadsheet/fpsopendocument.pas index 3dd599760..97bb0316a 100755 --- a/components/fpspreadsheet/fpsopendocument.pas +++ b/components/fpspreadsheet/fpsopendocument.pas @@ -58,6 +58,9 @@ type { TsSpreadOpenDocNumFormatParser } TsSpreadOpenDocNumFormatParser = class(TsNumFormatParser) + private + function BuildDateTimeXMLAsString(ASection: Integer; AIndent: String; + out AIsTimeOnly: Boolean): String; protected function BuildXMLAsStringFromSection(ASection: Integer; AIndent,AFormatName: String): String; @@ -312,6 +315,104 @@ end; { TsSpreadOpenDocNumFormatParser } +function TsSpreadOpenDocNumFormatParser.BuildDateTimeXMLAsString(ASection: Integer; + AIndent: String; out AIsTimeOnly: boolean): String; +var + el: Integer; + s: String; + prevToken: TsNumFormatToken; +begin + Result := ''; + AIsTimeOnly := true; + with FSections[ASection] do begin + el := 0; + while el < Length(Elements) do begin + case Elements[el].Token of + nftYear: + begin + prevToken := Elements[el].Token; + AIsTimeOnly := false; + s := IfThen(Elements[el].IntValue > 2, 'number:style="long" ', ''); + Result := Result + AIndent + + ' ' + LineEnding; + end; + + nftMonth: + begin + prevToken := Elements[el].Token; + AIsTimeOnly := false; + case Elements[el].IntValue of + 1: s := ''; + 2: s := 'number:style="long"'; + 3: s := 'number:textual="true"'; + 4: s := 'number:style="long" number:textual="true"'; + end; + Result := result + AIndent + + ' ' + LineEnding; + end; + + nftDay: + begin + prevToken := Elements[el].Token; + AIsTimeOnly := false; + case Elements[el].IntValue of + 1: s := 'day '; + 2: s := 'day number:style="long"'; + 3: s := 'day number:textual="true"'; + 4: s := 'day number:style="long" number:textual="true"'; + 5: s := 'day-of-week '; + 6: s := 'day-of-week number:style="long"'; + end; + Result := Result + AIndent + + ' ' + LineEnding; + end; + + nftHour, nftMinute, nftSecond: + begin + prevToken := Elements[el].Token; + case Elements[el].Token of + nftHour : s := 'hours '; + nftMinute: s := 'minutes '; + nftSecond: s := 'seconds '; + end; + s := s + IfThen(abs(Elements[el].IntValue) = 1, '', 'number:style="long" '); + if Elements[el].IntValue < 0 then + s := s + 'number:truncate-on-overflow="false" '; + Result := Result + AIndent + + ' ' + LineEnding; + end; + + nftMilliseconds: + begin + // ??? + end; + + nftDateTimeSep, nftText, nftEscaped, nftSpace: + begin + if Elements[el].TextValue = ' ' then + s := '' + else begin + s := Elements[el].TextValue; + if (s = '/') then begin + if prevToken in [nftYear, nftMonth, nftDay] then + s := FWorkbook.FormatSettings.DateSeparator + else + s := FWorkbook.FormatSettings.TimeSeparator; + end; + end; + Result := Result + AIndent + + ' ' + s + '' + LineEnding; + end; + + nftAMPM: + Result := Result + AIndent + + ' ' + LineEnding; + end; + inc(el); + end; + end; +end; + function TsSpreadOpenDocNumFormatParser.BuildXMLAsString(AIndent, AFormatName: String): String; var @@ -340,6 +441,7 @@ var clr: TsColorvalue; el: Integer; s: String; + isTimeOnly: Boolean; begin Result := ''; @@ -528,6 +630,23 @@ begin end; // while Result := Result + sStyleMap + AIndent + '' + LineEnding; end; + + // date/time + nftYear, nftMonth, nftDay, nftHour, nftMinute, nftSecond: + begin + s := BuildDateTimeXMLAsString(ASection, AIndent, isTimeOnly); + if isTimeOnly then + Result := Result + AIndent + + '' + LineEnding + + s + AIndent + + '' + LineEnding + else + Result := Result + AIndent + + '' + LineEnding + + s + AIndent + + '' + LineEnding; + exit; + end; end; inc(el); end; @@ -3035,20 +3154,36 @@ end; *******************************************************************} procedure TsSpreadOpenDocWriter.WriteDateTime(AStream: TStream; const ARow, ACol: Cardinal; const AValue: TDateTime; ACell: PCell); +const + FMT: array[boolean] of string = (ISO8601FormatExtended, ISO8601FormatTimeOnly); + DT: array[boolean] of string = ('date', 'time'); + // Index "boolean" is to be understood as "isTimeOnly" var - lStyle: string = ''; + lStyle: string; + strValue: String; + displayStr: String; lIndex: Integer; + isTimeOnly: Boolean; begin if ACell^.UsedFormattingFields <> [] then begin lIndex := FindFormattingInList(ACell); - lStyle := ' table:style-name="ce' + IntToStr(lIndex) + '" '; + lStyle := 'table:style-name="ce' + IntToStr(lIndex) + '" '; end else lStyle := ''; // The row should already be the correct one - FCellContent := - ' ' + LineEnding + - ' ' + LineEnding; + + // We have to distinguish between time-only values and values that contain date parts. + isTimeOnly := IsTimeFormat(ACell^.NumberFormat) or IsTimeFormat(ACell^.NumberFormatStr); + strValue := FormatDateTime(FMT[isTimeOnly], AValue); + displayStr := FormatDateTime(ACell^.NumberFormatStr, AValue); + + FCellContent := Format( + ' ' + LineEnding + + ' %s ' + LineEnding + + ' ' + LineEnding, [ + DT[isTimeOnly], DT[isTimeOnly], strValue, lStyle, displayStr + ]); end; { diff --git a/components/fpspreadsheet/fpsutils.pas b/components/fpspreadsheet/fpsutils.pas index c17a4c592..d47f8c515 100644 --- a/components/fpspreadsheet/fpsutils.pas +++ b/components/fpspreadsheet/fpsutils.pas @@ -29,6 +29,8 @@ const ISO8601Format='yyyymmdd"T"hhmmss'; // Extended ISO 8601 date/time format, used in e.g. ODF/opendocument ISO8601FormatExtended='yyyy"-"mm"-"dd"T"hh":"mm":"ss'; + // ISO 8601 time-only format, used in ODF/opendocument + ISO8601FormatTimeOnly='"PT"hh"H"nn"M"ss"S"'; // Endianess helper functions function WordToLE(AValue: Word): Word; @@ -71,6 +73,8 @@ function IfThen(ACondition: Boolean; AValue1,AValue2: TsNumberFormat): TsNumberF function IsCurrencyFormat(AFormat: TsNumberFormat): Boolean; function IsDateTimeFormat(AFormat: TsNumberFormat): Boolean; overload; function IsDateTimeFormat(AFormatStr: String): Boolean; overload; +function IsTimeFormat(AFormat: TsNumberFormat): Boolean; overload; +function IsTimeFormat(AFormatStr: String): Boolean; overload; function BuildCurrencyFormatString(ADialect: TsNumFormatDialect; ANumberFormat: TsNumberFormat; const AFormatSettings: TFormatSettings; @@ -579,6 +583,24 @@ begin end; end; +function IsTimeFormat(AFormat: TsNumberFormat): boolean; +begin + Result := AFormat in [nfShortTime, nfLongTime, nfShortTimeAM, nfLongTimeAM, + nfTimeInterval]; +end; + +function IsTimeFormat(AFormatStr: String): Boolean; +var + parser: TsNumFormatParser; +begin + parser := TsNumFormatParser.Create(nil, AFormatStr); + try + Result := parser.IsTimeFormat; + finally + parser.Free; + end; +end; + { Builds a date/time format string from the numberformat code. If the format code is nfFmtDateTime the given AFormatString is used. AFormatString can use the abbreviations "dm" (for "d/mmm"), "my" (for "mmm/yy"), "ms" (for "mm:ss")