views:

4674

answers:

5

Is there any easy way to create a class that uses IFormatProvider that writes out a user-friendly file-size?

public static string GetFileSizeString(string filePath)
{
    FileInfo info = new FileInfo(@"c:\windows\notepad.exe");
    long size = info.Length;
    string sizeString = size.ToString(FileSizeFormatProvider); // This is where the class does its magic...
}

It should result in strings formatted something like "2,5 MB", "3,9 GB", "670 bytes" and so on.

+32  A: 

I use this one, I get it from the web

public class FileSizeFormatProvider : IFormatProvider, ICustomFormatter
{
    public object GetFormat(Type formatType)
    {
        if (formatType == typeof(ICustomFormatter)) return this;
        return null;
    }

    private const string fileSizeFormat = "fs";
    private const Decimal OneKiloByte = 1024M;
    private const Decimal OneMegaByte = OneKiloByte * 1024M;
    private const Decimal OneGigaByte = OneMegaByte * 1024M;

    public string Format(string format, object arg, IFormatProvider formatProvider)
    {    
        if (format == null || !format.StartsWith(fileSizeFormat))    
        {    
            return defaultFormat(format, arg, formatProvider);    
        }

        if (arg is string)    
        {    
            return defaultFormat(format, arg, formatProvider);    
        }

        Decimal size;

        try    
        {    
            size = Convert.ToDecimal(arg);    
        }    
        catch (InvalidCastException)    
        {    
            return defaultFormat(format, arg, formatProvider);    
        }

        string suffix;
        if (size > OneGigaByte)
        {
            size /= OneGigaByte;
            suffix = "GB";
        }
        else if (size > OneMegaByte)
        {
            size /= OneMegaByte;
            suffix = "MB";
        }
        else if (size > OneKiloByte)
        {
            size /= OneKiloByte;
            suffix = "kB";
        }
        else
        {
            suffix = " B";
        }

        string precision = format.Substring(2);
        if (String.IsNullOrEmpty(precision)) precision = "2";
        return String.Format("{0:N" + precision + "}{1}", size, suffix);

    }

    private static string defaultFormat(string format, object arg, IFormatProvider formatProvider)
    {
        IFormattable formattableArg = arg as IFormattable;
        if (formattableArg != null)
        {
            return formattableArg.ToString(format, formatProvider);
        }
        return arg.ToString();
    }

}

an example of use would be:

Console.WriteLine(String.Format(new FileSizeFormatProvider(), "File size: {0:fs}", 100));
Console.WriteLine(String.Format(new FileSizeFormatProvider(), "File size: {0:fs}", 10000));

Credits for http://flimflan.com/blog/FileSizeFormatProvider.aspx

There is a problem with ToString(), it's expecting a NumberFormatInfo type that implements IFormatProvider but the NumberFormatInfo class is sealed :(

If you're using C# 3.0 you can use an extension method to get the result you want:

public static class ExtensionMethods
{
    public static string ToFileSize(this long l)
    {
        return String.Format(new FileSizeFormatProvider(), "{0:fs}", l);
    }
}

You can use it like this.

long l = 100000000;
Console.WriteLine(l.ToFileSize());

Hope this helps.

Eduardo Campañó
Good answer. Would be even better if you edited to include an example of usage.
AR
+7  A: 

OK I'm not going to wrap it up as a Format provider but rather than reinventing the wheel there's a Win32 api call to format a size string based on supplied bytes that I've used many times in various applications.

[DllImport("Shlwapi.dll", CharSet = CharSet.Auto)]
public static extern long StrFormatByteSize( long fileSize, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder buffer, int bufferSize );

So I imagine you should be able to put together a provider using that as the core conversion code.

Here's a link to the MSDN spec for StrFormatByteSize.

Shaun Austin
+1  A: 

My code... thanks for Shaun Austin.

[DllImport("Shlwapi.dll", CharSet = CharSet.Auto)]
public static extern long StrFormatByteSize(long fileSize, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder buffer, int bufferSize);

