unit HC3Utilities;
  interface
    uses
      HC3DM         (* tDebuggingMode, , dmHC3, Global *),
      ObservedAndWM (* tFailureReason, frSPS30Access,
                         frSPS30CommandState, frSPS30DataLength, frSPS30ExtArgRange,
                         frSPS30firmware, frSPS30IntArgRange, frSPS30Mystery,
                         frSPS30NoFan, frSPS30NoOldAutoclean, frSPS30NoReset,
                         frSPS30NoResponse, frSPS30NoSerialPort,
                         frSPS30NoSetAutoclean, frSPS30NoShutdown,
                         frSPS30NoStartMeasurement, frSPS30Protocol, frSPS30Timeout,
                         frSPS30UnknownCmd *),
      MyConsts      (* nullstring, TAB *),
      StdCtrls      (* tCheckBox, tLabel, tListBox, tMemo *);

    const
      DateTest = 'convert(WhenRecorded, SQL_DATE) between :BeginDate and :EndDate ';

    var
      INIChanged: boolean;
      INIFileName: string;


// ******* database-related ************************************************
    function GetFieldWidth(const TableOrFieldName: string;
                           FieldName: string = nullstring): integer;
                           
    function FindNewUIDOld       (const TableName: string): integer;  // in HC3Data
    function LastUIDOf           (const TableName: string): integer;  // ..
    function LastUIDOfOld        (const TableName: string): integer;  // ..
    function LastUIDOfHomeControl(const TableName: string): integer;  // in HomeControl

