Converting a number of bytes into a file size in C
Asked Answered
J

3

9

I want to convert a single number of bytes, into a file size (that has .KB, .MB and .GB).

If the number is 0, I don't want to have any unit. If the number is exactly divisible by a multiple of 1024 (not a floating point), then I will print: x . Otherwise, I want to print a floating point with one degree precision.

I made some code that seems to work well, but it's very cumbersome. I'm looking into ways I could make my function cleaner/more efficient please, it's honestly VERY ugly:

char *
calculateSize( off_t size )
{
  char *result = (char *) malloc(sizeof(char) * 20);
  static int GB = 1024 * 1024 * 1024;
  static int MB = 1024 * 1024;
  static int KB = 1024;
  if (size >= GB) {
    if (size % GB == 0)
      sprintf(result, "%d GB", size / GB);
    else
      sprintf(result, "%.1f GB", (float) size / GB);
  }
  else if (size >= MB) {
    if (size % MB == 0)
      sprintf(result, "%d MB", size / MB);
    else
      sprintf(result, "%.1f MB", (float) size / MB);
  }
  else {
    if (size == 0) {
      result[0] = '0';
      result[1] = '\0';
    }
    else {
      if (size % KB == 0)
        sprintf(result, "%d KB", size / KB);
      else
        sprintf(result, "%.1f KB", (float) size / KB);
    }
  }
  return result;
}

I would really appreciate if someone has a better way to achieve the same result please.

Janssen answered 10/10, 2010 at 3:0 Comment(3)
Since you are using the same logic for GB, MB, and KB you might be better off moving that logic into a separate functionSurcingle
you may want to use long instead of int.Asha
Thanks for those improvements!Janssen
M
18

Using a table-driven representation extended up to EiB.

#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define DIM(x) (sizeof(x)/sizeof(*(x)))

static const char     *sizes[]   = { "EiB", "PiB", "TiB", "GiB", "MiB", "KiB", "B" };
static const uint64_t  exbibytes = 1024ULL * 1024ULL * 1024ULL *
                                   1024ULL * 1024ULL * 1024ULL;

char *
calculateSize(uint64_t size)
{   
    char     *result = (char *) malloc(sizeof(char) * 20);
    uint64_t  multiplier = exbibytes;
    int i;

    for (i = 0; i < DIM(sizes); i++, multiplier /= 1024)
    {   
        if (size < multiplier)
            continue;
        if (size % multiplier == 0)
            sprintf(result, "%" PRIu64 " %s", size / multiplier, sizes[i]);
        else
            sprintf(result, "%.1f %s", (float) size / multiplier, sizes[i]);
        return result;
    }
    strcpy(result, "0");
    return result;
}

Test code

int main(void)
{   
    uint64_t list[] =
    {   
        0, 1, 2, 34, 900, 1023, 1024, 1025, 2048, 1024 * 1024, 
        1024 * 1024 * 1024 + 1024 * 1024 * 400
    };
    int i; 
    for (i = 0; i < DIM(list); i++)
    {   
        char *str = calculateSize(list[i]);
        printf("%18" PRIu64 " = %s\n", list[i], str);
        free(str);
    }
    return 0;
}

Test output

                 0 = 0
                 1 = 1 B
                 2 = 2 B
                34 = 34 B
               900 = 900 B
              1023 = 1023 B
              1024 = 1 KiB
              1025 = 1.0 KiB
              2048 = 2 KiB
           1048576 = 1 MiB
        1493172224 = 1.4 GiB
Monocle answered 10/10, 2010 at 4:10 Comment(3)
Thanks a lot! I love your solution! Thanks!Janssen
Minor nitpick: with the number 2047 then the result string is "2.0 KiB". Here I would expect "1.9 KiB". This is good code, Thank you.Potter
@neoneye: to be precise, all the values between 1997 and 2099 (except for 2048) are reported as 2.0 KiB. Similarly, the values from 1025 to 1075 are reported as 1.0 KiB, but the values from 973 to 1023 are reported as those integer values. Basically, it's a question of rounding. If the desire is to truncate rather than round, you modify the calculation accordingly. Replace (float) size / multiplier with something more complex, like floor((10.0 * size) / multiplier) / 10.0. We can debate the use of float vs double in the cast — that was carried over from the question.Monocle
A
5

I'd use a table approach. Something along the lines of:

void
printsize(size_t  size)
{                   
    static const char *SIZES[] = { "B", "kB", "MB", "GB" };
    size_t div = 0;
    size_t rem = 0;

    while (size >= 1024 && div < (sizeof SIZES / sizeof *SIZES)) {
        rem = (size % 1024);
        div++;   
        size /= 1024;
    }

    printf("%.1f %s\n", (float)size + (float)rem / 1024.0, SIZES[div]);
}
Amosamount answered 10/10, 2010 at 4:0 Comment(2)
Strictly, your formatting doesn't match the requirements, but that is easily fixed.Monocle
Notice that if the size is > 1024 GB, it will fail by going out of the bounds of your SIZES array. The stop condition of your while loop should be div < (sizeof SIZES / sizeof *SIZES) - 1.Diverting
S
1

A nicer way would be:

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

char *humanMemorySize(uint64_t bytes) {
    char *result = (char *) malloc(sizeof(char) * 20);
    char *sizeNames[] = { "B", "KB", "MB", "GB" };

    uint64_t i = (uint64_t) floor(log(bytes) / log(1024));
    double humanSize = bytes / pow(1024, i);
    snprintf(result, sizeof(char) * 20, "%g %s", humanSize, sizeNames[i]);

    return result;
}

The result of the function must be freed.

Supporter answered 12/10, 2023 at 7:45 Comment(2)
If the size of the input is bigger than 1024GB (1TB), this code will fail since i would be out of bound of your sizeNames array.Diverting
Extend sizeNames with "TB" and so on ...Supporter

© 2022 - 2025 — McMap. All rights reserved.