Browse Source

--- Merging r14854 into '.':
U rtl/objpas/sysutils/dati.inc
A tests/test/units/sysutils/strtotimetest.pp

# revisions: 14854
------------------------------------------------------------------------
r14854 | marco | 2010-02-03 16:10:14 +0100 (Wed, 03 Feb 2010) | 2 lines
Changed paths:
M /trunk/rtl/objpas/sysutils/dati.inc
A /trunk/tests/test/units/sysutils/strtotimetest.pp

* New more delphi compat intstrtotime by Bart Broersma #15505 + test

------------------------------------------------------------------------

git-svn-id: branches/fixes_2_4@14890 -

marco 15 years ago
parent
commit
b708f9e349
3 changed files with 303 additions and 66 deletions
  1. 1 0
      .gitattributes
  2. 143 66
      rtl/objpas/sysutils/dati.inc
  3. 159 0
      tests/test/units/sysutils/strtotimetest.pp

+ 1 - 0
.gitattributes

@@ -8716,6 +8716,7 @@ tests/test/units/system/tval3.pp svneol=native#text/plain
 tests/test/units/system/tval4.pp svneol=native#text/plain
 tests/test/units/system/tval5.pp svneol=native#text/plain
 tests/test/units/system/tvalc.pp svneol=native#text/plain
+tests/test/units/sysutils/strtotimetest.pp svneol=native#text/plain
 tests/test/units/sysutils/tastrcmp.pp svneol=native#text/plain
 tests/test/units/sysutils/tastrcmp1.pp svneol=native#text/plain
 tests/test/units/sysutils/texec1.pp svneol=native#text/plain

+ 143 - 66
rtl/objpas/sysutils/dati.inc

@@ -533,92 +533,169 @@ end;
     if S does not represent a valid time value an
     EConvertError will be raised   }
 
