views:

227

answers:

7

Hi,

Is there any way in Delphi 5 to convert a string to a TDateTime where you can specify the actual format to use?

I'm working on a jobprocessor, which accepts tasks from various workstations. The tasks have a range of parameters, some of which are dates, but (unfortunately, and out of my control) they're passed as strings. Since the jobs can come from different workstations, the actual datetime format used to format the dates as a string might (and, of course, actual do) differ.

Googling around, the only quick solutions I found was to sneakily change the ShortDateFormat variable, and restore it to its original value afterwards. Since ShortDateFormat is a global variable, and I'm working in a threaded environment the only way this would work is by synchronizing every access to it, which is completely unacceptable (and undoable).

I could copy the library code from the SysUtils unit into my own methods, and tweak them to work with a specified format instead of the global variables, but I'm just wondering if there's something more adequate out there that I missed.

Kind regards, and thanks in advance,

Willem

UPDATE

To put it more succinctly:

I need something like StrToDate (or StrToDateTime), with the added option of specifying the exact format it should use to convert the string to a TDateTime.

+1  A: 

I am not sure about what you want. I don't use Delphi 5 any more but I am pretty sure the function StrToDateTime exists in it. Using it you can convert a string to TDateTime with format settings. Then you can convert such TDateTime to any format using FormatDateTime which enables you to use any date format you wish.

