Recent

Author Topic: (Milliseconds, Minutes, etc.)Between Error: Invalid floating point operation  (Read 685 times)

Dio Affriza

  • Jr. Member
  • **
  • Posts: 87
I'm writing code "minutesbetween(Now, StartTime)" for Linux, it creates an error: Invalid floating point operation.
But it works fine in Windows.

I'm using FPC Fixes 3.0.5 cross-compile in Windows with FPCUpDeluxe.
Distro: Ubuntu Server 18.04.2 LTS

I think there's need something to do with TDateTimeEpsilon?
I just tried Abs(TDateTimeEpsilon) it cause the same error.
« Last Edit: September 16, 2019, 10:03:24 am by Dio Affriza »

Thaddy

  • Hero Member
  • *****
  • Posts: 9193
Include DateUtils. (Also in modern Delphi!!!)
Code: Pascal  [Select]
  1. {$mode delphi}{$H+}
  2. uses dateutils;
  3. begin
  4.   writeln(minutesbetween(0,99));
  5. end.

« Last Edit: September 16, 2019, 12:23:43 pm by Thaddy »
also related to equus asinus.

Dio Affriza

  • Jr. Member
  • **
  • Posts: 87
I've included that already. Try compare using TDateTime. Is that delphi mode affecting the error?

Thaddy

  • Hero Member
  • *****
  • Posts: 9193
No. That code works since before the year zero.
Why doesn't it work for you?
Show us where it goes wrong...In your code...
Has nothing to do with modes..

(My code is a full program)

Note that when comparing to TDatetime you have to be careful not to make any in between conversions, which is what you are trying to do.
A TDateTime is an alias  for double https://www.freepascal.org/docs-html/rtl/system/tdatetime.html
Don't believe the wiki: that is really incorrect. I wrote a warning (for now), but I will delete that entry and start all over again.

« Last Edit: September 16, 2019, 05:19:09 pm by Thaddy »
also related to equus asinus.

Dio Affriza

  • Jr. Member
  • **
  • Posts: 87
It's just a normal code.
Code: Pascal  [Select]
  1. procedure TControlMain.OpenSQL;
  2. var
  3.   JSONFinal: TJSONObject;
  4.   jObject: TJSONObject;
  5.   rows: TJSONArray;
  6.   fields: TJSONArray;
  7.   i: integer;
  8.   StartTime: TDateTime;
  9. begin
  10.   StartTime := Now;
  11.   JSONFinal := TJSONObject.Create;
  12.   rows := TJSONArray.Create;
  13.   try
  14.     KySQL.Connect(_post['hostname'], _post['uname'], _post['passwd'],
  15.       _post['DatabaseName']);
  16.     KySQL.SQL := _post['sqlstr'];
  17.     KySQL.Open;
  18.  
  19.     while not KySQL.EOF do
  20.     begin
  21.  
  22.       fields := TJSONArray.Create;
  23.       for i := 0 to KySQL.SQLQuery.Fields.Count -1 do
  24.       begin
  25.         jObject := TJSONObject.Create;
  26.         jObject.Add('name', KySQL.SQLQuery.Fields[i].FieldName);
  27.  
  28.         case KySQL.SQLQuery.Fields[i].DataType of
  29.           ftString:
  30.           begin
  31.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsString);
  32.             jObject.Add('type', 'string');
  33.           end;
  34.           ftSmallint:
  35.           begin
  36.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsInteger);
  37.             jObject.Add('type', 'integer');
  38.           end;
  39.           ftInteger:
  40.           begin
  41.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsInteger);
  42.             jObject.Add('type', 'integer');
  43.           end;
  44.           ftBoolean:
  45.           begin
  46.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsBoolean);
  47.             jObject.Add('type', 'boolean');
  48.           end;
  49.           ftFloat:
  50.           begin
  51.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsFloat);
  52.             jObject.Add('type', 'float');
  53.           end;
  54.           ftCurrency:
  55.           begin
  56.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsCurrency);
  57.             jObject.Add('type', 'currency');
  58.           end;
  59.           ftDate:
  60.           begin
  61.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsDateTime);
  62.             jObject.Add('type', 'date');
  63.           end;
  64.           ftTime:
  65.           begin
  66.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsDateTime);
  67.             jObject.Add('type', 'time');
  68.           end;
  69.           ftDateTime:
  70.           begin
  71.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsDateTime);
  72.             jObject.Add('type', 'datetime');
  73.           end;
  74.           ftAutoInc:
  75.           begin
  76.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsInteger);
  77.             jObject.Add('type', 'integer');
  78.           end;
  79.           else
  80.           begin
  81.             jObject.Add('value', KySQL.SQLQuery.Fields[i].AsString);
  82.             jObject.Add('type', 'unknown');
  83.           end;
  84.         end;
  85.         fields.Add(jObject);
  86.       end;
  87.       rows.Add(fields);
  88.       KySQL.Next;
  89.     end;
  90.     KySQL.Close;
  91.     JSONFinal.Add('rows', rows);
  92.     JSONFinal.Add('result', 'OK');
  93.   except
  94.     on e: exception do
  95.     begin
  96.       JSONFinal.Add('result', 'ERROR');
  97.       JSONFinal.Add('msg', e.Message);
  98.       JSONFinal.Add('serve_time', IntToStr(MillisecondsBetween(Now, StartTime)) + ' ms');
  99.     end;
  100.  
  101.   end;
  102.   echo(JSONFinal.FormatJSON);
  103. end;
  104.  
