Search code examples
sqlitedatedelphilazarus

Lazarus SQLite date entry displays improperly


UPDATE: Issue was solved by @whosrdaddy. See comments below this question.

I am trying to resolve the following peculiar case: In a friend's Lazarus project, he tries to query an entry in SQLite. The asString()-method (in the procedure for displaying appointments) returns the proper date on Windows 64 Bit. On a 32 Bit operating system, however, only the first two digits are displayed ('16' instead of '28.02.2016'). What could be the reason?

This is the source code for initialising the form:

// Initialise Form
procedure TForm1.FormCreate(Sender: TObject);
begin
  SQLite3Connection1.DatabaseName:='Kalender.sqlite';
  SQLTransaction1.Database:=SQLite3Connection1;
  SQLQuery1.Transaction:=SQLTransaction1;

  // Create Table "tblTermine"
  SQLQuery1.SQL.text := 'CREATE TABLE IF NOT EXISTS tblKalender (Datum DATETIME, Termin VARCHAR(10))';
  SQLQuery1.ExecSQL;
  SQLTransaction1.commit;
end;

There are two further procedures:

// Display Appointments
procedure TForm1.Button1Click(Sender: TObject);
begin
ListBox1.Clear;
SQLQuery1.Close;
SQLQuery1.SQL.text:='SELECT * FROM tblKalender';
SQLQuery1.Open;
while not SQLQuery1.Eof do
  begin
  // Should return 'dd.mm.yyyy'
  ListBox1.Items.add(SQLQuery1.Fields[0].AsString+ ': ' + SQLQuery1.Fields[1].AsString);
  SQLQuery1.Next;
  end;
end;

// Save Appointment
procedure TForm1.Button2Click(Sender: TObject);
var Termin: string;
    Datum: TDate;
begin
  Termin:=Edit1.text;
  if calendardialog1.execute then
  Datum:=TDate(calendardialog1.date);
  SQLQUERY1.close;
  SqlQuery1.SQL.text:= 'Insert into tblKalender Values (:Datum, :Termin)';
  SqlQuery1.ParamByName('Datum').AsDate:= Datum;
  SqlQuery1.ParamByName('Termin').AsString:= Termin;
  SqlQuery1.ExecSQL;
  SqlTransaction1.Commit;
  Button1.Click;
  Edit1.Text := '';
end; 

The intended output into the TListBox would be something like this.


Solution

  • you should convert first the DateTime to a Julian Date

    function DateTimeToJulianDate(const Datum: TDateTime): Double;
    

    and

    SqlQuery1.SQL.text:= 'Insert into tblKalender Values (:Datum, :Termin)';
    SqlQuery1.ParamByName('Datum').AsFloat := DateTimeToJulianDate(Datum);
    ...
    SqlQuery1.ExecSQL;
    

    to test and get the value use :
    function TryJulianDateToDateTime(const AValue: Double; ADateTime: TDateTime):Boolean;

    if TryJulianDateToDateTime(SQLQuery1.Fields[0].AsFloat,myDate)
       then
       ListBox1.Items.add(DateTimeToStr(myDate)+ ': ' + .....
       else
       ShowMessage('Not a valid Julian date');
    

    Update

    SQLite are capable of storing dates and times as TEXT, REAL, or INTEGER values:

    • TEXT as ISO8601 strings ("YYYY-MM-DD HH:MM:SS.SSS").
    • REAL as Julian day numbers, the number of days since noon in Greenwich on November 24, 4714 B.C. according to the proleptic Gregorian calendar.
    • INTEGER as Unix Time, the number of seconds since 1970-01-01 00:00:00 UTC.

    Double Test

    procedure TForm1.Button3Click(Sender: TObject);
    var
     Datum  : TDate;
     myDate : TDateTime;
     JulianDouble : Double;
    begin
    //  uses ....,DateUtils
    
    Datum := StrToDate('01.01.2013');  //German Culture settings
         Memo1.Lines.Add('01/01/2013 = '+DateTimeToStr(Datum)+ '    TDate as Text');
         Memo1.Lines.Add('01/01/2013 = '+FloatToStr(Datum)   + '           TDate Double');
    JulianDouble := DateTimeToJulianDate(Datum);
         Memo1.Lines.Add('01/01/2013 = '+FloatToStr(JulianDouble)  + '    Julian Double');
    if TryJulianDateToDateTime(JulianDouble,myDate)
       then
         Memo1.Lines.Add('01/01/2013 = '+DateTimeToStr(myDate)+ '   TDate as Text')
       else
         ShowMessage('Not a valid Julian date');
    end;
    

    Output :

    01/01/2013 = 01.01.2013 TDate as Text
    01/01/2013 = 41275 TDate Double
    01/01/2013 = 2456293,5 Julian Double
    01/01/2013 = 01.01.2013 TDate as Text


    Update-2 : To write Delphi TDate Double to a SQLite Date field is wrong

    Your comment shows me that you do not know the problems.

    Of course you can directly write a Delphi Double value into a database field. And read it back to a TDateTime.
    This will quickly lead to problems.

    Examples:

    SQLite:

    These functions only work for dates between 0000-01-01 00:00:00 and 9999-12-31 23:59:59 (julidan day numbers 1721059.5 through 5373484.5).
    For dates outside that range, the results of these functions are undefined.

    • 41275 Delphi TDate Double for 2013/01/01 is outside of above range !!

    • SQLite's own date functions can no longer be used.

    Compute the current date.
    SELECT date('now');
    Compute the last day of the current month.
    SELECT date('now','start of month','+1 month','-1 day');
    Compute the date and time given a unix timestamp 1092941466.
    SELECT datetime(1092941466, 'unixepoch');
    Compute the date and time given a unix timestamp 1092941466, and compensate for your local timezone
    SELECT datetime(1092941466, 'unixepoch', 'localtime');
    Compute the number of days since the signing of the US Declaration of Independence.
    SELECT julianday('now') - julianday('1776-07-04');
    etc. etc.

    • Changing the date value with above functions will give you a double 2456293,5 for a Date 2013/01/01
      If you now use unproved and pass it to a Delphi TDateTime it will be 3387/11/26).
      This is not far from the maximum value of a TDateTimePicker.
      which is 2958465.5 and means 9999/12/31

      DateTimePicker1.DateTime := 2958465.5;

      DateTimePicker1 9999/12/31

    If one already know that it is wrong one should not use it up to a crash.