Eduardo Mauro
Yes, it does exists. But it works with a global variable: The current setting of ShortDateFormat. I'm dealing with multiple formats at a given time. I need to be able to specify the exact format to use to convert the string to a TDateTime. Adjusting the global variable (for reasons specified in the question) is not an option.
Willem van Rumpt
The problem is Delphi 5! At least since Delphi 7 there is an overloaded StrToDateTime which accepts format settings solving the problem with multithreaded applications. That is the drawback of using an outdated environment.
Uwe Raabe
@Uwe: I knew it :( I think I'll just copy the library code, and tweak it...
Willem van Rumpt
@Uwe: Regarding using an outdated environment: Updating/upgrading is not my assignment. I think the latest Delphi release was just under a month ago, but from what I can see in the existing codebase, and given the changes in Delphi, I don't think they'll ever upgrade...
Willem van Rumpt
I can feel with you. This scheme is seen too often in the wild. That's why I try to upgrade continuously. It makes the process much easier. If you pile it up version by version there is a point when you cannot handle it anymore.
Uwe Raabe
Personally, I haven't been using Delphi since version 7. My business revolves mostly around .NET. I've kept following the (quite numerous amount of) installments of Delphi since then from the side, but for me there, there's no business case for purchasing Delphi (let alone **with** SA). Working with Delphi is because of the contractor having an existing code base (in this case Delphi 5), not because it was my choice. It's business. Currently, for me, there is no business case for buying Delphi. I like Delphi, and should it become viable for my business to purchase it, I will. But for now: No.
Willem van Rumpt
A: 

I would go the other way around it. As I see it, you have about two options of wich you mentioned one yourself

  • Adjust the ShortDateFormat and keep every acces to it synchronized.
  • If you know the format of the strings you are recieving (somehow, you'll have to), just do some string juggling to first get your strings in your current shortdateformat. After that, convert the (juggled) string to a TDateTime.

I do wonder how you are going to determine the format for say 04/05/2010.

program DateTimeConvert;
{$APPTYPE CONSOLE}
uses
  SysUtils;


function GetPart(const part, input, format: string): string;
var
  I: Integer;
begin
  for I := 1 to Length(format) do
    if Uppercase(format[I]) = Uppercase(part) then
      Result := Result + input[I];
end;

function GetDay(const input, format: string): string;
begin
  Result := GetPart('d', input, format);
  if Length(Result) = 1 then Result := SysUtils.Format('0%0:s', [Result]);
end;

function GetMonth(const input, format: string): string;
begin
  Result := GetPart('m', input, format);
  if Length(Result) = 1 then Result := SysUtils.Format('0%0:s', [Result]);
end;

function GetYear(const input, format: string): string;
begin
  Result := GetPart('y', input, format);
end;

function ConvertToMyLocalSettings(const input, format: string): string;
begin
  Result := SysUtils.Format('%0:s/%1:s/%2:s', [GetDay(input, format), GetMonth(input, format), GetYear(input, format)]);
end;

begin
  Writeln(ConvertToMyLocalSettings('05/04/2010', 'dd/mm/yyyy'));
  Writeln(ConvertToMyLocalSettings('05-04-2010', 'dd-mm-yyyy'));
  Writeln(ConvertToMyLocalSettings('5-4-2010', 'd-m-yyyy'));
  Writeln(ConvertToMyLocalSettings('4-5-2010', 'm-d-yyyy'));
  Writeln(ConvertToMyLocalSettings('4-05-2010', 'M-dd-yyyy'));
  Writeln(ConvertToMyLocalSettings('05/04/2010', 'dd/MM/yyyy'));
  Readln;
end.
Lieven
Adjusting the datetime parameters to be no longer passed as a string would be undoable. Adding an additional parameter specifying the ShortDateFormat used on the workstation would be trivial however. That's why I'm searching for something like StrToDate(strDate: string; format: string) :)
Willem van Rumpt
Either I or you misunderstood. The crux of the solution **is** to pass the datetimes as strings. On **your** workstation you fixup the passed through strings to match the format you use on your workstation. After that, it's just a call to StrToDate on your workstation.
Lieven
We're on the same level. I can't change the dates being passed as strings. I **can** add an additional parameter specifying the format used. What I don't have in Delphi 5 is a StrToDate method that accepts a format parameter. I'll have to write my own code (or, glancing over the library code, copy and adjust it): your option two (and my own option 1, if there isn't anything else that covers my needs).
Willem van Rumpt
To make it clear (it's friday after all), the values passed to your workstation by others is a TDateTime then?! If not, it's you who've misunderstood <g>
Lieven
No. The value I receive is a string. And (sofar, sogood, hoping for the best for now) it will be a string formatted using the ShortDateFormat. StrToDate in Delphi 5 in this case is useless, as it will convert the string using the ShortDateFormat of the current user. For workstation X the format is "M-dd-yyyy", for workstation Y its "dd/MM/yyyy", for workstation Z its "MMM/d/y". There's a plethora of options. It's fiendlishly difficult to convert a string to a date, that's why I'm looking for a built in option, before resorting to a custom method.
Willem van Rumpt
ok, one last try because I still don't grasp it: I wouldn't know how to convert "MMM/d/y" (the y part) but all other examples are converted (on your local machine) to dd/mm/yyyy with the edited answer. From there on, it's just a StrToDate on your local machine.
Lieven
Reread the problems about threading.Add the problems surrounding converting a string to a date. It's not trivial. Your code does not even come close to covering every aspect. Open up SysUtils.pas (if you have the apropriate license), and emerge yourself into the internals of StrToDate.
Willem van Rumpt
Presented code is threadsafe and while (very) much is left for optimization, if you can convert the input string to a string that can be processed by StrTodate, there's no need trying to mimic "all" the dirty details of StrToDate. btw, if I gave you the impression that I thought this to be trivial, I don't. But at least this solution can be relatively easy extended when you encounter a "new" format from a new workstation.
Lieven
Since it IS friday: Trying to convert a string to a date(time), without knowing the format it was formatted with, is an exercise of trial and error. Without format, no date, there is just void. This goes for whatever library or language you use. It's just that some stand a better chance of getting it right.
Willem van Rumpt
You've said it would be no problem passing the format in. I've based the answer on that. If that's not possible, well, you'r <ahum> screwed.
Lieven
<off topic> enig idee waarom we dit gesprek niet gewoon in het Nederlands voeren? :). Goed weekend. </off topic>
Lieven
When you say "...there is no need trying to mimic...", are you talking about your demands or my demands? I don't know what your criteria are, I know what mine are. I get a date, in string format, it might be in any format, allowed by windows. That's what I have to convert to TDateTime. StrToDate in Delphi 5 was not made to accomadate that requirement. Really, as I said before: Converting a string to a date, in any language is fiendlishly difficult. .NET (the language I use the mose, C# to be specific), gets it right 99% of the cases. And even there, there's room for exceptions.
Willem van Rumpt
<offTopic>Geen idee :) zodra het inhoudelijk over werk gaat ("programmeren") is engels wel zo makkelijk. Daarnaast werk ik veel in het buitenland, en dan is Engels de enige mogelijke voertaal :) </offTopic>("dan is Engels de enige mogelijk voertaal") => Belgie en enkele ex-colonies uitgezonderd....
Willem van Rumpt
+3  A: 

Later versions of Delphi can take an extra TFormatSettings argument to the string conversion functions. TFormatSettings is a structure containing the various format global variables (ShortDateFormat, LongDateFormat, etc). So you can override those value in a thread-safe manner and even for a single call.

I don't remember in which version of Delphi this was introduced, but I'm pretty sure it was after Delphi 5.

So yeah, as far as I know, you either need to synchronize every access to ShortDateFormat, or use a different function.

Ken Bourassa
That's what I was afraid of. Uwe said the functionality I want is there from Delphi 7 and upwards. Unfortunately that's out of my control. A different function it will be :)
Willem van Rumpt
+3  A: 

