18
\$\begingroup\$

This function is simple, it gets a number of bytes and returns its representation in either Bytes, KB, MB, GB and TB.

As simple as it is, I am sure there are other (and perhaps better ways) to write it.

function kmgtbytes (num) {
    if (num > 0 ){
        if (num < 1024)             { return [num, "Bytes"] }
        if (num < 1048576)          { return [num/1024, "KB"] }
        if (num < 1073741824)       { return [num/1024/1024, "MB"] }
        if (num < 1099511600000)    { return [num/1024/1024/1024, "GB"] }

        return [num/1024/1024/1024/1024, "TB"]
    }

    return num
}
\$\endgroup\$
12
  • 3
    \$\begingroup\$ Your primary concern should be the clarity of your code. minitech's answer is sweet and terse, yet your code requires little thought to discern its function. Both of you should add comments to your code indicating its purpose. \$\endgroup\$ Aug 16, 2013 at 17:12
  • 7
    \$\begingroup\$ jsPerf for all current answers \$\endgroup\$
    – Ry-
    Aug 16, 2013 at 17:29
  • 2
    \$\begingroup\$ Having your function return a different type as the default will probably cause type errors everywhere \$\endgroup\$
    – Eric
    Aug 16, 2013 at 18:00
  • 2
    \$\begingroup\$ This function lacks only a better name and an introductory comment (incl. examples of possible outputs). I haven't managed to come up with either, by the way. \$\endgroup\$ Aug 16, 2013 at 21:02
  • 2
    \$\begingroup\$ You may find reading how others have done it in the past enlightening. \$\endgroup\$
    – kojiro
    Aug 17, 2013 at 12:10

7 Answers 7

15
\$\begingroup\$

Here's a slightly less repetitive way to write it since we know each unit is 1024 as large as the last:

function kmgtbytes (num) {
    var unit, units = ["TB", "GB", "MB", "KB", "Bytes"];
    for (unit = units.pop(); units.length && num >= 1024; unit = units.pop()) {
        num /= 1024;
    }
    return [num, unit];
}
\$\endgroup\$
2
  • \$\begingroup\$ I'll admit that using pop() is a little inefficient as you're creating and mutating the array of units on every call. Whether this is actually a problem depends on how often you expect to be calling this function, and your definition of "better" as used in the question :) \$\endgroup\$
    – Nick
    Aug 18, 2013 at 12:45
  • \$\begingroup\$ Why is the result of this function an array? Very surprising. \$\endgroup\$ Aug 19, 2014 at 13:46
12
\$\begingroup\$

What about:

var units=["Byte","KB","MB","GB","TB","PB"];

function getUnit(bytes){
    for(var i of units){
        if(bytes<1024) return bytes+" "+units[i];
        bytes/=1024;
    }
}

Edit:

It prevents you from precalculating your values for each if-clause. It's easily extensible: Just add another unit, wherever you want. And for the rest: it does its job in an easy understandable way. No rocket science.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Please don’t use for…in loops to iterate arrays. It’s darn slow and not necessarily accurate. \$\endgroup\$
    – Ry-
    Aug 17, 2013 at 5:19
  • 2
    \$\begingroup\$ Optimizing this loop is barking up the wrong tree, even if it were slow. If that's the only "bottleneck" in your code, everything is okay I would say. \$\endgroup\$ Aug 17, 2013 at 17:02
  • 2
    \$\begingroup\$ Yes, but a for...in loop isn't guaranteed to execute in order. More reading here. \$\endgroup\$
    – jahroy
    Aug 23, 2013 at 6:11
11
\$\begingroup\$

You can use logarithms:

var sizes = ["bytes", "KB", "MB", "GB", "TB"];

function formatSize(bytes) {
    var l = Math.min(sizes.length - 1, Math.log(bytes) / Math.LN2 / 10 | 0);
    return [bytes / Math.pow(1024, l), sizes[l]];
}

The confusing part, Math.log(bytes) / Math.LN2 / 10 | 0, gets the base-1024 logarithm of bytes and truncates it.

\$\endgroup\$
7
  • \$\begingroup\$ The use of logarithm is a wise one! \$\endgroup\$ Aug 16, 2013 at 15:57
  • 2
    \$\begingroup\$ Excessive use of floating point is icky for my taste, and also likely to perform worse. \$\endgroup\$ Aug 16, 2013 at 17:17
  • 1
    \$\begingroup\$ @200_success: You can’t avoid floating-point here. 1024^4 > 2^32. Apart from that, “once, which forms the crux of this shortcut” is difficult to avoid :P \$\endgroup\$
    – Ry-
    Aug 16, 2013 at 17:18
  • 1
    \$\begingroup\$ No doubt, the problem itself requires floating point, as the return value is FP. It's the Math.log() that I find excessive. \$\endgroup\$ Aug 16, 2013 at 17:22
  • 2
    \$\begingroup\$ @200_success: This is the fastest answer so far on Firefox 23.0, for what it’s worth. (Which is not very much.) \$\endgroup\$
    – Ry-
    Aug 16, 2013 at 17:35
