In Delphi is there a function to convert XML date and time to TDateTime

23,325

Solution 1

Delphi has a XSBuiltIns unit (since Delphi 6) that contains data types that can help you convert some XML data types:

(there are more, like TXSDecimal, you get the idea)

All of these contain at least these two methods:

You can use it like this:

with TXSDateTime.Create() do
  try
    AsDateTime := ClientDataSetParam.AsDateTime; // convert from TDateTime
    Attribute.DateTimeValue := NativeToXS; // convert to WideString
  finally
    Free;
  end;

with TXSDateTime.Create() do
  try
    XSToNative(XmlAttribute.DateTimeValue); // convert from WideString
    CurrentField.AsDateTime := AsDateTime; // convert to TDateTime
  finally
    Free;
  end;

That should get you going.

--jeroen

Solution 2

This has already been answered ok, but I'll add here somethings I ended up doing in a similar case that I had. From the XSBuiltIns unit I found the method

function XMLTimeToDateTime(const XMLDateTime: InvString; AsUTCTime: Boolean = False): TDateTime;

which seemed to be what I wanted. What I wanted was to be able parse all the different XML time strings defined here: http://www.w3schools.com/schema/schema_dtypes_date.asp

This includes strings with just Date, just Time, or both Date and Time, and all these with the options of specified time zone or UTC time or local for the source string, and return value as local time. Also, when given a time only, I wanted it to always be within the "day zero", i.e after the operation the whole part of the returned TDateTime (cast to real number) to be zero.

Finally, I wanted the function to return DateTime.MinValue on erroneous input (mainly when given an empty string).

I'm not certain whether I was using the function differently than it is specified, but at least it unfortunately failed in several places for me. I ended up making my own function around this one, which covered all the cases I encountered and now I'm ok going forward. It can be argued that maybe I would've been better off writing the entire parsing myself since it couldn't have been much more complex than the working-around-of-problems that I ended up doing, but at least for the time being, I'm going with what I have and decided to post it here also in case someone else finds any of this useful.

Main problem points (I may already forget some):

  • Empty string results in DateTime corresponding to a date in Year 1, while Delphi's MinDateTime is in Year 100.
  • Strings with only Date are always considered UTC regardless of the presence or absence of 'Z' or explicit time zone definitions.
  • Strings with only Time are erroneously identified as Date strings and wrongly parsed.
  • Time zone modifiers are applied ONLY if explicitly defined, otherwise all are assumed to be UTC, even when there is no 'Z'.
  • Fractional seconds are not supported, but rather milliseconds are always converted to 0.
  • Since only-Time strings are not supported, I had to add a dummy date to them, then ensure it is the current date (to cover DST issues when converting to/from UTC, which in turn had to be done due to the erroneous UTC considerations) and then in the end again subtract it from the result, and finally in these cases ensure the day-zero requirement for only-Time strings.

The end result is a function of about 100 lines (including comments etc.), which utilizes a fair amount of helper functions (which should be pretty self-explanatory and which are not the topic of this message :) ). I stripped the relevant code bits to a separate file and the unit tests I used to test this to another one, I'm including both below. Feel free to utilize and comment as necessary. Note that the form and its related using's etc. are just what Delphi put in the demo project I dropped this in, they are in no way needed.

unit Main;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, XSBuiltIns, Math, DateUtils;

const
  EPSILON = 10e-9;

type
  TForm1 = class(TForm)
  private
    { Private declarations }
  public
    { Public declarations }
  end;

{Returns whether the given variable represents negative infinity.}
function IsNegInf(AValue : extended) : boolean;

{Returns whether the given variable represents positive infinity.}
function IsPosInf(AValue : extended) : boolean;

{Checks the less than relation of the given real numbers (R1 < R2), up to
precision EPSILON.}
function RealLessThan(R1, R2 : double) : boolean;

{Checks the greater than or equal to relation of the given real numbers (R1 >= R2),
 up to precision EPSILON.}
function RealGreaterThanOrEqualTo(R1, R2 : double) : boolean;

{Checks the less than or equal to relation of the given real numbers (R1 <= R2),
up to precision EPSILON.}
function RealLessThanOrEqualTo(R1, R2 : double) : boolean;

{Return the floor of R, up to precision EPSILON. If Frac(R) < EPSILON, return R.}
function RealFloor(R : extended) : extended;