I created such routine for FreePascal's dateutils unit, and it should easy to port, if porting is needed at all.

Code:

http://svn.freepascal.org/cgi-bin/viewvc.cgi/trunk/rtl/objpas/dateutil.inc?view=co

(code is the last (huge) procedure at the end of the file)

documentation:

http://www.freepascal.org/docs-html/rtl/dateutils/scandatetime.html

Note that it is not a complete inverse of formatdatetime, and it has some extensions:

  • An inverse of FormatDateTime is not 100% an inverse, simply because one can put e.g. time tokens twice in the format string,and scandatetime wouldn't know which time to pick.

  • Strings like hn can't be reversed safely. E.g. 1:2 (2 minutes after 1) delivers 12 which is parsed as 12:00 and then misses chars for the "n" part.

    • trailing characters are ignored.
    • no support for Eastern Asian formatting characters since they are windows only.
    • no MBCS support.
  • Extensions

    • #9 eats whitespace.
    • whitespace at the end of a pattern is optional.
    • ? matches any char.
    • Quote the above chars to really match the char.

(I believe these comments are slightly outdated in the sense hat some Asian support was added later but I am not sure)

Marco van de Voort
Thanks :) I will go over the code. I know string to date conversions are hell, to put it mildly, so I'm not expecting a 100% success rate, there's always something missing (military timezones anyone? ;) ). Just a decent method, accepting a datetime as a string and a reasonable format.
Willem van Rumpt
Thanks again. Although I didn't use your method(s) (opting instead for copying of library code), yours was the only viable other solution. My rationale is that if I was going to copy code doing basically the same as StrToDate with the added option of specifying the format instead of using a global variable, I prefer it to do exactly the same as the built in one, just not to have it use a global variable. That way both for me, as for my current employers, the end result isin conformance with their environment, and more deterministic (in testing, for example).
Willem van Rumpt
+2  A: 

Use VarToDateTime instead. It supports many more date formats in the string and converts them automatically.

var
  DateVal: TDateTime;
begin
  DateVal := VarToDateTime('23 Sep 2010');
  ShowMessage(DateToStr(DateVal));
end;

I see you're using Delphi 5. Some versions of Delphi will need to add Variants to the uses clause; most later versions add it for you. I don't remember which category Delphi 5 fell into.

Ken White
Thanks Ken, I haven't tried it yet, but it sounds promising. Anything that will avoid rolling my own conversion (be it by hand or by copying) is definitely an option.
Willem van Rumpt
+1  A: 

If you want to know how this was solved in later Delphi's, you can take a look at the source of a slightly more modern (looks like Delphi 6) sysutils.pas here:

http://anygen.googlecome.com/.../SysUtils.pas

