【问题标题】:Converting a string to TDateTime based on an arbitrary format基于任意格式将字符串转换为 TDateTime
【发布时间】:2010-09-24 12:01:06
【问题描述】:

Delphi 5 中是否有任何方法可以将字符串转换为 TDateTime,您可以在其中指定要使用的实际格式?

我正在开发一个作业处理器,它接受来自不同工作站的任务。这些任务有一系列参数,其中一些是日期,但(不幸的是,我无法控制)它们作为字符串传递。由于作业可能来自不同的工作站,用于将日期格式化为字符串的实际日期时间格式可能(当然,实际)不同。

四处搜索,我发现的唯一快速解决方案是偷偷更改ShortDateFormat 变量,然后将其恢复为原始值。由于ShortDateFormat 是一个全局变量,并且我在线程环境中工作,唯一可行的方法是同步对它的每次访问,这是完全不可接受的(并且是不可撤销的)。

我可以将库代码从 SysUtils 单元复制到我自己的方法中,并调整它们以使用指定格式而不是全局变量,但我只是想知道那里是否有更充分的东西我错过了。

更新

更简洁地说:

我需要StrToDate(或StrToDateTime)之类的东西,并添加了指定用于将字符串转换为 TDateTime 的确切格式的选项。

【问题讨论】:

标签: delphi delphi-5


【解决方案1】:

改用 VarToDateTime。它支持字符串中的更多日期格式并自动转换它们。

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

我看到您使用的是 Delphi 5。某些版本的 Delphi 需要在 uses 子句中添加 Variants;大多数更高版本都会为您添加它。我不记得 Delphi 5 属于哪个类别。

【讨论】:

  • 谢谢 Ken,我还没有尝试过,但听起来很有希望。任何可以避免滚动我自己的转换(无论是手动还是复制)的方法都绝对是一种选择。
  • 谢谢肯,你也拯救了我的一天!
  • VarToDateTime 适用于大多数地区设置,但对于使用逗号 (,) 作为 DecimalSeparator 和点 (.) 作为 ThousandSeparator 的印度尼西亚等国家/地区则失败
  • 太棒了!它适用于Jul 29, 2019 5:37:55 AM之类的字符串
  • 谢谢,XE3 中使用的是 System.Variants
【解决方案2】:

我为 FreePascal 的 dateutils 单元创建了这样的例程,如果需要移植,它应该很容易移植。

代码:

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

(代码是文件末尾的最后一个(巨大的)过程)

文档:

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

注意,它不是formatdatetime的完全倒数,它有一些扩展:

  • FormatDateTime 的倒数不是 100% 的倒数,仅仅是因为可以将例如格式字符串中的时间标记两次,scandatetime不知道选择哪个时间。

  • 像 hn 这样的字符串不能安全地反转。例如。 1:2(1 后 2 分钟)传递 12 被解析为 12:00 然后 缺少“n”部分的字符。

    • 忽略尾随字符。
    • 不支持东亚格式字符,因为它们只是窗口。
    • 不支持 MBCS。
  • 扩展

    • #9 吃掉空格。
    • 模式末尾的空格是可选的。
    • ?匹配任何字符。
    • 引用上述字符以真正匹配字符。

(我相信这些 cmets 有点过时了,因为后来添加了一些亚洲支持,但我不确定)

【讨论】:

  • 谢谢 :) 我会检查代码。我知道字符串到日期的转换是地狱,说得客气一点,所以我不期望 100% 的成功率,总是缺少一些东西(军事时区吗?;))。只是一个不错的方法,接受日期时间作为字符串和合理的格式。
  • 再次感谢。尽管我没有使用您的方法(选择复制库代码),但您的方法是唯一可行的其他解决方案。我的理由是,如果我要复制与 StrToDate 基本相同的代码,并添加指定格式而不是使用全局变量的选项,我更喜欢它与内置的完全相同,只是没有它使用全局变量。这样对我和我现在的雇主来说,最终结果都是符合他们的环境,并且更具确定性(例如在测试中)。
  • 更新了链接。同时该文件已在 SVN 中移动。
【解决方案3】:

Delphi 的后期版本可以为字符串转换函数提供一个额外的 TFormatSettings 参数。 TFormatSettings 是一个包含各种格式全局变量(ShortDateFormat、LongDateFormat 等)的结构。因此,您可以以线程安全的方式覆盖这些值,甚至可以在一次调用中覆盖。

我不记得这是在哪个版本的 Delphi 中引入的,但我很确定它是在 Delphi 5 之后。

是的,据我所知,您要么需要同步对 ShortDateFormat 的每次访问,要么使用不同的函数。

【讨论】:

  • 这就是我害怕的。 Uwe 说我想要的功能来自 Delphi 7 及更高版本。不幸的是,这超出了我的控制。它将是一个不同的功能:)
【解决方案4】:

这里是函数,它的两个助手,以及所有代码,我写的是使用精确日期时间格式解析字符串:

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;