Other code implementation: https://github.com/afuriza/kyoukai_framework/blob/development/src/units/standard/kyoukai.standard.defaulthtml.pas
This is the demo:
http://103.56.205.179/mysqljson/opensql
« Last Edit: September 17, 2019, 03:48:29 am by Dio Affriza »

Dio Affriza

  • Jr. Member
  • **
  • Posts: 87
I'm trying to reproduce the error by doing this:
Code: Pascal  [Select]
  1. Function DateTimeDiff(const ANow, AThen: TDateTime): TDateTime;
  2. begin
  3.   Result:= ANow - AThen;
  4.   if (ANow>0) and (AThen<0) then
  5.     Result:=Result-0.5
  6.   else if (ANow<-1.0) and (AThen>-1.0) then
  7.     Result:=Result+0.5;
  8. end;
  9.  
  10. procedure TControlMain.DateTimeTest;
  11. const
  12.    HoursPerDay = 24;
  13.    MinsPerHour = 60;
  14.    SecsPerMin  = 60;
  15.    MSecsPerSec = 1000;
  16.    MinsPerDay  = HoursPerDay * MinsPerHour;
  17.    SecsPerDay  = MinsPerDay * SecsPerMin;
  18.    MSecsPerDay = SecsPerDay * MSecsPerSec;
  19.    TDateTimeEpsilon = 2.2204460493e-16;
  20. var
  21.   testf: double;
  22. begin
  23.   // this is works ok
  24.   testf := Abs(DateTimeDiff(Now,Now))*MSecsPerDay;
  25.   // error invalid operation floating point
  26.   testf := DateTimeDiff(Now,Now)+TDateTimeEpsilon;
  27.   // error invalid operation floating point
  28.   testf := TDateTimeEpsilon;
  29.  
  30.   echo(floattostr(testf));
  31. end;
  32.  