{Return the floor of R as integer, up to precision EPSILON. If Frac(R) < EPSILON, return R.}
function RealFloorInt(R : extended) : integer;

{Round the value X (properly) to an integer.}
function RoundProper(X : extended) : integer; overload;

function UtcTimeToLocalTime(AUtcTime: TDateTime): TDateTime;
function LocalTimeToUtcTime(ALocalTime: TDateTime): TDateTime;

function CountOccurrences(const SubText: string; const Text: string): Integer;
  // Returns a count of the number of occurences of SubText in Text

function XMLTimeStamp2DateTime(TimeStamp : String): TDateTime;
  // Parses an XML time stamp string to a TDateTime. All returned times are in
  // local time. If time stamp string contains no time stamp definition (either
  // explicit time zone info or UTC flag), the time is assumed to be in local time.
  // Otherwise the time is parsed as the time zone indicated, and converted to local.
  // If no time section is contained in the stamp, the time is assumed to be
  // 0:00:00 in the time zone specified (or local time if no specification set).
  // If time string is not valid MinDateTime is returned.

var
  Form1: TForm1;

implementation

{$R *.dfm}

function XMLTimeStamp2DateTime(TimeStamp : String): TDateTime;
var
  HasDateAndTimePart, HasUTCForce, HasExplicitTimeZone, HasDatePart, HasFractionalSeconds: Boolean;
  PlusCount, MinusCount, HourOffset, MinuteOffset, FractionIndex, I: Integer;
  TimeOffset: TDateTime;
  TimeZoneString, TimeZoneDelimiter: string;
  Year, Month, Day, MilliSeconds: Word;
  YearS, MonthS, DayS, FracSecS: string;
  CurrentDate, MSecsFromFractions: TDateTime;
  DotSeparatedDecimals: TFormatSettings;
