A Method For Creating A Human-Readable File Size

by on August 13, 2012 7:56 am

Recently, I was working on a project in which the users needed to see a list of files available for download. While it wasn’t a specific requirement, I thought it might be helpful to have the file size appear next to the file name. This is a common enough use case that I figured that there must be an open source library that would give a human-readable file size if I were to give it a file length.

A quick search later, I found the Apache Commons FileUtils class and the byteCountToDisplaySize method. Looking at the JavaDoc, we see that it returns a “human-readable version of the file size, where the input represents a specific number of bytes. If the size is over 1GB, the size is returned as the number of whole GB, i.e. the size is rounded down to the nearest GB boundary. Similarly for the 1MB and 1KB boundaries.”

This seemed to be what I was looking for, and for this particular use case it works fine. However, it could be misleading if you needed more accurate sizes. If I’m looking at a file that has a size of 1.99 MB, it would display as 1 MB. Even worse, a 1.99 GB file would display as only being 1 GB in size. This is even pointed out in a JIRA ticket attached to the JavaDoc.

I decided to implement an improved version. Surprisingly, I got my inspiration from Windows Explorer. When you look at the drive size and space free in Windows Explorer (in this case, the Windows 7 version), you’ll only see the three most significant digits of the number. Here I’ll implement a version of the method, based on the original byteCountToDisplaySize, to have the same behavior.

A Look At The Original

The original class divides the byte length by multiples of a byte from high (yottabyte) to low (kilobyte). When one of those divisions equals a number above zero (since it is an integer division), it has reached the appropriate multiple and outputs the value followed by the appropriate symbol.

For this method, I’ll follow the same pattern to determine the appropriate symbol:

  /**
     * The number of bytes in a kilobyte.
     */
    public static final BigInteger ONE_KB = BigInteger.valueOf(1024);

    /**
     * The number of bytes in a megabyte.
     */
    public static final BigInteger ONE_MB = ONE_KB.multiply(ONE_KB);

    /**
     * The number of bytes in a gigabyte.
     */
    public static final BigInteger ONE_GB = ONE_KB.multiply(ONE_MB);

    /**
     * The number of bytes in a terabyte.
     */
    public static final BigInteger ONE_TB = ONE_KB.multiply(ONE_GB);

    /**
     * The number of bytes in a petabyte.
     */
    public static final BigInteger ONE_PB = ONE_KB.multiply(ONE_TB);

    /**
     * The number of bytes in an exabyte.
     */
    public static final BigInteger ONE_EB = ONE_KB.multiply(ONE_PB);

    /**
     * The number of bytes in a zettabyte.
     */
    public static final BigInteger ONE_ZB = ONE_KB.multiply(ONE_EB);

    /**
     * The number of bytes in a yottabyte.
     */
    public static final BigInteger ONE_YB = ONE_KB.multiply(ONE_ZB);

    /**
     * Returns a human-readable version of the file size, where the input
     * represents a specific number of bytes.
     *
     * @param size
     *        	the number of bytes
     * @return a human-readable display value (includes units - YB, ZB, EB, PB, TB, GB,
     *     	MB, KB or bytes)
     */
    public static String byteCountToDisplaySize(BigInteger size) {
    	String displaySize;

   	 if (size.divide(ONE_YB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_YB)) + " YB";
   	 } else if (size.divide(ONE_ZB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_ZB)) + " ZB";
   	 } else if (size.divide(ONE_EB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_EB)) + " EB";
   	 } else if (size.divide(ONE_PB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_PB)) + " PB";
   	 } else if (size.divide(ONE_TB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_TB)) + " TB";
   	 } else if (size.divide(ONE_GB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_GB)) + " GB";
   	 } else if (size.divide(ONE_MB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_MB)) + " MB";
   	 } else if (size.divide(ONE_KB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_KB)) + " KB";
   	 } else {
   		 displaySize = String.valueOf(size) + " bytes";
   	 }
   	 return displaySize;
    }