public void getFileInfo(string filename)
{
    System.IO.FileInfo fileinfo = new FileInfo(filename);
    this.FileName.Text = fileinfo.Name;
    StringBuilder buffer = new StringBuilder();
    StrFormatByteSize(fileinfo.Length, buffer, 100);
    this.FileSize.Text = buffer.ToString();
}
ariso
+1  A: 

I have taken Eduardo's answer and combined it with a similar example from elsewhere to provide additional options for the formatting.

public class FileSizeFormatProvider : IFormatProvider, ICustomFormatter
{
   public object GetFormat(Type formatType)
   {
      if (formatType == typeof(ICustomFormatter))
      {
         return this;
      }

      return null;
   }

   private const string fileSizeFormat = "FS";
   private const string kiloByteFormat = "KB";
   private const string megaByteFormat = "MB";
   private const string gigaByteFormat = "GB";
   private const string byteFormat = "B";
   private const Decimal oneKiloByte = 1024M;
   private const Decimal oneMegaByte = oneKiloByte * 1024M;
   private const Decimal oneGigaByte = oneMegaByte * 1024M;

   public string Format(string format, object arg, IFormatProvider formatProvider)
   {
      //
      // Ensure the format provided is supported
      //
      if (String.IsNullOrEmpty(format) || !(format.StartsWith(fileSizeFormat, StringComparison.OrdinalIgnoreCase) ||
                                            format.StartsWith(kiloByteFormat, StringComparison.OrdinalIgnoreCase) ||
                                            format.StartsWith(megaByteFormat, StringComparison.OrdinalIgnoreCase) ||
                                            format.StartsWith(gigaByteFormat, StringComparison.OrdinalIgnoreCase)))
      {
         return DefaultFormat(format, arg, formatProvider);
      }

      //
      // Ensure the argument type is supported
      //
      if (!(arg is long || arg is decimal || arg is int))
      {
         return DefaultFormat(format, arg, formatProvider);
      }

      //
      // Try and convert the argument to decimal
      //
      Decimal size;

      try
      {
         size = Convert.ToDecimal(arg);
      }
      catch (InvalidCastException)
      {
         return DefaultFormat(format, arg, formatProvider);
      }

      //
      // Determine the suffix to use and convert the argument to the requested size
      //
      string suffix;

      switch (format.Substring(0, 2).ToUpper())
      {
         case kiloByteFormat:
            size = size / oneKiloByte;
            suffix = kiloByteFormat;
            break;
         case megaByteFormat:
            size = size / oneMegaByte;
            suffix = megaByteFormat;
            break;
         case gigaByteFormat:
            size = size / oneGigaByte;
            suffix = gigaByteFormat;
            break;
         case fileSizeFormat:
            if (size > oneGigaByte)
            {
               size /= oneGigaByte;
               suffix = gigaByteFormat;
            }
            else if (size > oneMegaByte)
            {
               size /= oneMegaByte;
               suffix = megaByteFormat;
            }
            else if (size > oneKiloByte)
            {
               size /= oneKiloByte;
               suffix = kiloByteFormat;
            }
            else
            {
               suffix = byteFormat;
            }
            break;
         default:
            suffix = byteFormat;
            break;
      }

      //
      // Determine the precision to use
      //
      string precision = format.Substring(2);

      if (String.IsNullOrEmpty(precision))
      {
         precision = "2";
      }

      return String.Format("{0:N" + precision + "}{1}", size, suffix);
   }

   private static string DefaultFormat(string format, object arg, IFormatProvider formatProvider)
   {
      IFormattable formattableArg = arg as IFormattable;

      if (formattableArg != null)
      {
         return formattableArg.ToString(format, formatProvider);
      }

      return arg.ToString();
   }
}
Tyler Durden
+3  A: 

I realize now that you were actually asking for something that would work with String.Format() - I guess I should have read the question twice before posting ;-)

I don't like the solution where you have to explicitly pass in a format provider every time - from what I could gather from this article, the best way to approach this, is to implement a FileSize type, implementing the IFormattable interface.

I went ahead and implemented a struct that supports this interface, and which can be cast from an integer. In my own file-related APIs, I will have my .FileSize properties return a FileSize instance.

Here's the code:

using System.Globalization;

public struct FileSize : IFormattable
{
    private ulong _value;