begin
  TimeOffset := 0; TimeZoneString := ''; TimeZoneDelimiter := '+';
  FractionIndex := Pos('.', TimeStamp);
  {$REGION 'Get the fractional seconds as milliseconds'}
  HasFractionalSeconds := FractionIndex > 0;
  FracSecS := '0.';
  if HasFractionalSeconds then
  begin
    for I := FractionIndex + 1 to Length(TimeStamp) do
    begin
      if CharInSet(TimeStamp[I], ['0'..'9']) then FracSecS := FracSecS + TimeStamp[I]
      else Break;
    end;
  end else FracSecS := FracSecS + '0';
  DotSeparatedDecimals.Create;
  DotSeparatedDecimals.DecimalSeparator := '.';
  DotSeparatedDecimals.ThousandSeparator := #0;
  MilliSeconds := RoundProper(StrToFloatDef(FracSecS, 0, DotSeparatedDecimals) * 1000);
  MSecsFromFractions := EncodeTime(0, 0, 0, MilliSeconds);
  {$ENDREGION}
  MinusCount := CountOccurrences('-', TimeStamp);
  HasDatePart := (MinusCount > 1) or (TimeStamp = '');
  PlusCount := CountOccurrences('+', TimeStamp);
  HasExplicitTimeZone := PlusCount > 0;
  if not HasExplicitTimeZone then
  begin
    HasExplicitTimeZone := Odd(MinusCount); // 1 or 3 minuses => explicit time zone
    TimeZoneDelimiter := '-';
  end;
  if HasExplicitTimeZone then
  begin
    TimeZoneString := Copy(TimeStamp, LastDelimiter(TimeZoneDelimiter, TimeStamp) + 1, Length(TimeStamp));
    // Now TimeZoneString should be of format xx:xx where x's are numbers!
    if (Length(TimeZoneString) = 5) and (TimeZoneString[3] = ':') then
    begin
      HourOffset := StrToIntDef(Copy(TimeZoneString, 1, 2), 0);
      MinuteOffset := StrToIntDef(Copy(TimeZoneString, 3, 2), 0);
      TimeOffset := EncodeTime(HourOffset, MinuteOffset, 0, 0);
      if TimeZoneDelimiter = '-' then TimeOffset := -TimeOffset;
    end;
  end;
  CurrentDate := Now;
  Year := 0; Month := 0; Day := 0;
  DecodeDate(CurrentDate, Year, Month, Day);
  if not HasDatePart then
  begin
    // Since XMLTimeToDateTime doesn't cope with strings without date part, add
    // a dummy one on current date if it doesn't exist - we can't use day zero
    // since then the daylight saving time calculation in the LocalTimeToUtcTime
    // fixup being possibly done later will go wrong, if local time is in DST
    // and day zero is not. So we have to use current day here, then remove it
    // from the final result once we're done otherwise.
    YearS := IntToStr(Year);
    MonthS := IntToStr(Month);
    DayS := IntToStr(Day);
    while Length(YearS) < 4 do YearS := '0' + YearS;
    while Length(MonthS) < 2 do MonthS := '0' + MonthS;
    while Length(DayS) < 2 do DayS := '0' + DayS;
    TimeStamp := YearS + '-' + MonthS + '-' + DayS + SoapTimePrefix + TimeStamp;
  end;
  HasDateAndTimePart := Pos(SoapTimePrefix, TimeStamp) > 0;
  HasUTCForce := Pos(SLocalTimeMarker, TimeStamp) > 0;
  Result := XMLTimeToDateTime(TimeStamp); // This doesn't support fractions of a second!
  // Now the conversion is done with zero milliseconds, we need to add the fractions
  Result := Result + MSecsFromFractions;
  // XMLTimeToDateTime assumes source as UTC when:
  // - No time part is defined and one of the following holds:
  //   - Explicit time zone is defined (to other than UTC) - here it works WRONG!
  //   - Explicit time zone is NOT defined and UTC flag is NOT defined - here it works WRONG!
  //   - Explicit UTC flag is defined - here it works CORRECT!
  // - Time part is defined and one of the following holds:
  //   - Explicit time zone is NOT defined and UTC flag is NOT defined - here it works WRONG!
  //   - Explicit UTC flag is defined - here it works CORRECT!
  // In the cases where it works wrong, we need to manually offset its result
  // by the local-to-UTC difference.
  if (not HasExplicitTimeZone) and (not HasUTCForce) then
    Result := LocalTimeToUtcTime(Result)
  else if HasExplicitTimeZone and (not HasDateAndTimePart) then
    Result := Result - TimeOffset;  // Minus to remove the effect of the offset
  if not HasDatePart then
  begin
    // We added the current date to make XMLTimeToDateTime work, now we need to
    // remove (the date part of) it back from the end result.
    Result := Result - EncodeDate(Year, Month, Day);
    // Since there originally was no date part, then there should not be one in
    // the end result also, meaning that the result's date should correspond to
    // the zero-day.
    while RealGreaterThanOrEqualTo(Result, 1) do Result := Result - 1;
    while RealLessThan(Result, 0) do Result := Result + 1;
  end;
  Result := Max(Result, MinDateTime); // In erroneous situations XMLTimeToDateTime returns something less than MinDateTime, which we want as default
end;

{ Returns a count of the number of occurences of SubText in Text }
function CountOccurrences(const SubText: string; const Text: string): Integer;
var
  i, j, SubLength: Integer;
  First: Char;
begin
  Result := 0;
  if Length(SubText) <= 0 then Exit;
  First := SubText[1];
  SubLength := Length(SubText);
  for i := 1 to Length(Text) do
  begin
    if Text[i] = First then
    begin
      j := 2;
      while (j <= SubLength) and (Text[i + j - 1] = SubText[j]) do Inc(j);
      if j > SubLength then Inc(result); // Matched all the way
    end;
  end;
end;

function UtcTimeToLocalTime(AUtcTime: TDateTime): TDateTime;
begin
  Result := TTimeZone.Local.ToLocalTime(AUtcTime);
end;

function LocalTimeToUtcTime(ALocalTime: TDateTime): TDateTime;
begin
  Result := TTimeZone.Local.ToUniversalTime(ALocalTime);
end;

function RoundProper(X : extended) : integer;
begin
  Result := RealFloorInt(0.5 + x);
end;

function RealFloorInt(R : extended) : integer;
begin
  Result := Trunc(RealFloor(R));
end;

function RealFloor(R : extended) : extended;
var
  FracR : Extended;
begin
  Result := R;
  FracR := Abs(Frac(R));
  if (FracR >= EPSILON) and RealLessThan(FracR, 1) then begin
    if Frac(R) > 0 then Result := R - Frac(R)
    else Result := R - (1 - Abs(Frac(R)));
  end;
end;

