5

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.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
Jerry Dodge
  • 26,858
  • 31
  • 155
  • 327

2 Answers2

11

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;
LU RD
  • 34,438
  • 5
  • 88
  • 296
  • 1
    +1 for TStopWatch. Note that on some computers, I actually found that TStopWatch's underlying Win32 API functions were not stable and did not work properly (Windows XP, on certain Intel Pentium 4 chipsets with ICH4/ICH5 era chipsets). Not a problem anymore on modern hardware (Core2Duo and later), but ... I've spent a lot of time measuring milliseconds (trying to do it accurately) and Windows APIs have given me much grief for trying. (I'm not saying I want hard realtime, just a millisecond counter that doesn't freeze or lurch forward on me) - TStopWatch is far more accurate than GetTickCount! – Warren P Aug 27 '12 at 18:47
  • +1 indeed, now if only I could also take the file sizes into consideration....... far different story though........ – Jerry Dodge Aug 27 '12 at 22:07
8

Instead of a TDateTime you can use the TTimeSpan record, you can create a new instance passing the ticks elapsed to the constructor and from here you can use the Days, Hours, minutes, seconds and Milliseconds properties to display the elapsed time. Now for calculate the remaining time you need the total bytes to download and the current downloaded bytes.

RRUZ
  • 134,889
  • 20
  • 356
  • 483