Check out the overloaded versions of StrToDateTime that take a TFormatSettings parameter.

function StrToDateTime(const S: string;
  const FormatSettings: TFormatSettings): TDateTime; overload;
Wouter van Nifterick
A: 

Here's the function, and its two helpers, i wrote to parse a string using an exact datetime format. And since Stackoverflow is also a code-wiki: here's all the code:

class function TDateTimeUtils.TryStrToDateExact(const S, DateFormat: string; PivotYear: Integer;
        out Value: TDateTime): Boolean;
var
    Month, Day, Year: Integer;
    Tokens: TStringDynArray;
    CurrentToken: string;
    i, n: Integer;
    Partial: string;
    MaxValue: Integer;
    nCurrentYear: Integer;

    function GetCurrentYear: Word;
    var
        y, m, d: Word;
    begin
        DecodeDate(Now, y, m, d);
        Result := y;
    end;
begin
    Result := False;
{
    M/dd/yy

    Valid pictures codes are

        d       Day of the month as digits without leading zeros for single-digit days.
        dd      Day of the month as digits with leading zeros for single-digit days.
        ddd Abbreviated day of the week as specified by a LOCALE_SABBREVDAYNAME* value, for example, "Mon" in English (United States).
                Windows Vista and later: If a short version of the day of the week is required, your application should use the LOCALE_SSHORTESTDAYNAME* constants.
        dddd    Day of the week as specified by a LOCALE_SDAYNAME* value.

        M       Month as digits without leading zeros for single-digit months.
        MM      Month as digits with leading zeros for single-digit months.
        MMM Abbreviated month as specified by a LOCALE_SABBREVMONTHNAME* value, for example, "Nov" in English (United States).
        MMMM    Month as specified by a LOCALE_SMONTHNAME* value, for example, "November" for English (United States), and "Noviembre" for Spanish (Spain).

        y       Year represented only by the last digit.
        yy      Year represented only by the last two digits. A leading zero is added for single-digit years.
        yyyy    Year represented by a full four or five digits, depending on the calendar used. Thai Buddhist and Korean calendars have five-digit years. The "yyyy" pattern shows five digits for these two calendars, and four digits for all other supported calendars. Calendars that have single-digit or two-digit years, such as for the Japanese Emperor era, are represented differently. A single-digit year is represented with a leading zero, for example, "03". A two-digit year is represented with two digits, for example, "13". No additional leading zeros are displayed.
        yyyyy   Behaves identically to "yyyy".

        g, gg   Period/era string formatted as specified by the CAL_SERASTRING value.
                The "g" and "gg" format pictures in a date string are ignored if there is no associated era or period string.


        PivotYear
                The maximum year that a 1 or 2 digit year is assumed to be.
                The Microsoft de-factor standard for y2k is 2029. Any value greater
                than 29 is assumed to be 1930 or higher.

                e.g. 2029:
                    1930, ..., 2000, 2001,..., 2029

                If the PivotYear is between 0 and 99, then PivotYear is assumed to be
                a date range in the future. e.g. (assuming this is currently 2010):

                    Pivot   Range
                    0       1911..2010  (no future years)
                    1       1912..2011
                    ...
                    98      2009..2108
                    99      2010..2099  (no past years)

                0 ==> no years in the future
                99 ==> no years in the past
}
    if Length(S) = 0 then
        Exit;
    if Length(DateFormat) = 0 then
        Exit;

    Month := -1;
    Day := -1;
    Year := -1;

    Tokens := TDateTimeUtils.TokenizeFormat(DateFormat);
    n := 1; //input string index
    for i := Low(Tokens) to High(Tokens) do
    begin
        CurrentToken := Tokens[i];
        if CurrentToken = 'MMMM' then
        begin
            //Long month names, we don't support yet (you're free to write it)
            Exit;
        end
        else if CurrentToken = 'MMM' then
        begin
            //Short month names, we don't support yet (you're free to write it)
            Exit;
        end
        else if CurrentToken = 'MM' then
        begin
            //Month, with leading zero if needed
            if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 1{MinValue}, 12{MaxValue}, {var}Month) then Exit;
        end
        else if CurrentToken = 'M' then
        begin
            //months
            if not ReadDigitString(S, n, 1{MinDigits}, 2{MaxDigits}, 1{MinValue}, 12{MaxValue}, {var}Month) then Exit;
        end
        else if CurrentToken = 'dddd' then
        begin
            Exit; //Long day names, we don't support yet (you're free to write it)
        end
        else if CurrentToken = 'ddd' then
        begin
            Exit; //Short day names, we don't support yet (you're free to write it);
        end
        else if CurrentToken = 'dd' then
        begin
            //If we know what month it is, and even better if we know what year it is, limit the number of valid days to that
            if (Month >= 1) and (Month <= 12) then
            begin
                if Year > 0 then
                    MaxValue := MonthDays[IsLeapYear(Year), Month]
                else
                    MaxValue := MonthDays[True, Month]; //we don't know the year, assume it's a leap year to be more generous
            end
            else
                MaxValue := 31; //we don't know the month, so assume it's the largest

            if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 1{MinValue}, MaxValue{MaxValue}, {var}Day) then Exit;
        end
        else if CurrentToken = 'd' then
        begin
            //days
            //If we know what month it is, and even better if we know what year it is, limit the number of valid days to that
            if (Month >= 1) and (Month <= 12) then
            begin
                if Year > 0 then
                    MaxValue := MonthDays[IsLeapYear(Year), Month]
                else
                    MaxValue := MonthDays[True, Month]; //we don't know the year, assume it's a leap year to be more generous
            end
            else
                MaxValue := 31; //we don't know the month, so assume it's the largest

            if not ReadDigitString(S, n, 1{MinDigits}, 2{MaxDigits}, 1{MinValue}, MaxValue{MaxValue}, {var}Day) then Exit;
        end
        else if (CurrentToken = 'yyyy') or (CurrentToken = 'yyyyy') then
        begin
            //Year represented by a full four or five digits, depending on the calendar used.
            {
                Thai Buddhist and Korean calendars have five-digit years.
                The "yyyy" pattern shows five digits for these two calendars,
                    and four digits for all other supported calendars.
                Calendars that have single-digit or two-digit years, such as for
                    the Japanese Emperor era, are represented differently.
                    A single-digit year is represented with a leading zero, for
                    example, "03". A two-digit year is represented with two digits,
                    for example, "13". No additional leading zeros are displayed.
            }
            if not ReadDigitString(S, n, 4{MinDigits}, 4{MaxDigits}, 0{MinValue}, 9999{MaxValue}, {var}Year) then Exit;
        end
        else if CurrentToken = 'yyy' then
        begin
            //i'm not sure what this would look like, so i'll ignore it
            Exit;
        end
        else if CurrentToken = 'yy' then
        begin
            //Year represented only by the last two digits. A leading zero is added for single-digit years.
            if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 0{MinValue}, 99{MaxValue}, {var}Year) then Exit;

            nCurrentYear := GetCurrentYear;
            Year := (nCurrentYear div 100 * 100)+Year;

            if (PivotYear < 100) and (PivotYear >= 0) then
            begin
                //assume pivotyear is a delta from this year, not an absolute value
                PivotYear := nCurrentYear+PivotYear;
            end;

            //Check the pivot year value
            if Year > PivotYear then
                Year := Year - 100;
        end
        else if CurrentToken = 'y' then
        begin
            //Year represented only by the last digit.
            if not ReadDigitString(S, n, 1{MinDigits}, 1{MaxDigits}, 0{MinValue}, 9{MaxValue}, {var}Year) then Exit;

            nCurrentYear := GetCurrentYear;
            Year := (nCurrentYear div 10 * 10)+Year;

            if (PivotYear < 100) and (PivotYear >= 0) then
            begin
                //assume pivotyear is a delta from this year, not an absolute value
                PivotYear := nCurrentYear+PivotYear;
            end;

            //Check the pivot year value
            if Year > PivotYear then
                Year := Year - 100;
        end
        else
        begin
            //The input string should contains CurrentToken starting at n
            Partial := Copy(S, n, Length(CurrentToken));
            Inc(n, Length(CurrentToken));
            if Partial <> CurrentToken then
                Exit;
        end;
    end;

    //If there's still stuff left over in the string, then it's not valid
    if n <> Length(s)+1 then
    begin
        Result := False;
        Exit;
    end;

    if Day > MonthDays[IsLeapYear(Year), Month] then
    begin
        Result := False;
        Exit;
    end;

    try
        Value := EncodeDate(Year, Month, Day);
    except
        Result := False;
        Exit;
    end;
    Result := True;