(********** misc **************************************************************)
    function  BoolSpell(const B: boolean): string;
    function  CtoF(const TempC: real): real;
    function  Degrees(const Rad: real): real;
    function  DHT20FailureString(const Reason: tFailureReason): string;
    function  ExpandTabsOfLine(const TheLine: string): string;
    function  FtoC(const TempF: real): real;
    function  FormatFCTemp(const TempF: real): string;
    function  LogDirectory: string;
    procedure NewDampedValue(var DampedValue: real;
                             const DampingFactor, NewRawValue: real);
    procedure OnOffDisplay(NameLabel, ValueLabel: tLabel;
                           const AnchorLocal: tDateTime;
                           const IsNowOn: boolean;
                           const HoursPassed, HoursOn: real);

    function  Radians(const Degrees: real): real;
    procedure SaveListBox(TheBox: tListbox; const TheFileName, TheCaption: string);
    procedure SaveMemo(TheMemo: tMemo; const TheFileName, TheCaption: string);
    function  SPS30FailureString(const Reason: tFailureReason): string;
    function  TrimInternalWhiteSpace(const S: string): string;
    procedure UpdateReportInterval(const LastReportTimeUTC: tDateTime;
                                   var DampedReportIntervalMinutes: real;
                                   TheLabel: tLabel);

    // ******* INI-related *****************************************************
    function  INILoadBoolean (const Section, Ident: string): boolean;                                    overload;
    function  INILoadBoolean (const          Ident: string): boolean;                                    overload;
    function  INILoadDateTime(const Section, Ident: string; const Default: tDateTime): tDateTime;        overload;
    function  INILoadDateTime(const Section, Ident: string                          ): tDateTime;        overload;
    function  INILoadDateTime(const          Ident: string; const Default: tDateTime): tDateTime;        overload;
    function  INILoadDateTime(const          Ident: string                          ): tDateTime;        overload;
    function  INILoadFloat   (const Section, Ident: string;       Default: real = 0): real;              overload;
    function  INILoadFloat   (const          Ident: string;       Default: real = 0): real;              overload;
    function  INILoadInteger (const Section, Ident: string;       Default: integer = 0): integer;        overload;
    function  INILoadInteger (const          Ident: string;       Default: integer = 0): integer;        overload;
    function  INILoadString  (const Section, Ident: string;       Default: string = nullstring): string; overload;
    function  INILoadString  (const          Ident: string;       Default: string = nullstring): string; overload;
    procedure INISaveDateTime(const Section, Ident: string; const DT: tDateTime);                        overload;
    procedure INISaveDateTime(const          Ident: string; const DT: tDateTime);                        overload;
    procedure INISaveBoolean (const Section, Ident: string; const B: boolean);
    procedure INISaveFloat   (const Section, Ident: string; const R: real);                              overload;
    procedure INISaveFloat   (const          Ident: string; const R: real);                              overload;
    procedure INISaveInteger (const Section, Ident: string; const N: integer);                           overload;
    procedure INISaveInteger (const          Ident: string; const N: integer);                           overload;
    procedure INISaveString  (const Section, Ident: string; const S: string);                            overload;
    procedure INISaveString  (const          Ident: string; const S: string);                            overload;

    // ******* time-related ****************************************************
    function FDT(const DT: tDateTime): string;
    function  LastSecondOfTheDay(const DT: tDateTime): tDateTime;
    function  MyStrToDateTime(const S: string): tDateTime;
    function  MilTimeToMSM(const MT: integer): integer;
    function  MSMtoMilTime(const MSM: integer): integer;
    function  RecodeDateTime(const DT: tDateTime): tDateTime;

  implementation
    uses
      AdsTable  (* tAdsQuery *),
      Classes   (* tStrings *),
      DateUtils (* DecodeDateTime, EncodeDateTime, IncDay, IncSecond, Now *),
      Dialogs   (* tSaveDialog *),
      Forms     (* Application *),
      Graphics  (* clPurple, clWindowText *),
      INIFiles  (* tINIFile *),
      SysUtils  (* ChangeFileExt, CreateDir, DirectoryExists, EncodeDate,
                     FormatDateTime, Now, ReplaceTime, rfReplaceAll,
                     StringReplace, StrToInt, StrToTime *);
    var
      INI: tINIFile;
      RecentINISection: string;
      OldTableName: string;
      TabString: string;

    // ******* database-related ************************************************
    function GetFieldWidth(const TableOrFieldName: string;
                           FieldName: string = nullstring): integer;
      (* Get the field width for limiting data-entry fields *)

      var
        FN, TN: string;

      begin (* tdmHomeControl.GetFieldWidth *)
        if FieldName = nullstring then
          begin (* use last-used table name *)
            TN := OldTableName;
            FN := TableOrFieldName
          end   (* use last-used table name *)
        else
          begin (* general case *)
            TN := TableOrFieldName;
            FN := FieldName;
            OldTableName := TableOrFieldName
          end   (* general case *);

        with dmHC3.qFieldWidth do
          begin (* qFieldWidth *)
            Close;
            ParamByName('TableName').AsString := TN;
            ParamByName('FieldName').AsString := FN;
            Open;
            if IsEmpty then
              Result := 0
            else
              Result := FieldByName('FieldWidth').AsInteger
          end   (* qFieldWidth *)
      end   (* tdmHomeControl.GetFieldWidth *);

    function InnerLastUID(TheQuery: tAdsQuery; const TableName: string): integer;
      (* Queries are identical except for DB *)
    
      begin (* InnerLastUID *)
        with TheQuery do
          begin (* TheQuery *)
            Close;
            SQL.Text := 'select max(UID) from ' + TableName;
            Open;
            Result := Fields[0].AsInteger;
            Close
          end   (* TheQuery *)
      end   (* InnerLastUID *);

    function FindNewUIDOld(const TableName: string): integer;
      (* like LastUIDOfOld, but will find an unused UID, possibly not after
      the highest used one *)

      var
        I, Tentative: integer;

      begin (* FindNewUIDOld *)
        Tentative := LastUIDOfOld(TableName) + 1;
        Result := Tentative;
        with dmHC3.qLastUIDOld do
          begin (* dmHC3.qLastUIDOld *)
            Close;
            SQL.Text := 'select UID from ' + TableName + ' order by UID';
            Open;
            for I := 1 to Tentative do
              if not Locate('UID', I, []) then
                begin (* found a gap *)
                  Result := I;
                  break
                end   (* found a gap *);
            Close
          end   (* dmHC3.qLastUIDOld *)
      end   (* FindNewUIDOld *);

    function LastUIDOf(const TableName: string): integer;
      (* DB HC3Data *)

      begin (* LastUIDOf *)
        Result := InnerLastUID(dmHC3.qLastUIDHC3Data, TableName)
      end   (* LastUIDOf *);

    function LastUIDOfOld(const TableName: string): integer;
      (* DB HomeControl *)
    
      begin (* LastUIDOfOld *)
        Result := InnerLastUID(dmHC3.qLastUIDOld, TableName)
      end   (* LastUIDOfOld *);

    function LastUIDOfHomeControl(const TableName: string): integer;
      (* like LastUIDOf, but in HomeControl DB, not HC3Data *)
    
      begin (* LastUIDOfHomeControl *)
        Result := InnerLastUID(dmHC3.qLastUIDHomeControl, TableName)
      end   (* LastUIDOfHomeControl *);