function RealLessThan(R1, R2 : double) : boolean;
begin
  if IsPosInf(R2) then Result := not IsPosInf(R1)
  else if IsNegInf(R2) or IsPosInf(R1) then Result := False
  else if IsNegInf(R1) then Result := not IsNegInf(R2)
  else                            // (-Inf, -EPSILON) => Less,
    Result := R1 - R2 < -EPSILON; // [-EPSILON, EPSILON] => Equal
end;                              // (EPSILON, Inf) => Greater

function RealGreaterThanOrEqualTo(R1, R2 : double) : boolean;
begin
  if IsPosInf(R1) or IsNegInf(R2) then Result := True
  else if IsPosInf(R2) or IsNegInf(R1) then Result := False
  else                            // (-Inf, -EPSILON) => Less,
    Result := R1 - R2 > -EPSILON; // [-EPSILON, EPSILON] => Equal
end;                              // (EPSILON, Inf) => Greater

function RealLessThanOrEqualTo(R1, R2 : double) : boolean;
begin
  if IsPosInf(R2) or IsNegInf(R1) then Result := True
  else if IsPosInf(R1) or IsNegInf(R2) then Result := False
  else                            // (-Inf, -EPSILON) => Less,
    Result := R1 - R2 < EPSILON;  // [-EPSILON, EPSILON] => Equal
end;                              // (EPSILON, Inf) => Greater

function IsPosInf(AValue : extended) : boolean;
begin
  Result := IsInfinite(AValue) and (Sign(AValue) = 1);
end;

function IsNegInf(AValue : extended) : boolean;
begin
  Result := IsInfinite(AValue) and (Sign(AValue) = -1);
end;

end.

Then the unit tests are here:

unit TestMain;
{

  Delphi DUnit Test Case
  ----------------------
  This unit contains a skeleton test case class generated by the Test Case Wizard.
  Modify the generated code to correctly setup and call the methods from the unit 
  being tested.

}

interface

uses
  TestFramework, System.SysUtils, Vcl.Graphics, XSBuiltIns, Winapi.Windows,
  System.Variants, DateUtils, Vcl.Dialogs, Vcl.Controls, Vcl.Forms, Winapi.Messages, Math,
  System.Classes, Main;

type
  // Test methods for class TForm1

  TestTForm1 = class(TTestCase)
  strict private
  public
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestXMLTimeStamp2DateTime;
  end;

implementation

procedure TestTForm1.SetUp;
begin
  // Nothing to do here
end;

procedure TestTForm1.TearDown;
begin
  // Nothing to do here
end;

procedure TestTForm1.TestXMLTimeStamp2DateTime;
const
  TIME_TOLERANCE = 0.0000000115741; // Approximately 1 millisecond, in days
var
  Source: string;
  ReturnValue, ExpectedValue, Today: TDateTime;

  function DateTimeOfToday: TDateTime;
  var
    Year, Month, Day: Word;
  begin
    Year := 0; Month := 0; Day := 0;
    DecodeDate(Now, Year, Month, Day);
    Result := EncodeDate(Year, Month, Day);
  end;