end;


class function TDateTimeUtils.TokenizeFormat(fmt: string): TStringDynArray;
var
    i: Integer;
    partial: string;

    function IsDateFormatPicture(ch: AnsiChar): Boolean;
    begin
        case ch of
        'M','d','y': Result := True;
        else Result := False;
        end;
    end;
begin
    SetLength(Result, 0);

    if Length(fmt) = 0 then
        Exit;

    //format is only one character long? If so then that's the tokenized entry
    if Length(fmt)=1 then
    begin
        SetLength(Result, 1);
        Result[0] := fmt;
    end;

    partial := fmt[1];
    i := 2;
    while i <= Length(fmt) do
    begin
        //If the characters in partial are a format picture, and the character in fmt is not the same picture code then write partial to result, and reset partial
        if IsDateFormatPicture(partial[1]) then
        begin
            //if the current fmt character is different than the running partial picture
            if (partial[1] <> fmt[i]) then
            begin
                //Move the current partial to the output
                //and start a new partial
                SetLength(Result, Length(Result)+1);
                Result[High(Result)] := partial;
                Partial := fmt[i];
            end
            else
            begin
                //the current fmt character is more of the same format picture in partial
                //Add it to the partial
                Partial := Partial + fmt[i];
            end;
        end
        else
        begin
            //The running partial is not a format picture.
            //If the current fmt character is a picture code, then write out the partial and start a new partial
            if IsDateFormatPicture(fmt[i]) then
            begin
                //Move the current partial to the output
                //and start a new partial
                SetLength(Result, Length(Result)+1);
                Result[High(Result)] := partial;
                Partial := fmt[i];
            end
            else
            begin
                //The current fmt character is another non-picture code. Add it to the running partial
                Partial := Partial + fmt[i];
            end;
        end;

        Inc(i);
        Continue;
    end;

    //If we have a running partial, then add it to the output
    if partial <> '' then
    begin
        SetLength(Result, Length(Result)+1);
        Result[High(Result)] := partial;
    end;
