Delphi formatting bytes to GB
Asked Answered
B

2

6

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 ?

Bullen answered 26/7, 2014 at 10:30 Comment(5)
Your LongInts are overflowing, try Int64. Max longint value = 2147483647, max int64 value = 9223372036854775807Komsa
@DavidA Exactly: putting a break point on the first line of the function and inspecting bytes makes that very clear! It shows as 1767128484, not 323889675684Partee
Doh, how did I miss that? Much appreciated, I was starting to think I was going crazy.Bullen
actually, you should use UInt64, and all your IF conditions should be if bytes >= then... note the greater than or equal to.Hubble
You can use StrFormatByteSizeEx function for this kind of stuff.Flite
B
8

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;
Berrie answered 2/5, 2015 at 5:53 Comment(1)
+1 for showing the "uses"! So many posts don't include that info and it can be really hard to nail down sometimes.Babbette
B
1

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.

Bur answered 26/7, 2014 at 12:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.