begin
  Today := DateTimeOfToday; // Counted only once, we ignore the theoretic chance of day changing during the test execution from DST to non-DST or vice versa
  {$REGION 'Empty string'}
  // Setup method call parameters
  Source := '';
  ExpectedValue := MinDateTime;
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for empty string should return MinDateTime, but did not!');
  {$ENDREGION}
  {$REGION 'Date only strings'}
  {$REGION 'Date string - local'}
  // Setup method call parameters
  Source := '2002-09-24';
  ExpectedValue := EncodeDate(2002, 9, 24);
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, 'XMLTimeStamp2DateTime for date string - local should return 24.9.2002, but did not!');
  {$ENDREGION}
  {$REGION 'Date string - UTC'}
  // Setup method call parameters
  Source := '2002-09-24Z';
  ExpectedValue := EncodeDate(2002, 9, 24);
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue);
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for date string - UTC should return 24.9.2002 + local time offset, but did not!');
  {$ENDREGION}
  {$REGION 'Date string - negative offset'}
  // Setup method call parameters
  Source := '2002-09-24-03:00';
  ExpectedValue := EncodeDate(2002, 9, 24);
  ExpectedValue := ExpectedValue + EncodeTime(3, 0, 0, 0);  // First convert to UTC by removing the offset
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue); // Then convert to local from UTC
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for date string - negative offset should return 24.9.2002 + three hours + local time offset, but did not!');
  {$ENDREGION}
  {$REGION 'Date string - positive offset'}
  // Setup method call parameters
  Source := '2002-09-24+11:00';
  ExpectedValue := EncodeDate(2002, 9, 24);
  ExpectedValue := ExpectedValue - EncodeTime(11, 0, 0, 0); // First convert to UTC by removing the offset
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue); // Then convert to local from UTC
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for date string - positive offset should return 24.9.2002 - eleven hours + local time offset, but did not!');
  {$ENDREGION}
  {$ENDREGION}
  {$REGION 'Time only strings'}
  {$REGION 'Time string - local'}
  // Setup method call parameters
  Source := '09:30:10';
  ExpectedValue := EncodeTime(9, 30, 10, 0);
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for time string - local should return 09:30:10, but did not!');
  {$ENDREGION}
  {$REGION 'Time string - UTC'}
  // Setup method call parameters
  Source := '09:30:10Z';
  // Have to add Today for the UtcTimeToLocalTime call to have correct DST
  // - then have to remove Today again away to have correct zero-day date
  ExpectedValue := Today + EncodeTime(9, 30, 10, 0);
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue);
  ExpectedValue := ExpectedValue - Today;
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for time string - UTC should return 09:30:10 + local time offset, but did not!');
  {$ENDREGION}
  {$REGION 'Time string - negative offset'}
  // Setup method call parameters
  Source := '09:30:10-03:00';
  // Have to add Today for the UtcTimeToLocalTime call to have correct DST
  // - then have to remove Today again away to have correct zero-day date
  ExpectedValue := Today + EncodeTime(9, 30, 10, 0);
  ExpectedValue := ExpectedValue + EncodeTime(3, 0, 0, 0);  // First convert to UTC by removing the offset
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue); // Then convert to local from UTC
  ExpectedValue := ExpectedValue - Today;
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for time string - negative offset should return 09:30:10 + three hours + local time offset, but did not!');
  {$ENDREGION}
  {$REGION 'Time string - positive offset over date line'}
  // Setup method call parameters
  Source := '06:30:10+11:00';
  // Have to add Today for the UtcTimeToLocalTime call to have correct DST
  // - then have to remove Today again away to have correct zero-day date
  ExpectedValue := Today + EncodeTime(6, 30, 10, 0);
  ExpectedValue := ExpectedValue - EncodeTime(11, 0, 0, 0);  // First convert to UTC by removing the offset
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue); // Then convert to local from UTC
  ExpectedValue := ExpectedValue - Today;
  if RealGreaterThanOrEqualTo(ExpectedValue, 1) then ExpectedValue := ExpectedValue - 1;  // When having time only, date should always be zero!
  if RealLessThan(ExpectedValue, 0) then ExpectedValue := ExpectedValue + 1;  // When having time only, date should always be zero!
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for time string - positive offset (over day change) should return 06:30:10 - eleven hours + local time offset (modulo 24 hours), but did not!');
  {$ENDREGION}
  {$REGION 'Fractional time string with negative offset over date line'}
  // Setup method call parameters
  Source := '14:30:10.25-11:00';
  // Have to add Today for the UtcTimeToLocalTime call to have correct DST
  // - then have to remove Today again away to have correct zero-day date
  ExpectedValue := Today + EncodeTime(14, 30, 10, 250);
  ExpectedValue := ExpectedValue + EncodeTime(11, 0, 0, 0);  // First convert to UTC by removing the offset
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue); // Then convert to local from UTC
  ExpectedValue := ExpectedValue - Today;
  if RealGreaterThanOrEqualTo(ExpectedValue, 1) then ExpectedValue := ExpectedValue - 1;  // When having time only, date should always be zero!
  if RealLessThanOrEqualTo(ExpectedValue, 0) then ExpectedValue := ExpectedValue + 1;  // When having time only, date should always be zero!
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for fractional time string - negative offset (over day change) should return 14:30:10.25 + eleven hours + local time offset (modulo 24 hours), but did not!');
  {$ENDREGION}
  {$ENDREGION}
  {$REGION 'Date and time strings}
  {$REGION 'Date and time string - local'}
  // Setup method call parameters
  Source := '2002-09-24T09:30:10.25';
  ExpectedValue := EncodeDate(2002, 9, 24) + EncodeTime(9, 30, 10, 250);
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for date and time string - local should return 24.9.2002 09:30:10.25, but did not!');
  {$ENDREGION}
  {$REGION 'Date and time string - UTC'}
  // Setup method call parameters
  Source := '2002-09-24T09:30:10.25Z';
  ExpectedValue := EncodeDate(2002, 9, 24) + EncodeTime(9, 30, 10, 250);
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue);
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for date and time string - UTC should return 24.9.2002 09:30:10.25 + local time offset, but did not!');
  {$ENDREGION}
  {$REGION 'Date and time string - positive offset over date line'}
  // Setup method call parameters
  Source := '2002-09-24T06:30:10.25+11:00';
  ExpectedValue := EncodeDate(2002, 9, 24) + EncodeTime(6, 30, 10, 250);
  ExpectedValue := ExpectedValue - EncodeTime(11, 0, 0, 0);  // First convert to UTC by removing the offset
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue); // Then convert to local from UTC
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for date and time string - positive offset (over day change) should return 24.9.2002 06:30:10.25 - eleven hours + local time offset, but did not!');
  {$ENDREGION}
  {$REGION 'Date and time string - negative offset over date line'}
  // Setup method call parameters
  Source := '2002-09-24T14:30:10.25-11:00';
  ExpectedValue := EncodeDate(2002, 9, 24) + EncodeTime(14, 30, 10, 250);
  ExpectedValue := ExpectedValue + EncodeTime(11, 0, 0, 0);  // First convert to UTC by removing the offset
  ExpectedValue := UtcTimeToLocalTime(ExpectedValue); // Then convert to local from UTC
  // Call the method
  ReturnValue := XMLTimeStamp2DateTime(Source);
  // Validate method results
  CheckEquals(ExpectedValue, ReturnValue, TIME_TOLERANCE, 'XMLTimeStamp2DateTime for date and time string - negative offset (over day change) should return 14:30:10.25 + eleven hours + local time offset, but did not!');
  {$ENDREGION}
  {$ENDREGION}