【讨论】:

    【解决方案5】:

    如果你想知道在后来的 Delphi 中是如何解决这个问题的,你可以在这里查看一个稍微更现代的(看起来像 Delphi 6)sysutils.pas 的源代码:

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

    查看采用TFormatSettings 参数的StrToDateTime 的重载版本。

    function StrToDateTime(const S: string;
      const FormatSettings: TFormatSettings): TDateTime; overload;
    

    【讨论】:

      【解决方案6】:

      使用 RegExpr 库 (https://github.com/masterandrey/TRegExpr)

      var
          RE: TRegExpr;
      
      begin
          RE := TRegExpr.Create;
          try
              RE.Expression := '^(\d\d\d\d)/(\d\d)/(\d\d)T(\d\d):(\d\d):(\d\d)$';
              if RE.Exec( Value ) then
              begin
                  try
                      Result := EncodeDate( StrToInt( RE.Match[1] ),
                                            StrToInt( RE.Match[2] ),
                                            StrToInt( RE.Match[3] ) ) +
                                EncodeTime( StrToInt( RE.Match[4] ),
                                            StrToInt( RE.Match[5] ),
                                            StrToInt( RE.Match[6] ),
                                            0 )
                  except
                      raise EConvertError.Create( 'Invalid date-time: ' + Value )
                  end
              end
              else
                  raise EConvertError.Create( 'Bad format: ' + Value )
          finally
              RE.Free
          end
      end;
      

      【讨论】:

        【解决方案7】:

        我不确定你想要什么。我不再使用 Delphi 5,但我很确定函数 StrToDateTime 存在于其中。使用它,您可以使用格式设置将字符串转换为 TDateTime。然后,您可以使用 FormatDateTime 将此类 TDateTime 转换为任何格式,这样您就可以使用任何您希望的日期格式。

        【讨论】:

        • 是的,它确实存在。但它适用于全局变量:ShortDateFormat 的当前设置。我在给定时间处理多种格式。我需要能够指定用于将字符串转换为 TDateTime 的确切格式。调整全局变量(出于问题中指定的原因)不是一种选择。
        • 问题是Delphi 5!至少从 Delphi 7 开始,有一个重载的 StrToDateTime 接受格式设置,解决了多线程应用程序的问题。这就是使用过时环境的缺点。
        • @Uwe:关于使用过时的环境:更新/升级不是我的任务。我认为最新的 Delphi 版本是在不到一个月前发布的,但从我在现有代码库中看到的情况来看,鉴于 Delphi 的变化,我认为它们永远不会升级......
        • 我能感觉到你。这种方案在野外经常出现。这就是我尝试不断升级的原因。它使该过程变得更加容易。如果您逐个版本地堆积它,那么您将无法再处理它。
        • 就个人而言,我从第 7 版开始就没有使用过 Delphi。我的业务主要围绕 .NET。从那以后,我一直在关注 Delphi 的(相当多的)分期付款,但对我来说,没有购买 Delphi 的商业案例(更不用说 SA)。使用 Delphi 是因为承包商拥有现有的代码库(在本例中为 Delphi 5),而不是因为这是我的选择。这是生意。目前,对我来说,没有购买 Delphi 的商业案例。我喜欢德尔福,如果我的企业可以购买它,我会的。但现在:没有。
        【解决方案8】:

        我会反其道而行之。在我看来,您自己提到的一个选项大约有两种选择

        • 调整 ShortDateFormat 并保持对它的每次访问同步。
        • 如果您知道要接收的字符串的格式(不知何故,您必须知道),只需进行一些字符串处理,首先将字符串转换为当前的短日期格式。之后,将(杂耍)字符串转换为TDateTime

        我确实想知道您将如何确定 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.
        

        【讨论】:

        • 调整日期时间参数以不再作为字符串传递将是不可撤销的。但是,添加一个指定工作站上使用的 ShortDateFormat 的附加参数将是微不足道的。这就是为什么我要寻找类似 StrToDate(strDate: string; format: string) :)
        • 我或你误会了。解决方案 的关键是 将日期时间作为字符串传递。在 您的 工作站上,您修复传递的字符串以匹配您在工作站上使用的格式。之后,只需在您的工作站上调用 StrToDate。
        • 我们处于同一水平。我无法更改作为字符串传递的日期。我可以添加一个额外的参数来指定使用的格式。我在 Delphi 5 中没有的是接受格式参数的 StrToDate 方法。我必须编写自己的代码(或者,浏览库代码,复制并调整它):您的选项二(以及我自己的选项 1,如果没有其他内容可以满足我的需求)。
        • 说清楚(毕竟是星期五),其他人传递给您的工作站的值是 TDateTime 吗?!如果不是,那是你误会了
        • 没有。我收到的值是一个字符串。并且(到目前为止,还不错,希望现在最好)它将是一个使用 ShortDateFormat 格式化的字符串。在这种情况下,Delphi 5 中的 StrToDate 是无用的,因为它将使用当前用户的 ShortDateFormat 转换字符串。对于工作站 X,格式为“M-dd-yyyy”,对于工作站 Y,格式为“dd/MM/yyyy”,对于工作站 Z,格式为“MMM/d/y”。有很多选择。将字符串转换为日期非常困难,这就是我在使用自定义方法之前寻找内置选项的原因。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-12-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-01
        • 2018-08-30
        相关资源
        最近更新 更多