That code replicates the behavior of the original byteCountToDisplaySize method. The if/else if /else block structure will remain the same for our method, but the calculation of the displaySize must change. A new method, getThreeSigFigs, will be created for this.

Our New Calculation

That code replicates the behavior of the original byteCountToDisplaySize method. The if/else if /else block structure will remain the same for our method, but the calculation of the displaySize must change. A new method, getThreeSigFigs, will be created for this.

	private static String getThreeSigFigs(double displaySize) {
   	  String number = String.valueOf(displaySize);
   	  StringBuffer trimmedNumber = new StringBuffer();
   	  int cnt = 0;
   	  for (char digit : number.toCharArray()) {
   		  if (cnt < 3) {
   			  trimmedNumber.append(digit);
   		  }
   		  if (digit != '.') {
   			  cnt++;
   		  }
   	  }
   	  return trimmedNumber.toString();
    }

The Updated Method

The above method will grab the first three digits and the decimal, if it occurs before the third digit, and output it as a string. Now let’s plug this into the method.

	public static String byteCountToDisplaySize(BigInteger size) {
   	 String displaySize;
   	 BigDecimal decimalSize = new BigDecimal(size);

   	 if (size.divide(ONE_YB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = String.valueOf(size.divide(ONE_YB)) + " YB";
   	 } else if (size.divide(ONE_ZB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = getThreeSigFigs(decimalSize.divide(new BigDecimal(ONE_ZB))) + " ZB";
   	 } else if (size.divide(ONE_EB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = getThreeSigFigs(decimalSize.divide(new BigDecimal(ONE_EB))) + " EB";
   	 } else if (size.divide(ONE_PB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = getThreeSigFigs(decimalSize.divide(new BigDecimal(ONE_PB))) + " PB";
   	 } else if (size.divide(ONE_TB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = getThreeSigFigs(decimalSize.divide(new BigDecimal(ONE_TB))) + " TB";
   	 } else if (size.divide(ONE_GB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = getThreeSigFigs(decimalSize.divide(new BigDecimal(ONE_GB))) + " GB";
   	 } else if (size.divide(ONE_MB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = getThreeSigFigs(decimalSize.divide(new BigDecimal(ONE_MB))) + " MB";
   	 } else if (size.divide(ONE_KB).compareTo(BigInteger.ZERO) > 0) {
   		 displaySize = getThreeSigFigs(decimalSize.divide(new BigDecimal(ONE_KB))) + " KB";
   	 } else {
   		 displaySize = String.valueOf(size) + " bytes";
   	 }
   	 return displaySize;
    }
</code>

We leave the method out for two of the branches. The first branch in the extremely rare case that we have a file over 999 YB and the last branch because we will always show all of the digits for values under one kilobyte. But how do we know this code works?

Unit Test

package com.keyholesoftware;

import java.util.Arrays;
import java.util.Collection;

import junit.framework.Assert;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class KHSFileUtilsTest {

    private Long input;
    private String output;

    public KHSFileUtilsTest(Long input, String output) {
   	 this.input = input;
   	 this.output = output;
    }

    @Parameters
    public static Collection<Object[]> generateData() {
   	 return Arrays.asList(new Object[][] { { 0L, "0 bytes" },
   			 { 27L, "27 bytes" }, { 999L, "999 bytes" }, {1000L, "1000 bytes" },
   			 {1023L, "1023 bytes"},{1024L, "1.0 KB"},{1728L, "1.68 KB"},{110592L, "108 KB"},
   			 {7077888L, "6.75 MB"}, {452984832L, "432 MB"}, {28991029248L, "27.0 GB"},
   			 {1855425871872L, "1.68 TB"}, {9223372036854775807L, "8.0 EB"}});
    }

    @Test
    public void testByteCountToDisplaySizeBigInteger() {
   	 Assert.assertEquals(output, KHSFileUtils.byteCountToDisplaySize(input));
    }
}

I use a parameterized JUnit test here so we can easily test multiple inputs against their expected output.

I hope you’ll find this improved version of the method byteCountToDisplaySize (with surprising inspiration from Windows Explorer) useful. Please let me know if you have any questions.

– Brice McIver, asktheteam@keyholesoftware.com

  • Share:

3 Responses to “A Method For Creating A Human-Readable File Size”

  1. David says:

    Has this patch been submitted to apache? I think they would find this useful…Here’s a link on how to submit? http://commons.apache.org/patches.html

Leave a Reply

Things Twitter is Talking About
  • Famo.us' main idea is for HTML5/JS/CSS web pages to feel like native mobile apps. So, @zachagardner tried it out - http://t.co/S77TSKHDKd
    April 15, 2014 at 6:40 PM
  • @JKFeldkamp Thanks for your RT! Such a neat technology. We're so excited @zachagardner is getting involved. Have a great day!
    April 15, 2014 at 4:00 PM
  • .@zachagardner has been tinkering with Famo.us (@befamous) released 4/10. What he's learned so far with a POC app - http://t.co/1jMqBfZURn
    April 15, 2014 at 2:29 PM
  • Tutorial: create #RabbitMQ Template to send msg to an exchange & listen for msgs with a routing key pattern - http://t.co/qDbq6TrxtW
    April 11, 2014 at 10:02 AM
  • There's a great #KC conference coming up on April 23rd - @KCITP's Mobile Midwest http://t.co/CuQGby6kvD Shift into a “Mobile First” mindset!
    April 10, 2014 at 3:59 PM
  • Interesting - 6 #programming paradigms that change how u think about coding: http://t.co/QpRdx76Sn2 & its discussion: http://t.co/DVBRstecba
    April 10, 2014 at 10:11 AM
  • DYK? When we share/RT/blog/etc, it doesn't mean that Keyhole endorses it - we just like variety of opinions! Info: http://t.co/MXhB9lE9tV
    April 9, 2014 at 2:13 PM
  • Developers, need a chuckle? 12 Problems Only Programmers Understand - http://t.co/8PxJSYg0FA #funny
    April 9, 2014 at 2:00 PM
  • Immediately looking to add to our team a Sr. C# developer with knowledge of #NodeJS, #Marionette & #MongoDB. Details: http://t.co/Yyq0b6iza3
    April 9, 2014 at 1:27 PM
  • A huge welcome to Vince Pendergrass who joins the Keyhole team this week!
    April 8, 2014 at 2:37 PM
  • We have 5 Keyhole folks with birthdays this week! Happy birthdays to @Judyj5, @zachagardner, @bmongar, @brianletteri & Mark D!
    April 8, 2014 at 12:22 PM
  • RT @tomonezero: #MongoDB 2.6 is out – Our Biggest Release Ever http://t.co/7iIDYU3CKF
    April 8, 2014 at 9:02 AM
  • @kcjobseekers Thank you for the RT! Have a fantastic day.
    April 7, 2014 at 2:16 PM
  • Immediately looking to add to our team a Sr. C# developer with knowledge of #NodeJS, #Marionette, #MongoDB. Details - http://t.co/Yyq0b6iza3
    April 7, 2014 at 2:10 PM
  • Need to get up-to-speed fast? We train dev teams. Here's one of our newest courses covering UI dev with #AngularJS: http://t.co/Bf3UuClj4Z
    April 7, 2014 at 9:20 AM
  • “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” - Martin Fowler
    April 4, 2014 at 11:36 AM
  • No better cure for IT stress than friendly humor. Here are 6 funny moments from Keith Shakib's development career - http://t.co/m7qX9q98EB
    April 4, 2014 at 10:21 AM
  • This could come in handy - 75 Essential Cheat Sheets for Designers & Programmers: http://t.co/ST9YnSqdHD #HTML5 #Java #JavaScript #SQL etc.
    April 3, 2014 at 4:43 PM
  • No better cure for stress than friendly humor. Use itwisely & it can be one of your most important #softskills - http://t.co/m7qX9q98EB
    April 3, 2014 at 10:32 AM
  • Keith's favorite #funny line of all time came from one of the most kind & loveable programmers he has ever met - http://t.co/m7qX9q98EB
    April 2, 2014 at 2:25 PM