+
 function IntStrToTime(Out ErrorMsg : AnsiString; const S: PChar; Len : integer;const defs:TFormatSettings; separator : char = #0): TDateTime;
+const
+  AMPM_None = 0;
+  AMPM_AM = 1;
+  AMPM_PM = 2;
+  tiHour = 0;
+  tiMin = 1;
+  tiSec = 2;
+  tiMSec = 3;
+type
+  TTimeValues = array[tiHour..tiMSec] of Word;
 var
-   Current: integer; PM: integer;
+   AmPm: integer;
+   TimeValues: TTimeValues;
 
     function StrPas(Src : PChar; len: integer = 0) : ShortString;
-    var
-       tmp : integer;
     begin
-        {tmp := IndexChar(Src[0], len, #0);
-        len :=ifthen(tmp >= 0, tmp, len);
-        len :=ifthen(len > 255, 255, len);}
+        //this is unsafe for len > 255, it will trash memory (I tested this)
+        //reducing it is safe, since whenever we use this a string > 255 is invalid anyway
+        if len > 255 then len := 255;
         SetLength(Result, len);
         move(src[0],result[1],len);
     end;
 
-   function GetElement: integer;
+   function SplitElements(out TimeValues: TTimeValues; out AmPm: Integer): Boolean;
+   //Strict version. It does not allow #32 as Separator, it will treat it as whitespace always
+   const
+     Digits = ['0'..'9'];
    var
-     j, c: integer;
-     CurrentChar : Char;
+      Cur, Offset, ElemLen, Err, TimeIndex, FirstSignificantDigit: Integer;
+      Value: Word;
+      DigitPending, MSecPending: Boolean;
+      AmPmStr: ShortString;
+      CurChar: Char;
    begin
-   result := -1;
-   while (result = -1) and (Current < Len) do
+     Result := False;
+     AmPm := AMPM_None; //No Am or PM in string found yet
+     MSecPending := False;
+     TimeIndex := 0; //indicating which TTimeValue must be filled next
+     FillChar(TimeValues, SizeOf(TTimeValues), 0);
+     Cur := 0;
+     //skip leading blanks
+     While (Cur < Len) and (S[Cur] =#32) do Inc(Cur);
+     Offset := Cur;
+     //First non-blank cannot be Separator or DecimalSeparator
+     if (Cur > Len - 1) or (S[Cur] = Separator) or (S[Cur] = defs.Decimalseparator) then Exit;
+     DigitPending := (S[Cur] in Digits);
+     While (Cur < Len) do
      begin
-       CurrentChar := S[Current];
-       if CurrentChar in ['0'..'9'] then
-          begin
-            j := Current;
-            while (Current+1 < Len) and (s[Current + 1] in ['0'..'9']) do
-              Inc(Current);
-            val(StrPas(S+j, 1 + current - j), result, c);
-          end
-       else if ((defs.TimeAMString<>'') and (CurrentChar = defs.TimeAMString[1])) or (S[Current] in ['a', 'A']) then
-          begin
-            pm:=1;
-            Current := 1 + Len;
-          end
-       else if ((defs.TimePMString<>'') and (CurrentChar = defs.TimePMString[1])) or (S[Current] in ['p', 'P']) then
+       //writeln;
+       //writeln('Main While loop:  Cur = ',Cur,' S[Cur] = "',S[Cur],'" Len = ',Len);
+       CurChar := S[Cur];
+       if CurChar in Digits then
+       begin//Digits
+         //HH, MM, SS, or Msec?
+         //writeln('Digit');
+         //Digits are only allowed after starting Am/PM or at beginning of string or after Separator
+         //and TimeIndex must be <= tiMSec
+         //Uncomment "or (#32 = Separator)" and it will allllow #32 as separator
+         if (not (DigitPending {or (#32 = Separator)})) or (TimeIndex > tiMSec) then Exit;
+         OffSet := Cur;
+         if (CurChar <> '0') then FirstSignificantDigit := OffSet else FirstSignificantDigit := -1;
+         while (Cur < Len -1) and (S[Cur + 1] in Digits) do
+         begin
+           //Mark first Digit that is not '0'
+           if (FirstSignificantDigit = -1) and (S[Cur] <> '0') then FirstSignificantDigit := Cur;
+           Inc(Cur);
+         end;
+         if (FirstSignificantDigit = -1) then FirstSignificantDigit := Cur;
+         ElemLen := 1 + Cur - FirstSignificantDigit;
+         //writeln('  S[FirstSignificantDigit] = ',S[FirstSignificantDigit], ' S[Cur] = ',S[Cur],' ElemLen = ',ElemLen,' -> ', StrPas(S + Offset, ElemLen));
+         //writeln('  Cur = ',Cur);
+         //this way we know that Val() will never overflow Value !
+         if (ElemLen <= 2) or ((ElemLen <= 3) and (TimeIndex = tiMSec) ) then
          begin
-           Current := 1 + Len;
-           PM := 2;
+           Val(StrPas(S + FirstSignificantDigit, ElemLen), Value, Err);
+           //writeln('  Value = ',Value,' HH = ',TimeValues[0],' MM = ',TimeValues[1],' SS = ',TimeValues[2],' MSec = ',Timevalues[3]);
+           //This is safe now, because we know Value < High(Word)
+           TimeValues[TimeIndex] := Value;
+           Inc(TimeIndex);
+           DigitPending := False;
          end
-      else if (CurrentChar = Separator) or (CurrentChar = ' ') then
-         Inc(Current)
-      else
-        ErrorMsg:=Format(SErrInvalidTimeFormat,[StrPas(S)]);
-      end ;
-   end ;
-
-var
-   i: integer;
-   TimeValues: array[0..4] of integer;
+         else  Exit; //Value to big, so it must be a wrong timestring
+       end//Digits
+       else if (CurChar = #32) then
+       begin
+         //writeln('#32');
+         //just skip, but we must adress this, or it will be parsed by either AM/PM or Separator
+       end
+       else if (CurChar = Separator) then
+       begin
+         //writeln('Separator');
+         if DigitPending or (TimeIndex > tiSec) then Exit;
+         DigitPending := True;
+         MSecPending := False;
+       end
+       else if (CurChar = defs.DecimalSeparator) then
+       begin
+         //writeln('DecimalSeparator');
+         if DigitPending or MSecPending or (TimeIndex <> tiMSec) then Exit;
+         DigitPending := True;
+         MSecPending := True;
+       end
+       else
+       begin//AM/PM?
+         //None of the above, so this char _must_ be the start of AM/PM string
+         //If we already have found AM/PM or we expect a digit then then timestring must be wrong at this point
+         //writeln('AM/PM?');
+         if (AmPm <> AMPM_None) or DigitPending then Exit;
+         OffSet := Cur;
+         while (Cur < Len -1) and (not (S[Cur + 1] in [Separator, #32, defs.DecimalSeparator]))
+           and not (S[Cur + 1] in Digits) do Inc(Cur);
+         ElemLen := 1 + Cur - OffSet;
+         //writeln('  S[Offset] = ',S[Offset], ' S[Cur] = ',S[Cur],' ElemLen = ',ElemLen,' -> ', StrPas(S + Offset, ElemLen));
+         //writeln('  Cur = ',Cur);
+         AmPmStr := StrPas(S + OffSet, ElemLen);
+
+         //writeln('AmPmStr = ',ampmstr,' (',length(ampmstr),')');
+         //We must compare to TimeAMString before hardcoded 'AM' for delphi compatibility
+         //Also it is perfectly legal, though insane to have TimeAMString = 'PM' and vice versa
+         if (AnsiCompareText(AmPmStr, defs.TimeAMString) = 0) then AmPm := AMPM_AM
+         else if (AnsiCompareText(AmPmStr, defs.TimePMString) = 0) then AmPm := AMPM_PM
+         else if (CompareText(AmPmStr, 'AM') = 0) then AmPm := AMPM_AM
+         else if (CompareText(AmPmStr, 'PM') = 0) then AmPm := AMPM_PM
+         else Exit; //If text does not match any of these, timestring must be wrong;
+         //if AM/PM is at beginning of string, then a digit is mandatory after it
+         if (TimeIndex = tiHour) then
+         begin
+           DigitPending := True;
+         end
+         //otherwise, no more TimeValues allowed after this
+         else
+         begin
+           TimeIndex := tiMSec + 1;
+           DigitPending := False;
+         end;
+       end;//AM/PM
+       Inc(Cur)
+     end;//while
+
+     //If we arrive here, parsing the elements has been successfull
+     //if not at least Hours specified then input is not valid
+     //when am/pm is specified Hour must be <= 12 and not 0
+     if (TimeIndex = tiHour) or ((AmPm <> AMPM_None) and ((TimeValues[tiHour] > 12) or (TimeValues[tiHour] = 0))) or DigitPending then Exit;
+     Result := True;
+   end;
 
 begin
   if separator = #0 then
         separator := defs.TimeSeparator;
-  Current := 0;
-  PM := 0;
-  for i:=0 to 4 do
-    timevalues[i]:=0;
-  i := 0;
-  TimeValues[i] := GetElement;
-  If ErrorMsg<>'' then 
+  AmPm := AMPM_None;
+  if not SplitElements(TimeValues, AmPm) then
+  begin
+    ErrorMsg:=Format(SErrInvalidTimeFormat,[StrPas(S, Len)]);
     Exit;
-  while (i < 5) and (TimeValues[i] <> -1) do 
-    begin
-     i := i + 1;
-     Inc(Current);
-     TimeValues[i] := GetElement;
-     If ErrorMsg<>'' then
-        Exit;
-   end ;
-  If (i<5) and (TimeValues[I]=-1) then
-    TimeValues[I]:=0;
-  if PM=2 then
-    begin
-     if (TimeValues[0] <> 12) then
-       Inc(TimeValues[0], 12);
-    end
-  else
-    begin
-      if (pm=1) and ((TimeValues[0]=12)) then
-        TimeValues[0]:=0;
-    end;
+  end;
+  if (AmPm=AMPM_PM) and (TimeValues[tiHour]<>12) then Inc(TimeValues[tiHour], 12)
+  else if (AmPm=AMPM_AM) and (TimeValues[tiHour]=12) then TimeValues[tiHour]:=0;
 
-  if not TryEncodeTime(TimeValues[0], TimeValues[1], TimeValues[2], TimeValues[3],result) Then
-    errormsg:='Invalid time.';
+  if not TryEncodeTime(TimeValues[tiHour], TimeValues[tiMin], TimeValues[tiSec], TimeValues[tiMSec], result) Then
+    //errormsg:='Invalid time.';
+    ErrorMsg:=Format(SErrInvalidTimeFormat,[StrPas(S, Len)]);
 end ;
 
 function StrToTime(const S: PChar; Len : integer; separator : char = #0): TDateTime;

+ 159 - 0
tests/test/units/sysutils/strtotimetest.pp

@@ -0,0 +1,159 @@
+program strtmtest;
+
+{$ifdef FPC}
+  {$mode objfpc}{$H+}
+{$else}
+  {$apptype console}
+{$endif}
+
+uses sysutils,sysconst{$ifndef fpc},windows{$endif};
+
+{$ifndef fpc}
+function defaultformatsettings:TFormatSettings;
+begin
+  GetLocaleFormatSettings(getsystemdefaultlcid,result);
+end;
+{$endif}
+var exitwitherror:integer =0;
+    fmt : TFormatSettings;
+
+Procedure Check(TestNo : Integer; inputstr : String;shouldfailstrtotime:boolean=false;shouldfailcomparison:boolean=false;resultstr:string='');
+
+var dt :TDateTime;
+    outputstr:ansistring;
+
+begin
+  if TryStrToTime(inputstr,dt,fmt) then
+   begin
+     if shouldfailstrtotime then
+       begin
+         writeln('test ',TestNo,' should fail on strtotime while it didn''t ',timetostr(dt,fmt));
+         exitwitherror:=1;
+       end
+     else
+       begin
+         outputstr:=TimeToStr(dt,fmt); // note because of this bugs can also be in timetostr
+         if resultstr<>'' then
+            begin
+              if outputstr<>resultstr then
+                begin
+                  writeln('test ',TestNo,' should be "',resultstr,'" is "',outputstr,'"');
+                  exitwitherror:=1;
+                end;
+              exit; // don't do other comparisons
+            end;
+
+         if inputstr<>outputstr then
+           begin
+            if not shouldfailcomparison then
+              begin
+                writeln('test ',TestNo,' failed "',inputstr,'" <> "',outputstr,'"');
+                exitwitherror:=1;
+              end;
+           end
+         else
+           begin
+            if shouldfailcomparison then
+              begin
+                writeln('test ',TestNo,' succeeded "',inputstr,'" = "',outputstr,'", while it shouldn''t');
+                exitwitherror:=1;
+              end;
+           end;
+       end;
+   end
+  else
+    if not shouldfailstrtotime then
+     begin
+       Writeln('Test ',TestNo,' failed: ',inputstr);
+       exitwitherror:=1;
+    end;
+end;
+
+procedure setdecimalsep(c:char);
+begin
+  fmt.DecimalSeparator:=c;
+  fmt.longtimeformat:='hh:nn:ss'+fmt.DecimalSeparator+'zzz';
+end;
+
+var value: word;
+  code : longint;
+begin
+  fmt:=defaultformatsettings;
+  fmt.TimeSeparator:=':';
+  fmt.TimeAmstring:='AM';
+  fmt.TimePmstring:='PM';
+
+  setdecimalsep('.');
+  Check( 0,'12:34:45.789',false,false);
+  Check( 1,'12:34:45,789',true,false);
+
+  setdecimalsep(',');
+  Check( 2,'12:34:45.789',true,false);
+  Check( 3,'12:34:45,789',false,false);
+
+  Check( 4,'12 am',false,false,'00:00:00,000');
+  Check( 5,'pm 12:34',false,false,'12:34:00,000');
+  Check( 6,'12::45',true,false);
+  Check( 7,'12:34:56 px',true,false);
+  Check( 8,'12:34:5x',true,false);
+  Check( 9,'12:34:56:78:90',true,false);
+  Check(10,'5 am',false,false,'05:00:00,000');
+  Check(11,'5 pm',false,false,'17:00:00,000');
+  Check(12,'am 5',false,false,'05:00:00,000');
+  Check(13,'pm 5',false,false,'17:00:00,000');
+  fmt.longtimeformat:='hh:nn:ss'+fmt.DecimalSeparator+'zzz am/pm';
+  Check(14,'5 am',false,false,'05:00:00,000 am');
+  Check(15,'5 pm',false,false,'05:00:00,000 pm');
+  Check(16,'am 5',false,false,'05:00:00,000 am');
+  Check(17,'pm 5',false,false,'05:00:00,000 pm');
+  fmt.TimeAmstring:='AM';
+  fmt.TimePmstring:='PM';
+  fmt.longtimeformat:='hh:nn:ss'+fmt.DecimalSeparator+'zzz a/p';
+  Check(18,'am 5',false,false,'05:00:00,000 a');
+  Check(19,'pm 5',false,false,'05:00:00,000 p');
+
+  fmt.TimeAMString:='a'; fmt.TimePMString:='p';
+
+  Check(20,'a 5',false,false,'05:00:00,000 a');
+  Check(21,'p 5',false,false,'05:00:00,000 p');
+  Check(22,'12:',True,false);
+  Check(23,'13:14:',True,false);
+  Check(24,'a 17:00',True,false);
+  Check(25,'p 19:00',True,false);
+  Check(26,'1:2:3',false,false,'01:02:03,000 a');
+  Check(27,'1:4',false,false,'01:04:00,000 a');
+  Check(28,'111:2:3',True,false);
+  Check(29,'1:444',True,false);
+  Check(30,'1:2:333',True,false);
+  Check(31,'1:4:55,4',False,false,'01:04:55,004 a');
+  Check(32,'1:4:55,12',False,false,'01:04:55,012 a');
+  Check(33,'1:4:55,004',False,false,'01:04:55,004 a');
+  Check(34,'1:4:55,0012',False,false,'01:04:55,012 a');
+  Check(35,'1:4:55,004'#9'am',true,false,'01:04:55,004'#9'am');
+  Check(36,#9'1:4:55,0012',true,false,'01:04:55,012 a');
+  Check(37,' 1:4:55,4',False,false,'01:04:55,004 a');
+  Check(38,'1: 4:55,12',False,false,'01:04:55,012 a');
+  Check(39,'1:4: 55,004',False,false,'01:04:55,004 a');
+  Check(40,'1:4:55, 2',False,false,'01:04:55,002 a');
+  Check(41,'1:4:55,   4',False,false,'01:04:55,004 a'); // note more padding then needed
+  Check(42,'1:    4:55,   4',False,false,'01:04:55,004 a'); // note more padding then needed
+  Check(43,'1:  4:   55,   4',False,false,'01:04:55,004 a'); // note more padding then needed
+  Check(44,'1:  4:  55,   4',False,false,'01:04:55,004 a'); // note more padding then needed
+  Check(45,'1 4 55 4',True,false);
+  fmt.timeseparator:=' ';
+  Check(46,'01 04 55',True,false);
+  Check(47,'a 01',false,false,'01 00 00,000 a');
+  Check(52,'a01',false,false,'01 00 00,000 a');
+  fmt.TimeSeparator:=':';
+  Check(48,'1:4:55,0000000000000000000000012',false,false,'01:04:55,012 a');
+  Check(49,'1:4:55,0000100012',True,false);
+  Check(50,'1:4:55,000001012',True,false);
+  Check(51,'12:034:00056',false,false,'12:34:56,000 p');
+
+  exitcode:=exitwitherror;
+
+ {$ifndef fpc} // halt in delphi ide  
+  readln;
+ {$endif}
+end.
+