end;

class function TDateTimeUtils.ReadDigitString(const S: string; var Pos: Integer;
            MinDigits, MaxDigits: Integer; MinValue, MaxValue: Integer;
            var Number: Integer): Boolean;
var
    Digits: Integer;
    Value: Integer;
    Partial: string;
    CandidateNumber: Integer;
    CandidateDigits: Integer;
begin
    Result := False;
    CandidateNumber := -1;
    CandidateDigits := 0;

    Digits := MinDigits;
    while Digits <= MaxDigits do
    begin
        Partial := Copy(S, Pos, Digits);
        if Length(Partial) < Digits then
        begin
            //we couldn't get all we wanted. We're done; use whatever we've gotten already
            Break;
        end;

        //Check that it's still a number
        if not TryStrToInt(Partial, Value) then
            Break;

        //Check that it's not too big - meaning that getting anymore wouldn't work
        if (Value > MaxValue) then
            Break;

        if (Value >= MinValue) then
        begin
            //Hmm, looks good. Keep it as our best possibility
            CandidateNumber := Value;
            CandidateDigits := Digits;
        end;

        Inc(Digits); //try to be greedy, grabbing even *MORE* digits
    end;

    if (CandidateNumber >= 0) or (CandidateDigits > 0) then
    begin
        Inc(Pos, CandidateDigits);
        Number := CandidateNumber;
        Result := True;
    end;
end;
Ian Boyd