Search code examples
delphidatetime-conversion

How do I convert milliseconds to a TDateTime?


I'm doing a long loop downloading thousands of files. I would like to display an estimated time remaining, since it could take hours. However, with what I've written, I get an average number of milliseconds. How do I convert this average download time from milliseconds to a TDateTime?

See where I'm setting Label1.Caption:

procedure DoWork;
const
  AVG_BASE = 20; //recent files to record for average, could be tweaked
var
  Avg: TStringList; //for calculating average
  X, Y: Integer; //loop iterators
  TS, TE: DWORD; //tick counts
  A: Integer; //for calculating average
begin
  Avg:= TStringList.Create;
  try
    for X:= 0 to FilesToDownload.Count - 1 do begin //iterate through downloads
      if FStopDownload then Break; //for cancelling
      if Avg.Count >= AVG_BASE then //if list count is 20
        Avg.Delete(0); //remove the oldest average
      TS:= GetTickCount; //get time started
      try
        DownloadTheFile(X); //actual file download process
      finally
        TE:= GetTickCount - TS; //get time elapsed
      end;
      Avg.Add(IntToStr(TE)); //add download time to average list
      A:= 0; //reset average to 0
      for Y:= 0 to Avg.Count - 1 do //iterate through average list
        A:= A + StrToIntDef(Avg[Y], 0); //add to total download time
      A:= A div Avg.Count; //divide count to get average download time
      Label1.Caption:= IntToStr(A); //<-- How to convert to TDateTime?
    end;
  finally
    Avg.Free;
  end;
end;

PS - I'm open to different ways of calculating the average speed of the last 20 (or AVG_BASE) downloads, because I'm sure my string list solution isn't the best. I don't want to calculate it based on all downloads, because speed may change over that time. Therefore, I'm just checking the last 20.


Solution

  • A TDateTime value is essentially a double, where the integer part is the number of days and fraction is the time.

    In a day there are 24*60*60 = 86400 seconds (SecsPerDay constant declared in SysUtils) so to get A as TDateTime do:

    dt := A/(SecsPerDay*1000.0); // A is the number of milliseconds 
    

    A better way to clock the time would be to use the TStopWatch construct in the unit Diagnostics.

    Example:

    sw.Create;
    .. 
    sw.Start;
    // Do something
    sw.Stop;
    A := sw.ElapsedMilliSeconds;
    // or as RRUZ suggested ts := sw.Elapsed; to get the TimeSpan 
    

    To get your average time, consider using this moving average record:

    Type
      TMovingAverage = record
      private
        FData: array of integer;
        FSum: integer;
        FCurrentAverage: integer;
        FAddIx: integer;
        FAddedValues: integer;
      public
        constructor Create(length: integer);
        procedure Add( newValue: integer);
        function Average : Integer;
      end;
    
    procedure TMovingAverage.Add(newValue: integer);
    var i : integer;
    begin
      FSum := FSum + newValue - FData[FAddIx];
      FData[FAddIx] := newValue;
      FAddIx := (FAddIx + 1) mod Length(FData);
      if (FAddedValues < Length(FData)) then
        Inc(FAddedValues);
      FCurrentAverage := FSum div FAddedValues;
    end;
    
    function TMovingAverage.Average: Integer;
    begin
      Result := FCurrentAverage;
    end;
    
    constructor TMovingAverage.Create(length: integer);
    var
      i : integer;
    begin
      SetLength( FData,length);
      for i := 0 to length - 1 do
        FData[i] := 0;
      FSum := 0;
      FCurrentAverage := 0;
      FAddIx := 0;
      FAddedValues := 0;
    end;