(********** misc **************************************************************)
    function BoolSpell(const B: boolean): string;
      (* for debugging *)
    
      begin (* BoolSpell *)
        if B then
          Result := 'true'
        else
          Result := 'false'
      end   (* BoolSpell *);

    function  CtoF(const TempC: real): real;
      (* trivial *)
    
      begin (* CtoF *)
        Result := 1.8 * TempC + 32.0
      end   (* CtoF *);

    function Degrees(const Rad: real): real;
      (* result in [0, 360) *)
    
      begin (* Degrees *)
        Result := 180 * Rad / pi;
        while Result >= 360 do
          Result := Result - 360;
        while Result < 0 do
          Result := Result + 360
      end   (* Degrees *);

    function  DHT20FailureString(const Reason: tFailureReason): string;
      (* explain the problem *)
    
      begin (* DHT20FailureString *)
        Result := nullstring;
        case Reason of
                frDHT20_BadChecksum: Result := 'bad xx checksum';
              frDHT20_BadConnection: Result := 'bad xx connection';
               frDHT20_BytesAllZero: Result := 'received xx bytes all zero';
               frDHT20_MissingBytes: Result := 'received xx data too short';
                    frDHT20_Timeout: Result := 'xx timeout';
               frDHT20_TWI_Data_NAK: Result := 'xx TWI data NAK';
            frDHT20_TWI_DataTooLong: Result := 'xx TWI data too long';
                frDHT20_TWI_Mystery: Result := 'xx TWI mystery';
                frDHT20_TWI_SLA_NAK: Result := 'xx TWI SLA NAK'
          end (* case *);

        Result := StringReplace(Result, 'xx', 'DHT20', [])
      end   (* DHT20FailureString *);

    function ExpandTabsOfLine(const TheLine: string): string;
      (* probably just for debugging *)

      begin (* ExpandTabsOfLine *)
        Result := StringReplace(TheLine, TabString, '-->', [rfReplaceAll])
      end   (* ExpandTabsOfLine *);

    function FormatFCTemp(const TempF: real): string;
      (* service for OutdoorInstruments *)

      begin (* FormatFCTemp *)
        Result := Format('%.1f °F (%.1f °C)', [TempF, FToC(TempF)])
      end   (* FormatFCTemp *);

    function FtoC(const TempF: real): real;
      (* trivial function *)

      begin (* FtoC *)
        Result := (TempF - 32.0) / 1.8
      end   (* FtoC *);

    function  LogDirectory: string;
      (* tell caller where to save logs; create the directory if necessary *)

      const
        DeskTop = 'C:\Users\Robert R. Fenichel\Desktop';

      var
        DirName: string;

      begin (* LogDirectory *)
        DirName := Format('%s\HC3 %s', [DeskTop, FormatDateTime('mm-dd', Now)]);
        if not DirectoryExists(DirName) then
          CreateDir(DirName);
        Result := DirName
      end   (* LogDirectory *);

    procedure NewDampedValue(var DampedValue: real;
                             const DampingFactor, NewRawValue: real);
      (* tool *)
    
      begin (* NewDampedValue *)
        if DampedValue = 0 then
          DampedValue := NewRawValue
        else
          DampedValue :=  DampingFactor    * DampedValue +
                       (1 - DampingFactor) * NewRawValue
      end   (* NewDampedValue *);

    procedure OnOffDisplay(NameLabel, ValueLabel: tLabel;
                           const AnchorLocal: tDateTime;
                           const IsNowOn: boolean;
                           const HoursPassed, HoursOn: real);
      (* for OutdoorInstruments (waterfall) and Indoor Instruments (furnace, A/C) *)

      var
        PercentOn: real;
        FormattedTime, StateName: string;

      begin (* OnOffDisplay *)
        NameLabel.Enabled := true;
        StateName := 'off';
        FormattedTime := FormatDateTime('hhnn', AnchorLocal);

        with ValueLabel do
          begin (* ValueLabel *)
            Enabled := true;
            Font.Color := clWindowText;
            if IsNowOn then
              begin (* on *)
                StateName := 'on';
                Font.Color := clPurple
              end   (* on *);
            NameLabel.Font.Color := Font.Color;
            if HoursPassed < 0.01 then
              PercentOn := 0
            else
              PercentOn := 100*HoursOn/HoursPassed;

            if HoursOn = 0 then
              Caption := 'off all day'

            else if PercentOn >= 1.0 then
              Caption := Format('on %.0f%% today; now %s since %s',
                                [PercentOn,
                                 StateName,
                                 FormattedTime])
                                 
            else if IsNowOn then
              Caption := 'now on since ' + FormattedTime

            else
              Caption := 'on briefly today; now off since ' + FormattedTime
          end   (* ValueLabel *)
      end   (* OnOffDisplay *);

    function Radians(const Degrees: real): real;
      (* trivial *)
    
      begin (* Radians *)
        Result := pi * Degrees / 180
      end   (* Radians *);

    procedure SaveLines(TheLines: tStrings; const TheFileName, TheCaption: string);
      (* save memo or listbox *)
    
      begin (* SaveLines *)
        with tSaveDialog.Create(nil) do
          try     (* must free dialog *)
            DefaultExt := 'txt';
            FileName   := Format('%s\%s', [LogDirectory(), TheFileName]);
            Title      := TheCaption;
            if Execute then
              TheLines.SaveToFile(FileName)
          finally (* must free dialog *)
            Free
          end     (* must free dialog *);
      end   (* SaveLines *);

    procedure SaveListBox(TheBox: tListbox; const TheFileName, TheCaption: string);
      (* cliche *)

      begin (* SaveListBox *)
        SaveLines(TheBox.Items, TheFileName, TheCaption)
      end   (* SaveListBox *);

    procedure SaveMemo(TheMemo: tMemo; const TheFileName, TheCaption: string);
      (* another cliche *)
    
      begin (* SaveMemo *)
        SaveLines(TheMemo.Lines, TheFileName, TheCaption)
      end   (* SaveMemo *);

    function  SPS30FailureString(const Reason: tFailureReason): string;
      (* just a lookup for Indoor & outdoor displays *)
    
      begin (* SPS30FailureString *)
        Result := nullstring;
        case Reason of
                      frSPS30Access: Result := '0x03 no access right for command';
                frSPS30CommandState: Result := '0x43 command illegal in current state';
                  frSPS30DataLength: Result := '0x01 wrong data length for command';
                 frSPS30ExtArgRange: Result := '0x04 bad arg to command';
                    frSPS30firmware: Result := '0x88 firmware obsolete';
                 frSPS30IntArgRange: Result := '0x28 bad arg to internal procedure';
                     frSPS30Mystery: Result := 'mysterious failure';
                       frSPS30NoFan: Result := 'fan failure';
              frSPS30NoOldAutoclean: Result := 'no old autoclean';
                     frSPS30NoReset: Result := 'reset failure';
                  frSPS30NoResponse: Result := 'no response';
                frSPS30NoSerialPort: Result := 'no serial port';
              frSPS30NoSetAutoclean: Result := 'no set autoclean';
                  frSPS30NoShutdown: Result := 'failed shutdown';
          frSPS30NoStartMeasurement: Result := 'start failure';
                    frSPS30Protocol: Result := '0x51 misc ("protocol") error';
                     frSPS30Timeout: Result := '0x50 timeout during read';
                  frSPS30UnknownCmd: Result := '0x02 unknown command'
          end (* case *);

        Result := 'SPS30 ' + Result
      end   (* SPS30FailureString *);

    function  TrimInternalWhiteSpace(const S: string): string;
      (* service for HC3DM *)

      begin (* TrimInternalWhiteSpace *)
        Result := S;
        repeat (* double --> single *)
          Result := StringReplace(Result, '  ', BLANK, [rfReplaceAll])
        until  (* double --> single *) pos('  ', Result) = 0
      end   (* TrimInternalWhiteSpace *);

    procedure UpdateReportInterval(const LastReportTimeUTC: tDateTime;
                                   var DampedReportIntervalMinutes: real;
                                   TheLabel: tLabel);
      (* used by each of the sensor-related units *)

      const
        RIDamping = 0.7;

      var
        RawReportIntervalMinutes: real;

      begin (* UpdateReportInterval *)
        if LastReportTimeUTC <> Global.ImpossibleDateTime then
          begin (* estimate ReportInterval *)
            RawReportIntervalMinutes := SecondsBetween(Global.LatestData.UTCStamp,
                                                       LastReportTimeUTC) / 60.0;
            if DampedReportIntervalMinutes < 0 then
              DampedReportIntervalMinutes := RawReportIntervalMinutes
            else
              NewDampedValue(DampedReportIntervalMinutes,
                             RIDamping,
                             RawReportIntervalMinutes);
            with TheLabel do
              begin (* TheLabel *)
                Enabled := true;
                Caption := Format('reporting interval is about %.1f minutes',
                                  [DampedReportIntervalMinutes])
              end   (* TheLabel *)
          end   (* estimate ReportInterval *)
      end   (* UpdateReportInterval *);