end;

initialization
  // Register any test cases with the test runner
  RegisterTest(TestTForm1.Suite);
end.

Solution 3

OmniXML's unit OmniXMLUtils contains bunch of funcions to do XML to date and date to XML conversions.

function XMLStrToDateTime(nodeValue: XmlString; var value: TDateTime): boolean; overload;
function XMLStrToDateTime(nodeValue: XmlString): TDateTime; overload;
function XMLStrToDateTimeDef(nodeValue: XmlString; defaultValue: TDateTime): TDateTime;
function XMLStrToDate(nodeValue: XmlString; var value: TDateTime): boolean; overload;
function XMLStrToDate(nodeValue: XmlString): TDateTime; overload;
function XMLStrToDateDef(nodeValue: XmlString; defaultValue: TDateTime): TDateTime;
function XMLStrToTime(nodeValue: XmlString; var value: TDateTime): boolean; overload;
function XMLStrToTime(nodeValue: XmlString): TDateTime; overload;
function XMLStrToTimeDef(nodeValue: XmlString; defaultValue: TDateTime): TDateTime;

function XMLDateTimeToStr(value: TDateTime): XmlString;
function XMLDateTimeToStrEx(value: TDateTime): XmlString;
function XMLDateToStr(value: TDateTime): XmlString;
function XMLTimeToStr(value: TDateTime): XmlString;
Share:
23,325

Related videos on Youtube

Charles Faiga
Author by

Charles Faiga

Software developer working with: Delphi, C++, C, MySql and Embedded Systems

Updated on July 09, 2022

