Now that we know all about the binary prefix origins the next question is how do we format bytes? The answer is not as straightforward as it should be.

This proposed implementation offers 2 options. One is to auto-select the prefix based on the number of bytes and the other is to use a specific prefix. There are two core operations involved in formatting bytes, one is to convert the byte size to the requested unit and the other is to format the converted size into a string, managing decimals.

To help the implementation we need to define a few things. First, will the conversion follow the SI or IEC standard? Then which unit are we making the conversion to? Finally, we need to define the prefixes themselves. To make this all fit together we make the prefix array match the definition of the used norm and the selected unit:

enum PREFIXNORM
{
   PN_SI,                    // Base 10 (i.e. 1000)
   PN_IEC,                   // Base 2 (i.e. 1024)
   PN_DECIMAL = PN_SI,
   PN_BINARY = PN_IEC
};

const INT iPrefixBase[] = { 1000, 1024 };

enum BINARYPREFIX
{
   BP_NONE = -2,
   BP_AUTO,
// SI              IEC 60027      Exp (b10/b2)     Exp (b1000/b1024)
   BP_KILO,        // KIBI        3                1
   BP_MEGA,        // MEBI        6                2
   BP_GIGA,        // GIBI        9                3
   BP_TERA,        // TEBI        12               4
   BP_PETA,        // PEBI        15               5
   BP_EXA,         // EXBI        18               6
   BP_ZETTA,       // ZEBI        21               7
   BP_YOTTA        // YOBI        24               8
};

typedef BINARYPREFIX *LPBINARYPREFIX;

const LPTSTR pszPrefixes[][8] = {
   { _T("KB"),  _T("MB"),  _T("GB"),  _T("TB"),  _T("PB"),  _T("EB"),  _T("ZB"),  _T("YB") },
   { _T("KiB"), _T("MiB"), _T("GiB"), _T("TiB"), _T("PiB"), _T("EiB"), _T("ZiB"), _T("YiB") }
};

To do the actual conversion we need the byte size, the norm to use and the unit to convert to. The function returns the converted byte size and the unit used (if BP_AUTO is specified):

BOOL ConvertBytes(
   ULONGLONG ullSize,
   PREFIXNORM iPrefixNorm,
   BINARYPREFIX iPrefix,
   LPDOUBLE pdResult,
   LPBINARYPREFIX piPrefix)
{
   if (!pdResult || !piPrefix)
       return FALSE;

   DOUBLE dSize = (DOUBLE)ullSize;
   DOUBLE dBase = (FLOAT)iPrefixBase[iPrefixNorm];

   *pdResult = dSize;
   *piPrefix = BP_NONE;

   if (dSize < dBase)    // Already in bytes?
       return TRUE;

   if (iPrefix == BP_AUTO)
       *piPrefix = (BINARYPREFIX)((INT)::floor(::log(dSize) / ::log(dBase)) - 1);
   else
       *piPrefix = iPrefix;

   *pdResult = dSize / ::pow(dBase, (DOUBLE)(*piPrefix) + 1);

   return TRUE;
}

As you may have noticed the auto conversion is flawed. Indeed the maximum precision of a double allows to convert reliably only up to a terabyte. The conversion is still valid with higher values however the auto unit selection will diverge away from the multiple of the base as the number of bytes increases. For example converting the following values:

ULONGLONG ullSizes[] = {
   999ULL,
   1000ULL,                // 1K
   999999ULL,
   1000000ULL,             // 1M
   999999999ULL,
   1000000000ULL,          // 1G
   999999999999ULL,
   1000000000000ULL,       // 1T
   999999999999999ULL,
   1000000000000000ULL,    // 1P
   999999999999999999ULL,
   1000000000000000000ULL, // 1E
   // ...
};

Yields to these results (formatted with %.9f):

999.000000000 B
1.000000000 KB
999.999000000 KB
1.000000000 MB
999.999999000 MB
1.000000000 GB
999.999999999 GB
1.000000000 TB
1.000000000 PB // Should be 999­.999999999 TB but rounded to 1 PB
1.000000000 PB
1.000000000 EB // Should be 999.999999999 PB but rounded to 1 EB
1.000000000 EB

Now formatting the converted size into a readable string is a matter of using the prefix array:

CString FormatBytes(
    ULONGLONG ullBytes,
    INT iDecimals = 3,
    BINARYPREFIX iPrefix = BP_AUTO,
    PREFIXNORM iPrefixNorm = PN_SI)
{
    CString szResult;

    if (iPrefix == BP_NONE)
    {
        szResult.Format(_T("%I64u B"), ullBytes);
    }
    else
    {
        DOUBLE dResult;
        BOOL bRes = ::ConvertBytes(ullBytes, iPrefixNorm, iPrefix, &dResult, &iPrefix);
        ATLASSERT(bRes);

        CString szFormat;
        szFormat.Format(_T("%%.%df %%s"), iDecimals);
        szResult.Format(szFormat, dResult, pszPrefixes[iPrefixNorm][iPrefix]);
    }

    return szResult;
}

Finally, using the formatting is as simple as this:

::_tprintf(_T("%s\n"), ::FormatBytes(357913941));

To give this output:

357.914 MB

Advertisement