    private const int DEFAULT_PRECISION = 2;

    private static IList<string> Units;

    static FileSize()
    {
        Units = new List<string>(){
            "B", "KB", "MB", "GB", "TB"
        };
    }

    public FileSize(ulong value)
    {
        _value = value;
    }

    public static explicit operator FileSize(ulong value)
    {
        return new FileSize(value);
    }

    override public string ToString()
    {
        return ToString(null, null);
    }

    public string ToString(string format)
    {
        return ToString(format, null);
    }

    public string ToString(string format, IFormatProvider formatProvider)
    {
        int precision;

        if (String.IsNullOrEmpty(format))
            return ToString(DEFAULT_PRECISION);
        else if (int.TryParse(format, out precision))
            return ToString(precision);
        else
            return _value.ToString(format, formatProvider);
    }

    /// <summary>
    /// Formats the FileSize using the given number of decimals.
    /// </summary>
    public string ToString(int precision)
    {
        double pow = Math.Floor((_value > 0 ? Math.Log(_value) : 0) / Math.Log(1024));
        pow = Math.Min(pow, Units.Count - 1);
        double value = (double)_value / Math.Pow(1024, pow);
        return value.ToString(pow == 0 ? "F0" : "F" + precision.ToString()) + " " + Units[(int)pow];
    }
}

And a simple Unit Test that demonstrates how this works:

    [Test]
    public void CanUseFileSizeFormatProvider()
    {
        Assert.AreEqual(String.Format("{0}", (FileSize)128), "128 B");
        Assert.AreEqual(String.Format("{0}", (FileSize)1024), "1.00 KB");
        Assert.AreEqual(String.Format("{0:0}", (FileSize)10240), "10 KB");
        Assert.AreEqual(String.Format("{0:1}", (FileSize)102400), "100.0 KB");
        Assert.AreEqual(String.Format("{0}", (FileSize)1048576), "1.00 MB");
        Assert.AreEqual(String.Format("{0:D}", (FileSize)123456), "123456");

        // You can also manually invoke ToString(), optionally with the precision specified as an integer:
        Assert.AreEqual(((FileSize)111111).ToString(2), "108.51 KB");
    }

As you can see, the FileSize type can now be formatted correctly, and it is also possible to specify the number of decimals, as well as applying regular numeric formatting if required.

I guess you could take this much further, for example allowing explicit format selection, e.g. "{0:KB}" to force formatting in kilobytes. But I'm going to leave it at this.

I'm also leaving my initial post below for those two prefer not to use the formatting API...


100 ways to skin a cat, but here's my approach - adding an extension method to the int type:

public static class IntToBytesExtension
{
    private const int PRECISION = 2;

    private static IList<string> Units;

    static IntToBytesExtension()
    {
        Units = new List<string>(){
            "B", "KB", "MB", "GB", "TB"
        };
    }

    /// <summary>
    /// Formats the value as a filesize in bytes (KB, MB, etc.)
    /// </summary>
    /// <param name="bytes">This value.</param>
    /// <returns>Filesize and quantifier formatted as a string.</returns>
    public static string ToBytes(this int bytes)
    {
        double pow = Math.Floor((bytes>0 ? Math.Log(bytes) : 0) / Math.Log(1024));
        pow = Math.Min(pow, Units.Count-1);
        double value = (double)bytes / Math.Pow(1024, pow);
        return value.ToString(pow==0 ? "F0" : "F" + PRECISION.ToString()) + " " + Units[(int)pow];
    }
}

With this extension in your assembly, to format a filesize, simply use a statement like (1234567).ToBytes()

The following MbUnit test clarifies precisely what the output looks like:

    [Test]
    public void CanFormatFileSizes()
    {
        Assert.AreEqual("128 B", (128).ToBytes());
        Assert.AreEqual("1.00 KB", (1024).ToBytes());
        Assert.AreEqual("10.00 KB", (10240).ToBytes());
        Assert.AreEqual("100.00 KB", (102400).ToBytes());
        Assert.AreEqual("1.00 MB", (1048576).ToBytes());
    }

And you can easily change the units and precision to whatever suits your needs :-)

mindplay.dk