Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delphi formatting bytes to GB

I am using the following function to format Bytes to a more human readable format, but it is returning the incorrect information.

 //Format file byte size
 function FormatByteSize(const bytes: LongInt): string;
 const
   B = 1; //byte
   KB = 1024 * B; //kilobyte
   MB = 1024 * KB; //megabyte
   GB = 1024 * MB; //gigabyte
 begin
   if bytes > GB then
     result := FormatFloat('#.## GB', bytes / GB)
   else
     if bytes > MB then
       result := FormatFloat('#.## MB', bytes / MB)
     else
       if bytes > KB then
         result := FormatFloat('#.## KB', bytes / KB)
       else
         result := FormatFloat('#.## bytes', bytes) ;
 end;

Example:

procedure TForm1.Button1Click(Sender: TObject);
begin
 ShowMessage(FormatByteSize(323889675684)); //Returns 1.65GB when it should be ~301GB
end;

Reference: http://delphi.about.com/od/delphitips2008/qt/format-bytes.htm (Author: Zarco Gajic)

Can anyone explain why it is returning the incorrect information and more importantly know how to fix it so it returns the correct information ?

like image 572
user3839120 Avatar asked Feb 12 '23 09:02

user3839120


2 Answers

The problem are arithmetic overflow. You can rewirte the the function like this:

uses
  Math;

function ConvertBytes(Bytes: Int64): string;
const
  Description: Array [0 .. 8] of string = ('Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
var
  i: Integer;
begin
  i := 0;

  while Bytes > Power(1024, i + 1) do
    Inc(i);

  Result := FormatFloat('###0.##', Bytes / Power(1024, i)) + #32 + Description[i];
end;
like image 185
Jens Borrisholt Avatar answered Feb 20 '23 18:02

Jens Borrisholt


Like said in the comments, your problem is that you are overflowing your 32-bit integer with a 64-bit value, thus it gets truncated to 32 bit (the top 32 bits are simply thrown away, so f.ex. a value of 5 Gb will be understood as 1 Gb). Also, since you are talking about sizes, you really shouldn't use integers, as you will then throw away half of your range on values that can't be valid in any case (a file, f.ex., can't have a size of -2048 bytes).

I have for some time used the following two functions. The one without a Decimals parameter will return up to 3 decimals, but only if necessary (ie. if the size is exactly 1 Gb, then it will return the string "1 Gb" and not "1,000 Gb" (if your decimal point is the comma)).

The one with a Decimals parameter will always return the value with that number of decimals.

Also note, that the calculation is done using the binary scale (1 Kb = 1024 bytes). If you want it changed to the decimal scale, you should change the 1024 values with 1000 and probably the SizeUnits array as well.

CONST
  SizeUnits     : ARRAY[0..8] OF PChar = ('bytes','Kb','Mb','Gb','Tb','Pb','Eb','Zb','Yb');

FUNCTION SizeStr(Size : UInt64) : String; OVERLOAD;
  VAR
    P   : Integer;

  BEGIN
    Result:=SizeStr(Size,3);
    IF Size>=1024 THEN BEGIN
      P:=PRED(LastDelimiter(' ',Result));
      WHILE COPY(Result,P,1)='0' DO BEGIN
        DELETE(Result,P,1);
        DEC(P)
      END;
      IF CharInSet(Result[P],['.',',']) THEN DELETE(Result,P,1)
    END
  END;

FUNCTION SizeStr(Size : UInt64 ; Decimals : BYTE) : String; OVERLOAD;
  VAR
    I           : Cardinal;
    S           : Extended;

  BEGIN
    S:=Size;
    FOR I:=LOW(SizeUnits) TO HIGH(SizeUnits) DO BEGIN
      IF S<1024.0 THEN BEGIN
        IF I=LOW(SizeUnits) THEN Decimals:=0;
        Result:=Format('%.'+IntToStr(Decimals)+'f',[S]);
        Result:=Result+' '+StrPas(SizeUnits[I]);
        EXIT
      END;
      S:=S/1024.0
    END
  END;

If you are using a compiler version of Delphi that doesn't have the UInt64 type, you can use Int64 instead (you probably won't come acros files larger than 8 Eb = apprx. 8.000.000 TeraBytes in your lifetime :-), so Int64 should be sufficient in this case).

Also, the CharInSet function is one from the Unicode versions of Delphi. It can be implemneted as:

TYPE TCharacterSet = SET OF CHAR;

FUNCTION CharInSet(C : CHAR ; CONST Z : TCharacterSet) : BOOLEAN; INLINE;
  BEGIN
    Result:=(C IN Z)
  END;

or replaced directly in the source, if you are using a pre-Unicode version of Delphi.

like image 27
HeartWare Avatar answered Feb 20 '23 19:02

HeartWare