(******** INI-related *********************************************************)
    function INILoadBoolean(const Section, Ident: string): boolean;
      (* save section for short calls *)
    
      begin (* INILoadBoolean *)
        with INI do
          if ValueExists(Section, Ident) then
            Result := ReadBool(Section, Ident, false)
          else
            Result := false;
        RecentINISection := Section
      end   (* INILoadBoolean *);

    function INILoadBoolean(const Ident: string): boolean;
      (* short call *)

      begin (* INILoadBoolean *)
        Result := INILoadBoolean(RecentINISection, Ident)
      end   (* INILoadBoolean *);

    function INILoadDateTime(const Section, Ident: string; const Default: tDateTime): tDateTime;
      (* save section for short call *)
    
      begin (* INILoadDateTime *)
        with INI do
          if ValueExists(Section, Ident) then
            Result := INI.ReadDateTime(Section, Ident, Default)
          else
            Result := Default;
        RecentINISection := Section
      end   (* INILoadDateTime *);

    function INILoadDateTime(const Ident: string; const Default: tDateTime): tDateTime;
      (* short call *)

      begin (* INILoadDateTime *)
        Result := INILoadDateTime(RecentINISection, Ident, Default)
      end   (* INILoadDateTime *);

    function INILoadDateTime(const Section, Ident: string): tDateTime;
      (* another short call *)
    
      begin (* INILoadDateTime *)
        Result := INILoadDateTime(RecentINISection, Ident, Now)
      end   (* INILoadDateTime *);

    function INILoadDateTime(const Ident: string): tDateTime;
      (* shorter call *)
    
      begin (* INILoadDateTime *)
        Result := INILoadDateTime(RecentINISection, Ident, Now)
      end   (* INILoadDateTime *);

    function INILoadFloat(const Section, Ident: string; Default: real): real;
      (* save section for short calls *)
    
      begin (* INILoadFloat *)
        with INI do
          if ValueExists(Section, Ident) then
            Result := INI.ReadFloat(Section, Ident, Default)
          else
            Result := Default;
        RecentINISection := Section
      end   (* INILoadFloat *);

    function INILoadFloat(const Ident: string; Default: real): real;
      (* short call *)

      begin (* INILoadFloat *)
        Result := INILoadFloat(RecentINISection, Ident, Default)
      end   (* INILoadFloat *);

    function INILoadInteger(const Section, Ident: string; Default: integer): integer;
      (* save section for short call *)
    
      begin (* INILoadInteger *)
        with INI do
          if ValueExists(Section, Ident) then
            Result := INI.ReadInteger(Section, Ident, Default)
          else
            Result := Default;
        RecentINISection := Section
      end   (* INILoadInteger *);

    function INILoadInteger(const Ident: string; Default: integer): integer;
      (* short call *)
    
      begin (* INILoadInteger *)
        Result := INILoadInteger(RecentINISection, Ident, Default)
      end   (* INILoadInteger *);

    function INILoadString(const Section, Ident: string; Default: string): string;
      (* save section for short calls *)
    
      begin (* INILoadString *)
        with INI do
          if ValueExists(Section, Ident) then
            Result := INI.ReadString(Section, Ident, Default)
          else
            Result := Default;
        RecentINISection := Section
      end   (* INILoadString *);

    function INILoadString(const Ident: string; Default: string): string;
      (* short call *)
    
      begin (* INILoadString *)
        Result := INILoadString(RecentINISection, Ident, Default)
      end   (* INILoadString *);

    procedure INISaveBoolean(const Section, Ident: string; const B: boolean);
      (* get file updated *)
    
      begin (* INISaveBoolean *)
        INI.WriteBool(Section, Ident, B)
      end   (* INISaveBoolean *);

    procedure INISaveDateTime(const Section, Ident: string; const DT: tDateTime);
      (* file magically rewritten *)
    
      begin (* INISaveDateTime *)
        INI.WriteDateTime(Section, Ident, DT);
        INIChanged := true;
        RecentINISection := Section
      end   (* INISaveDateTime *);

    procedure INISaveDateTime(const Ident: string; const DT: tDateTime);
      (* short call *)
    
      begin (* INISaveDateTime *)
        INISaveDateTime(RecentINISection, IDent, DT)
      end   (* INISaveDateTime *);

    procedure INISaveFloat(const Section, Ident: string; const R: real);
      (* file gets rewritten *)
    
      begin (* INISaveFloat *)
        INI.WriteFloat(Section, Ident, R);
        INIChanged := true;
        RecentINISection := Section
      end   (* INISaveFloat *);

    procedure INISaveFloat(const Ident: string; const R: real);
      (* short call *)
    
      begin (* INISaveFloat *)
        INISaveFloat(RecentINISection, Ident, R)
      end   (* INISaveFloat *);

    procedure INISaveInteger(const Section, Ident: string; const N: integer);
      (* file magically reqwritten *)

      begin (* INISaveInteger *)
        INI.WriteInteger(Section, Ident, N);
        INIChanged := true;
        RecentINISection := Section
      end   (* INISaveInteger *);

    procedure INISaveInteger(const Ident: string; const N: integer);
      (* file magically reqwritten *)

      begin (* INISaveInteger *)
        INI.WriteInteger(RecentINISection, Ident, N)
      end   (* INISaveInteger *);

    procedure INISaveString(const Section, Ident: string; const S: string);
      (* file rewritten *)
    
      begin (* INISaveString *)
        INI.WriteString(Section, Ident, S);
        INIChanged := true;
        RecentINISection := Section
      end   (* INISaveString *);

    procedure INISaveString(const  Ident: string; const S: string);
      (* short call *)
    
      begin (* INISaveString *)
        INI.WriteString(RecentINISection, Ident, S)
      end   (* INISaveString *);

