Possible issue and fix with fractional seconds and AsString formatting.

The offical for ZeosLib 7.3 Report problems, ask for help, post proposals for the new version of Zeoslib 7.3/v8
Quick Info:
-We made two new drivers: odbc(raw and unicode version) and oledb
-GUID domain/field-defined support for FB
-extended error infos of Firebird
-performance ups are still in queue
In future some more feature will arrive, so stay tuned and don't hassitate to help
Post Reply
MJFShark
Expert Boarder
Expert Boarder
Posts: 211
Joined: 04.06.2020, 13:59

Possible issue and fix with fractional seconds and AsString formatting.

Post 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
marsupilami
Platinum Boarder
Platinum Boarder
Posts: 1918
Joined: 17.01.2011, 14:17

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

Post 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?
MJFShark
Expert Boarder
Expert Boarder
Posts: 211
Joined: 04.06.2020, 13:59

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

Post by MJFShark »

Will do! I may be changing both, so I'll post them in user patches later today. Thanks.

-Mark
Post Reply