I borrow TDateTimeEpsilon from the dateutil.inc line 443.
What the hell TDateTimeEpsilon actually is? :o Sorry, may my lack of knowledge. :-[
« Last Edit: September 17, 2019, 04:52:31 am by Dio Affriza »

Dio Affriza

  • Jr. Member
  • **
  • Posts: 87
For now, I removed the TDateTimeEpsilon. I'm not sure what the impact of removing that, but it still looks pretty accurate.

Zoran

  • Hero Member
  • *****
  • Posts: 1465
    • http://wiki.lazarus.freepascal.org/User:Zoran

This just works for me:

Code: Pascal  [Select]
  1. program Project1;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. uses
  6.   Classes, SysUtils
  7.   ;
  8.  
  9. function DateTimeDiff(const ANow, AThen: TDateTime): TDateTime;
  10. begin
  11.   Result:= ANow - AThen;
  12.   if (ANow>0) and (AThen<0) then
  13.     Result:=Result-0.5
  14.   else if (ANow<-1.0) and (AThen>-1.0) then
  15.     Result:=Result+0.5;
  16. end;
  17.  
  18. procedure DateTimeTest;
  19. const
  20.    HoursPerDay = 24;
  21.    MinsPerHour = 60;
  22.    SecsPerMin  = 60;
  23.    MSecsPerSec = 1000;
  24.    MinsPerDay  = HoursPerDay * MinsPerHour;
  25.    SecsPerDay  = MinsPerDay * SecsPerMin;
  26.    MSecsPerDay = SecsPerDay * MSecsPerSec;
  27.    TDateTimeEpsilon = 2.2204460493e-16;
  28. var
  29.   testf: double;
  30. begin
  31.   // this is works ok
  32.   testf := Abs(DateTimeDiff(Now,Now))*MSecsPerDay;
  33.   // error invalid operation floating point
  34.   testf := DateTimeDiff(Now,Now)+TDateTimeEpsilon;
  35.   // error invalid operation floating point
  36.   testf := TDateTimeEpsilon;
  37.  
  38.   // I don' t know what "echo" is, does not compile for me.
  39.   //echo(floattostr(testf));
  40.   WriteLn(FloatToStr(Testf));
  41. end;
  42.  
  43. begin
  44.   DateTimeTest;
  45. end.
  46.  

Gives this output:
Quote
2.2204460493E-16

Tested with FPC 3.0.4, Linux Mint 19.2.

There must be something else in your application which affects the behaviour you experience, and we cannot help you with what you gave so far.

That is why Taddy said, give us the full program.
Upload the application which shows the problem, as simple as possible, but make sure it reproduces your problem and send the whole application.

wp

  • Hero Member
  • *****
  • Posts: 6368
What the hell TDateTimeEpsilon actually is?

Since TDateTime is nothing but a floating point variable any calculation with them is affected by round-off error. This is particularly true for calculating date/time differences when the result has to be expressed as an integer which involves rounding.

Since the fractional part of a TDateTime variable is the time during this day, 0.000 refers to midnight, 0.25 to a quarter day, i.e. 6:00, 0.75 three quarters, i.e. 18:00 and so on. The numbers of minutes can be calculated as 24 hours * 60 minutes/hour = 1440. So, you can calculate the number of minutes between two dates or times as the difference of the DateTime values multiplied by 1440, the number of minutes per day. Since the result is a floating point value but you want only integer differences you must provide some rounding. Normally I would use the "Round()" function for this purpose. But this cannot be applied in date/time calculations where it is the convention to consider only full date/time periods. Suppose you have a child which was born on Jan 1, 2018. Calculating its age today (Sept 17, 2019) and using the "Round()" function would tell you that it is already 2 years old which is certainly not true - it is still 1 in common sense. Therefore, Trunc must be used instead of Round.

Using Trunc in floating point calculations, however, introduces the next problem that the result may be off by 1 in case of round-off error. Look at the following example:
Code: Pascal  [Select]
  1. program Project1;
  2. uses
  3.   SysUtils, DateUtils;
  4.  
  5. function MyMinutesBetween(dt1, dt2: TDateTime): Integer;
  6. begin
  7.   Result := Trunc((dt2 - dt1) * 24*60);
  8. end;
  9.  
  10. var
  11.   dt1, dt2: TDateTime;
  12. begin
  13.   dt1 := EncodeTime(3, 24, 51, 386);
  14.   dt2 := EncodeTime(3, 26, 51, 386);
  15.  
  16.   WriteLn('Integer minutes between ',
  17.     FormatDateTime('hh:nn:ss.zzz', dt1), ' and ',
  18.     FormatDateTime('hh:nn:ss.zzz', dt2), ': ',
  19.     MyMinutesBetween(dt1, dt2)
  20.   );
  21.   WriteLn('Floating point minutes: ', (dt2 - dt1) * 24*60);
  22.  
  23.   ReadLn;
  24. end.

The test times dt1 and dt2 have an exact difference of two minutes (3:24:51.386 and 3:26:51.386), but the calculation based on a simple "hand-made" MinutesBetween() function reports that there is only 1!

How can this be? Round-off error: look at the un-rounded difference displayed in the next line: 1.9999999999999929 - a little bit less than 2 due to round-off errors, but the Trunc function makes it to a 1. A huge error!

How to prevent this? This is where TDateTimeEpsilon comes in. It is added to the unrounded difference to make it larger than the next integer value. So, the 1.9999999999999929 is converted to something like (just guessing) 2.000000000001. Applying trunc now results in the correct difference.

Of course, the value of TDateTimeEpsilon must be selected carefully. If it is too small it may not bring the difference value above the next integer. If it is too large it may result in another error when the "true" unrounded value is too close to the integer limit.

Since the smallest time interval handled by EncodeTime / DecodeTime is a millisecond you are probably on the safe side when the correction is selected as a small fraction of a millisecond, maybe 1 microsecond - there are 24*60*60 seconds per day, i.e. 86400 millions microseconds; so, the correction value could be 1/8.64E10 = 1.157E-11.

The MinutesBetween routine in DateUtils calculates the correction term as MinutesPerDay * TDateTimeEpsilon = 3.2E-13 which is even smaller by two orders of magnitude.
« Last Edit: September 17, 2019, 11:30:35 pm by wp »
Lazarus trunk / fpc 3.0.4 / all 32-bit on Win-10

Dio Affriza

  • Jr. Member
  • **
  • Posts: 87
Here's my full program. The first folder is estoh_sqlhttp_bin, because it's a CGI App, I'm using Apache Server, second is estoh_sqlhttp_src, the actual source code.

It needs my library package kyoukai_standard.lpk. Download from my Github repo. Don't use OPM version, it's outdated.

This is how the result, it runs in my VPS: http://103.56.205.179/mysqljson/datetimetest
« Last Edit: September 18, 2019, 04:14:11 am by Dio Affriza »