9
\$\begingroup\$

I will discuss 2 points to your code: Readability/Cleanliness and Robustness

From the Readability/Cleanliness perspective I'd argue that your code is the easiest to read vs any answer you've received thus far. I knew in 3 seconds the purpose of all of that code and wouldn't need any comments to further explain it.

From the robustness perspective it all boils down to the use case of the code. If you know without a doubt that you will only ever see positive sized numbers <1024 TB I'd say again your code is fine. If you wanted to handle file size differences (as in 100kb-150kb = -50kb) or file sizes beyond 1023TB then clearly you need to enhance your approach.

To handle the more general case this question has already been answered here as well: https://stackoverflow.com/questions/281640/how-do-i-get-a-human-readable-file-size-in-bytes-abbreviation-using-net/4975942#4975942

As a side note it would appear size=0 would return [0,] but size of 1 would return [1,Bytes]. You may want to return [0,Bytes]

\$\endgroup\$
3
\$\begingroup\$

How about recursion? (http://jsfiddle.net/WhhCL/1/)

var units = ["Bytes", "KB", "MB", "GB", "TB"];

function unitize(num) {
    return unitizer(num, 0);
}

function unitizer(num, level) {
    if (num < 1024 || level > units.length - 1) {
        return num + " " + units[level];
    } else {
        return unitizer(num / 1024, level + 1);
    }
}

The readability of your original function is tough to beat. At least for now, it's also the most performant of the bunch.

\$\endgroup\$
2
\$\begingroup\$

I'd use a while loop, divide by 1024 and keep track of how many times you divided.

This is my completely bonkers function:

function resolve_to_power_of(bytes, power) {
    var powers;
    powers = 0;
    while (bytes >= power) {
        powers += 1;
        bytes = bytes / power;
    }
    return {
        quantity: bytes,
        powers: powers
    };
}

function format_default(bytes) {
    var resolved, descriptors;
    resolved = resolve_to_power_of(bytes, 1024);
    descriptors = [ "B", "KB", "MB", "TB", "GB", "PB", "EB", "ZB", "YB" ];
    return Math.ceil(resolved.quantity) + " " + descriptors[resolved.powers];
}

format_default(1024); // 1 KB
format_default(1000); // 1000 B
format_default(102400); // 100 KB

The reason it's set up like this is because it also supports dividing by 1000 and other byte lables like [ "B", "K", "M" .. ] etc. following IEC and SI standards.

\$\endgroup\$
6
  • \$\begingroup\$ why while loop? \$\endgroup\$ Aug 16, 2013 at 15:56
  • 2
    \$\begingroup\$ Because it makes the most sense: keep dividing while the number is larger than or equal to 1024. \$\endgroup\$ Aug 16, 2013 at 15:59
  • \$\begingroup\$ You can use ++ and /= and var powers = 0;, if you’re not against that sort of formatting. (I’d do /=, though.) \$\endgroup\$
    – Ry-
    Aug 16, 2013 at 16:03
  • \$\begingroup\$ @minitech, it's good practice to separate variable declaration and instantiation in JavaScript. Scripts should generally be written as with an order of: variable declaration, function declaration, initialization, execution. Doing this the more verbose way helps avoid some variable hoisting issues, and some initialization ordering issues. \$\endgroup\$
    – zzzzBov
    Aug 16, 2013 at 17:26
  • 1
    \$\begingroup\$ @minitech My coding style aims to increase readability, extensibility and maintainability and decrease the chance of errors. This code passes JSLint checks. Nick's solution is very clever, it uses only 6 lines. I also had to read it twice to make sure there is no off-by-one error. It has the most upvotes because we measure code quality by the least number of characters and best use of obscure functions. /sarcasm. If I found this code in one of my applications I would ask him to rewrite it. \$\endgroup\$ Aug 16, 2013 at 19:06
1
\$\begingroup\$

You can loop through the thousands until the number is small enough, then just return the remaining number and the corresponding unit:

function kmgtbytes(num) {
  for (var i = 0; num >= 1024 && i < 4; i++) num /= 1024;
  return [num, ["Bytes","kB","MB","GB","TB"][i]];
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.