Page 1 of 1

Possible issue and fix with fractional seconds and AsString formatting.

Posted: 14.11.2020, 22:03
by MJFShark
Hi all!

I was initially going to post this in user patches or do a ticket, but I realized that I'm not positive that this is a bug, it could easily be something I'm misunderstanding about how things are supposed to work. Also my patched method below is definitely not fully tested.

I believe that when using DateTime types with a scale (I'm testing with MySQL DateTime(n) and Oracle TimeStamp(n) fields btw) that TDateTime.AsString is supposed to be changing the format to add the fractional seconds based on the scale and the AdjustSecondFractionsFormat property which defaults to true. I believe that this does not work in the current 8 version (I started using Zeoslib on 7.3 alpha/8 so I can't say if it worked previously.)

The easiest way I've found to test this is using the following MySQL query:

Code: Select all

select now(), now(3), now(6), date_format(now(6), '%Y-%m-%d %h:%i:%s.%f') ServerFormat;
The original results for this query wouldn't show the fractional time (except for the serverside formatted final field.) The fixed one looks like this:

Code: Select all

now()                 now(3)                    now(6)                       ServerFormat                  
--------------------- ------------------------- ---------------------------- ----------------------------- 
2020-11-14 9:01:56 PM 2020-11-14 9:01:56.611 PM 2020-11-14 9:01:56.611936 PM 2020-11-14 09:01:56.611936 PM 
And here's my modified version with each of my changes marked with "mjf:"

Code: Select all

procedure TZDateTimeField.GetText(var Text: string; DisplayText: Boolean);
var
  Frmt: string;
  DT, D: TDateTime;
  Delim: Char;
  TS: TZTimeStamp;
  I,J: LengthInt;
  Fraction: Cardinal;
  B: Boolean;
  B2: Boolean;  // mjf: Added as it appears that "B" was reused half way through the procedure for something else.
  P: PChar;
  Millis: Word;
begin
  if FilledValueWasNull(TS)
  then Text := ''
  else begin
    B := DisplayText and (DisplayFormat <> '');
    if B
    then Frmt := DisplayFormat
    else begin //improve the "C" token of FormatDateTime
      if FindFirstDateFormatDelimiter({$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}ShortDateFormat, Delim) and
         (Delim <> {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}DateSeparator)
      then Frmt := ZSysUtils.ReplaceChar(Delim, {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}DateSeparator, {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}ShortDateFormat)
      else Frmt := {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}ShortDateFormat;
      if (FAdjSecFracFmt and (FScale > 0) and (TS.Fractions > 0) ) or
         (TS.Hour <> 0) or (TS.Minute <> 0) or (TS.Second <> 0) then begin
        Frmt := Frmt + ' ';
        if FindFirstTimeFormatDelimiter({$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}LongTimeFormat, Delim) and
           (Delim <> {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}TimeSeparator)
        then Frmt := Frmt + ZSysUtils.ReplaceChar(Delim, {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}TimeSeparator, {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}LongTimeFormat)
        else Frmt := Frmt + {$IFDEF WITH_FORMATSETTINGS}FormatSettings.{$ENDIF}LongTimeFormat;
      end;
    end;
    if Frmt <> FLastFormat[B] then begin
      FLastFormat[B] := Frmt;
      FSimpleFormat[b] := IsSimpleDateTimeFormat(Frmt);
      if FAdjSecFracFmt and (FScale > 0)   // mjf: added the FScale > 0 check.
      then FFractionFormat[b] := ConvertAsFractionFormat(Frmt, FScale, not FSimpleFormat[b], FFractionLen[b])
      else FFractionFormat[b] := Frmt;
    end;
    if FSimpleFormat[b] then begin
      P := @FBuff[0];
      Fraction := ts.Fractions;
      if not FAdjSecFracFmt then
        Fraction := RoundNanoFractionTo(Fraction, FScale);
      // mjf: The original used FLastFormat[B], but I think it's supposed to use FFractionFormat[B].
      //      I := {$IFDEF UNICODE}DateTimeToUni{$ELSE}DateTimeToRaw{$ENDIF}(
      //        TS.Year, TS.Month, TS.Day, TS.Hour, TS.Minute,
      //        TS.Second, Fraction, P, FLastFormat[B], False, TS.IsNegative);
      I := {$IFDEF UNICODE}DateTimeToUni{$ELSE}DateTimeToRaw{$ENDIF}(
        TS.Year, TS.Month, TS.Day, TS.Hour, TS.Minute,
        TS.Second, Fraction, P, FFractionFormat[B], False, TS.IsNegative);
      System.SetString(Text, P, I);
    end else begin
      // mjf: This section changed "B" and I think it may just be a bug.  I changed "B" to "B2" for this section
      B2 := False;
      if TryEncodeDate(TS.Year, TS.Month, TS.Day, d) then begin
        if FAdjSecFracFmt
        then Millis := 0
        else Millis := RoundNanoFractionToMillis(TS.Fractions);
        B2 := TryEncodeTime(TS.Hour, TS.Minute, TS.Second, Millis, DT);
        if B2 then
          if d < 0
          then DT := D - DT
          else DT := D + DT;
      end;
      if B2 then begin  // mjf: End of "B" to "B2" change
       //let the compiler do the complex stuff i.e. AM/PM and user defined additional tokens, week days etc.
        DateTimeToString(Text, FFractionFormat[b], DT);
        if FAdjSecFracFmt then begin
          //if shortformat the position may be variable. no chance to cache that info
          I := ZFastCode.Pos(MilliReplaceUnQuoted[FScale], Text);
          if I > 0 then begin
            P := Pointer(Text);
            Inc(P, I-1);
            Fraction := ts.Fractions;
            Fraction := RoundNanoFractionTo(Fraction, FScale);
            Fraction := Fraction div FractionLength2NanoSecondMulTable[FScale];
            {$IFDEF UNICODE}IntToUnicode{$ELSE}IntToRaw{$ENDIF}(Fraction, P, Byte(FScale));
            if FScale > FFractionLen[B] then begin
              J := I+FScale;
              P := Pointer(Text);
              Inc(P, j-2);
              Millis := 0;
              while (J>I) and (P^ = ('0')) do begin
                Inc(Millis);
                Dec(J);
                Dec(P);
              end;
              if Millis > 0 then
                Delete(Text, J, Millis);
            end;
          end;
        end;
      end else begin
        if DisplayText
        then Text := FInvalidText
        else Text := '';
        Exit;
      end;
      if TS.IsNegative then
        Text := '-'+Text;
    end;
  end;
end;
I hope this is helpful! I'd love to hear any thoughts.

-Mark

Re: Possible issue and fix with fractional seconds and AsString formatting.

Posted: 15.11.2020, 12:56
by marsupilami
I think you are correct. I will apply your patch later on. Could you prepare a similar patch for TZTimeField.GetText? I assume it is the same there?

Re: Possible issue and fix with fractional seconds and AsString formatting.

Posted: 15.11.2020, 14:40
by MJFShark
Will do! I may be changing both, so I'll post them in user patches later today. Thanks.

-Mark