19

I can convert a Delphi TDate to ISO 8601 format easily using this:

DateTimeToString(result, 'yyyy-mm-dd', myDate);

What's the idiomatic way to do the inverse conversion? StringToDateTime() doesn't seem to exist.

Obviously I can do it the "hard" way by manually parsing the string and encoding the result, but that seems a poor choice.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
Roddy
  • 66,617
  • 42
  • 165
  • 277
  • possible duplicate of [Converting a string to TDateTime based on an arbitrary format](http://stackoverflow.com/questions/3786823/converting-a-string-to-tdatetime-based-on-an-arbitrary-format) – NGLN Mar 27 '12 at 20:30

7 Answers7

19

From XE8 onwards, use ISO8601ToDate (and DateToISO8601) from dateutils.pas.

http://docwiki.embarcadero.com/Libraries/XE8/en/System.DateUtils.ISO8601ToDate

Roddy
  • 66,617
  • 42
  • 165
  • 277
19

why re-invent the wheel?

XML uses ISO 8601 for date and date-time storage.

Delphi has had built-in support for that since Delphi 6 in the XSBuiltIns unit.

This answer explains how for DateTime, this is for Date only using the TXSDate class:

with TXSDate.Create() do
  try
    AsDate := Date; // convert from TDateTime
    DateString := NativeToXS; // convert to WideString
  finally
    Free;
  end;

with TXSDate.Create() do
  try
    XSToNative(DateString); // convert from WideString
    Date := AsDate; // convert to TDateTime
  finally
    Free;
  end;
Community
  • 1
  • 1
Jeroen Wiert Pluimers
  • 23,965
  • 9
  • 74
  • 154
  • 2
    uses XSBuiltins; XMLTimeToDateTime(str, True); - will work as well. – T.S Jul 03 '18 at 13:48
  • Good point. That works from Delphi 7 and up. However note it does the same as the above code with `TXSDateTime` internally, so it includes the time portion as well. – Jeroen Wiert Pluimers Jul 03 '18 at 16:27
9

I think this should work... the documentation says the overloaded version of these methods is for use in threads, but it can be handy for specifying the format settings you wish to use at the time.

Function ISO8601ToDateTime(Value: String):TDateTime;
var
    FormatSettings: TFormatSettings;
begin
    GetLocaleFormatSettings(GetThreadLocale, FormatSettings);
    FormatSettings.DateSeparator := '-';
    FormatSettings.ShortDateFormat := 'yyyy-MM-dd';
    Result := StrToDate(Value, FormatSettings);
end;

You can of course write variants of this with StrToDateDef and TryStrToDate with equivalent functionality

Alexander233
  • 390
  • 3
  • 12
James
  • 9,774
  • 5
  • 34
  • 58
  • 3
    You might want to initialize the formatsettings too, with the system default. Depending if you are going to use it for something else except parsing dates: `GetLocaleFormatSettings(LOCALE_SYSTEM_DEFAULT, FormatSettings);` fills the FormatSettings record with the system defaults. – R-D Jul 11 '11 at 15:13
  • @Roald, Thanks.. I was just running a couple of tests on it! I'll update it in a moment – James Jul 11 '11 at 15:25
  • 2
    Definitely use the "thread-safe" overloaded versions, otherwise you will change the way your application displays dates if you use DateToStr, or FormatDateTime with 'c' or 'ddddd' or anything else that uses ShortDateFormat. – Gerry Coll Jul 12 '11 at 05:11
7

You can find Iso-8601 conversion routines in our SynCommons unit.

It has been deeply optimized for speed, so it's much faster than the DateTimeToString() functions and such, but of course, code is more difficult to follow. ;)

procedure Iso8601ToDateTimePUTF8CharVar(P: PUTF8Char; L: integer; var result: TDateTime); 
var i: integer;
    B: cardinal;
    Y,M,D, H,MI,SS: cardinal;
// we expect 'YYYYMMDDThhmmss' format but we handle also 'YYYY-MM-DD hh:mm:ss'
begin
  result := 0;
  if P=nil then
    exit;
  if L=0 then
    L := StrLen(P);
  if L<4 then
    exit; // we need 'YYYY' at least
  if P[0]='T' then
    dec(P,8) else begin
    B := ConvertHexToBin[ord(P[0])]; // first digit
    if B>9 then exit else Y := B; // fast check '0'..'9'
    for i := 1 to 3 do begin
      B := ConvertHexToBin[ord(P[i])]; // 3 other digits
      if B>9 then exit else Y := Y*10+B;
    end;
    if P[4] in ['-','/'] then begin inc(P); dec(L); end; // allow YYYY-MM-DD
    D := 1;
    if L>=6 then begin // YYYYMM
      M := ord(P[4])*10+ord(P[5])-(48+480);
      if (M=0) or (M>12) then exit;
      if P[6] in ['-','/'] then begin inc(P); dec(L); end; // allow YYYY-MM-DD
      if L>=8 then begin // YYYYMMDD
        D := ord(P[6])*10+ord(P[7])-(48+480);
        if (D=0) or (D>MonthDays[true][M]) then exit; // worse is leap year=true
      end;
    end else
      M := 1;
    if M>2 then // inlined EncodeDate(Y,M,D)
      dec(M,3) else
    if M>0 then begin
      inc(M,9);
      dec(Y);
    end;
    with Div100(Y) do
      result := (146097*YDiv100) shr 2 + (1461*YMod100) shr 2 +
            (153*M+2) div 5+D-693900;
    if (L<15) or not(P[8] in [' ','T']) then
      exit;
  end;
  H := ord(P[9])*10+ord(P[10])-(48+480);
  if P[11]=':' then inc(P); // allow hh:mm:ss
  MI := ord(P[11])*10+ord(P[12])-(48+480);
  if P[13]=':' then inc(P); // allow hh:mm:ss
  SS := ord(P[13])*10+ord(P[14])-(48+480);
  if (H<24) and (MI<60) and (SS<60) then // inlined EncodeTime()
    result := result + (H * (MinsPerHour * SecsPerMin * MSecsPerSec) +
             MI * (SecsPerMin * MSecsPerSec) + SS * MSecsPerSec) / MSecsPerDay;
end;

This is able to handle a very fast conversion from an UTF-8 encoded buffer into a TDateTime. For all constants dependencies, check the unit source code.

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • Typical. I'm using synapse anyway, but never realised this was included :-) – Roddy Jul 11 '11 at 15:11
  • 5
    This function isn't fully ISO8601 compliant though. The spec says to use "T" as separator between the date and time strings. Omitting it is only allowed by mutual agreement. Secondly there is no support for time zone indication at the end of the string, which is required by many webservices. – Anders E. Andersen Jan 24 '14 at 20:17
6

For more flexibility, you could consider Marco van de Voort's scandate routine which handles your string in any format:

var
  D: TDateTime;
begin
  D := ScanDate('yyyy-mm-dd', '2011-07-11');

See final version (7kB .zip) as added to FPC.

Community
  • 1
  • 1
NGLN
  • 43,011
  • 8
  • 105
  • 200
  • 2
    the English definitive does not equal the Dutch "definitief". In this context the Dutch "definitieve versie" is better translated as the "final version". A "definitive version" is more akin to saying the "ultimate"version... Then again, perhaps you meant to say that :-)) – Marjan Venema Jul 11 '11 at 18:28
  • Always happy to help a fellow country(wo)men – Marjan Venema Jul 11 '11 at 18:34
1
USES Soap.XSBuiltIns;
...
Function XMLDateTimeToLocalDateTime(const Value: String): TDateTime;
begin
  with TXSDateTime.Create do
  try
    XSToNative(Value);
    result := AsDateTime;
  finally
    Free;
  end;
end;

Delphi XE3

Akella225
  • 61
  • 2
  • 8
1

Starting with XE6 you can use function System.DateUtils.ISO8601ToDate:

uses
  System.DateUtils;
var
  vDat: TDateTime;
begin
  vDat := ISO8601ToDate('2018-03-26T11:01:35.867Z');
end.