(********** time-related ******************************************************)
    function FDT(const DT: tDateTime): string;
      (* space-saving *)

      begin (* FDT *)
        Result := FormatDateTime('yyyy-mm-dd hhnn:ss', DT)
      end   (* FDT *);

    function LastSecondOfTheDay(const DT: tDateTime): tDateTime;
      (* This replaces DateUtils' function EndOfTheDay, which
      returns 23:59:59.999.  Under some circumstances, the Advantage
      DB cannot handle fractional seconds, although
          select
            *
          from
            furnace
          where
            WhenRecorded > '2012-04-13 23:59:59.999' and
            Observed = 'workroom heater'
       works appropriately in the ARC32 SQL interpreter *)

      begin (* LastSecondOfTheDay *)
        Result := IncSecond(StartOfTheDay(IncDay(DT)), -1)
      end   (* LastSecondOfTheDay *);

    function MilTimeToMSM(const MT: integer): integer;
      (* military time to minutes since midnight *)
    
      begin (* MilTimeToMSM *)
        Result := 60 * (MT div 100) + (MT mod 100)
      end   (* MilTimeToMSM *);

    function MSMtoMilTime(const MSM: integer): integer;
      (* minutes since midnight to military time *)
    
      begin (* MSMtoMilTime *)
        Result := 100 * (MSM div 60) + (MSM mod 60)
      end   (* MSMtoMilTime *);

    function  MyStrToDateTime(const S: string): tDateTime;
      (* S is
                  1111111111
         1234567890123456789
         2019-02-04 10:51:30
      *)

      var
        Day, Month, Year: integer;

      begin (* MyStrToDateTime *)
        Year  := StrToInt(copy(S, 1, 4));
        Month := StrToInt(copy(S, 6, 2));
        Day   := StrToInt(copy(S, 9, 2));
        try    (* date conversion is fragile *)
          Result := EncodeDate(Year, Month, Day);
          ReplaceTime(Result,
                      StrToTime(copy(S, 12, maxint)))
        except (* date conversion is fragile *)
          on eConvertError do
            Result := 0
        end    (* date conversion is fragile *)
      end   (* MyStrToDateTime *);

    function  RecodeDateTime(const DT: tDateTime): tDateTime;
      (* clear out odd millisewconds for database *)

      var
        Day, Hour, Millisecond, Minute, Month, Second, Year: word;

      begin (* RecodeDateTime *)
        DecodeDateTime(DT, Year, Month, Day, Hour, Minute, Second, Millisecond);
        Result := EncodeDateTime(Year, Month, Day, Hour, Minute, Second, 0)
      end   (* RecodeDateTime *);

(********** end time-related **************************************************)
                                                                 {$WARNINGS OFF}
  initialization
    TabString := 'x';
    TabString[1] := TAB;
                                                                 {$WARNINGS ON}
    INIFileName := ChangeFileExt(Application.ExeName, '.ini');
    INI := tIniFile.Create(INIFileName);
    INIChanged := false;

end.