Comments

  • Charles Faiga
    Charles Faiga almost 2 years

    XML date and time are in the format

    '-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?

    were

    •'-'? yyyy is a four-or-more digit optionally negative-signed numeral that represents the year; if more than four digits, leading zeros are prohibited, and '0000' is prohibited

    •the remaining '-'s are separators between parts of the date portion;

    •the first mm is a two-digit numeral that represents the month;

    •dd is a two-digit numeral that represents the day;

    •'T' is a separator indicating that time-of-day follows;

    •hh is a two-digit numeral that represents the hour; '24' is permitted if the minutes and seconds represented are zero, and the dateTime value so represented is the first instant of the following day (the hour property of a dateTime object in the ·value space· cannot have a value greater than 23);

    •':' is a separator between parts of the time-of-day portion;

    •the second mm is a two-digit numeral that represents the minute;

    •ss is a two-integer-digit numeral that represents the whole seconds;

    •'.' s+ (if present) represents the fractional seconds;

    •zzzzzz (if present) represents the timezone (as described below).

    here are more examples

    Simple Example 2009-08-31T19:30:00

    More complex examples

    2002-10-10T12:00:00-05:00 (noon on 10 October 2002, Central Daylight Savings Time as well as Eastern Standard Time in the U.S.) is 2002-10-10T17:00:00Z, five hours later than 2002-10-10T12:00:00Z.

    see www.w3.org/TR/2004/REC-xmlschema-2-20041028/datatypes.html for more info

    • mghie
      mghie over 14 years
      If there is such a function, be very careful with it. It can lose information, as TDateTime can neither hold dates before TDateTime(0.0), nor has it a concept of time zones or daylight saving time.
    • Ken White
      Ken White over 14 years
      @Mason, the part of your comment about TDateTime = 0.0 is incorrect. Setting a TDateTime value to -693593.00, for instance, results in a date of 01/01/0001 correctly being stored. Using StrToDate('01/01/0001') also correctly returns -693593.00
    • Ken White
      Ken White over 14 years
      @mghie: Sorry for misdirecting the previous comment response to Mason. :-(
  • James
    James almost 14 years
    +1 I never realised this unit existed! Is it possible to ignore things like milliseconds/utc offset?
  • Jeroen Wiert Pluimers
    Jeroen Wiert Pluimers almost 14 years
    I think you can do that using the TXSBaseCustomDateTime class in the same unit.
  • DelphiUser
    DelphiUser over 10 years
    I've now been testing this, with the specification help from this page that I found: w3schools.com/schema/schema_dtypes_date.asp Unfortunately it seems that the the TXSDateTime really works correctly only with strings that contain both a date and time (as was asked in the OP so no prob there). But when I try to give strings that just the date part, the method ignores time zone info, and when I try to give strings with just time part, I get a "not a valid date time" error. So strings like '2002-09-24-03:00' and '09:30:10' don't seem to work, unless I've misunderstood this somehow.
  • DelphiUser
    DelphiUser over 10 years
    Addition: It seems that even when there are both parts, the method assumes times as UTC time if no explicit offset is given. I.e. when I'm locally at UTC+3 and give the string with the time 09:30:10, it returns the same result as it would with 09:30:10Z. In both cases it stores the hour offset as 0, while in the former I'd expect it to have a 3 hour offset.
  • Jeroen Wiert Pluimers
    Jeroen Wiert Pluimers over 10 years
    @DelphiUser Good observations. When you do not pass time information, use TXSDate and when you do not pass date information, use TXSTime.
  • DelphiUser
    DelphiUser over 10 years
    @JeroenWiertPluimers Thanks, I could try that, though with just looking at the code it seems as if the same problems (at least with the time zones) could hold, i.e. the parsing of the date with time zone info would always return the date as UTC and not local. But I'd have to verify this with code to be sure.
  • Jeroen Wiert Pluimers
    Jeroen Wiert Pluimers over 10 years
    @DelphiUser write some unit tests for it. Or post some strings and expected values here and I'll write some unit tests for it. Can apply the same tests with OmniXML: stackoverflow.com/questions/1438870/…
  • DelphiUser
    DelphiUser over 10 years
    Addendum: There was still a problem when the time-to-decode was in DST and the current time is not (or vice versa). I tried to fix this by adding this final step in the function: if HasDatePart then begin TimeStampInDST := IsTimeInDST(Result); if TimeStampInDST <> CurrentTimeInDST then begin DSTOffset := EncodeTime(1, 0, 0, 0); // 1 hour offset to either direction if TimeStampInDST then Result := Result + DSTOffset else Result := Result - DSTOffset; end; end; This uses IsTimeInDST (own method) and gets the current time's status earlier, but idea is this
  • DelphiUser
    DelphiUser over 7 years
    Addendum #2: Now after having switched to Delphi 10 (from XE3), I noticed my test started to fail in the case when the DST status of the time-to-decode is different from that of the current time. So since earlier the system implementation had a failure there and I had to manually correct this, now it seems that the system implementation has been fixed at this step, and I could remove my fix. So if you are using Delphi 10, then the original code (without my Addendum #1) should again be ok :).