ObjC/Cocoa class for converting size to human-readable string?
Asked Answered
P

9

28

Is there a simple way to do something like..

[NSMagicDataConverter humanStringWithBytes:20000000]

..which would return "19.1MB"?

Prussia answered 21/2, 2009 at 8:11 Comment(2)
NSByteCountFormatter in iOS 6.0 or later and OS X 10.8 or laterCollectivize
https://mcmap.net/q/369278/-how-to-get-file-size-properly-and-convert-it-to-mb-gb-in-cocoa-duplicateWeissman
S
18

Here's my own take on the problem:

enum {
    kUnitStringBinaryUnits     = 1 << 0,
    kUnitStringOSNativeUnits   = 1 << 1,
    kUnitStringLocalizedFormat = 1 << 2
};

NSString* unitStringFromBytes(double bytes, uint8_t flags){

    static const char units[] = { '\0', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' };
    static int maxUnits = sizeof units - 1;

    int multiplier = (flags & kUnitStringOSNativeUnits && !leopardOrGreater() || flags & kUnitStringBinaryUnits) ? 1024 : 1000;
    int exponent = 0;

    while (bytes >= multiplier && exponent < maxUnits) {
        bytes /= multiplier;
        exponent++;
    }
    NSNumberFormatter* formatter = [[[NSNumberFormatter alloc] init] autorelease];
    [formatter setMaximumFractionDigits:2];
    if (flags & kUnitStringLocalizedFormat) {
        [formatter setNumberStyle: NSNumberFormatterDecimalStyle];
    }
    // Beware of reusing this format string. -[NSString stringWithFormat] ignores \0, *printf does not.
    return [NSString stringWithFormat:@"%@ %cB", [formatter stringFromNumber: [NSNumber numberWithDouble: bytes]], units[exponent]];
}

By default (if 0 is passed for flags), it will output SI units (base ten). You can set kUnitStringBinaryUnits to select binary (base two) units suitable for memory, or kUnitStringOSNativeUnits to have the unit type selected automatically based on OS version (pre-Leopard gets base two, post-Leopard gets base ten). Setting kUnitStringLocalizedFormat formats the string based on the user's current locale. For example:

unitStringFromBytes(1073741824, 0); // → "1.07 GB"
unitStringFromBytes(1073741824, kUnitStringBinaryUnits); // → "1 GB"
unitStringFromBytes(1073741824, kUnitStringOSNativeUnits | kUnitStringLocalizedFormat); // → "1.07 GB" (In Mac OS 10.6)
unitStringFromBytes(12345678901234567890123456789, kUnitStringOSNativeUnits | kUnitStringLocalizedFormat); // → "12,345.68 YB" (In Mac OS 10.6, in the US)
unitStringFromBytes(12345678901234567890123456789, kUnitStringOSNativeUnits | kUnitStringLocalizedFormat); // → "12.345,68 YB" (In Mac OS 10.6, in Spain)

Here's the helper function required for OS-native units:

BOOL leopardOrGreater(){
    static BOOL alreadyComputedOS = NO;
    static BOOL leopardOrGreater = NO;
    if (!alreadyComputedOS) {
        SInt32 majorVersion, minorVersion;
        Gestalt(gestaltSystemVersionMajor, &majorVersion);
        Gestalt(gestaltSystemVersionMinor, &minorVersion);
        leopardOrGreater = ((majorVersion == 10 && minorVersion >= 5) || majorVersion > 10);
        alreadyComputedOS = YES;
    }
    return leopardOrGreater;
}
Scarf answered 4/6, 2010 at 15:40 Comment(0)
L
106

Starting in OS X 10.8 and iOS 6, you can use NSByteCountFormatter.

Your example would look like this:

[NSByteCountFormatter stringFromByteCount:20000000 countStyle:NSByteCountFormatterCountStyleFile];
Landscapist answered 1/10, 2012 at 13:53 Comment(1)
The only downside to using this is that the format is forced using the current locale.Euphonize
D
21

I would mush this into an NSFormatter subclass.

#import <Foundation/Foundation.h>

@interface SOFileSizeFormatter : NSNumberFormatter 
{
    @private
    BOOL useBaseTenUnits;
}

/** Flag signaling whether to calculate file size in binary units (1024) or base ten units (1000).  Default is binary units. */
@property (nonatomic, readwrite, assign, getter=isUsingBaseTenUnits) BOOL useBaseTenUnits;

@end

static const char sUnits[] = { '\0', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' };
static int sMaxUnits = sizeof sUnits - 1;

@implementation SOFileSizeFormatter

@synthesize useBaseTenUnits;

- (NSString *) stringFromNumber:(NSNumber *)number
{
    int multiplier = useBaseTenUnits ? 1000 : 1024;
    int exponent = 0;

    double bytes = [number doubleValue];

    while ((bytes >= multiplier) && (exponent < sMaxUnits)) {
        bytes /= multiplier;
        exponent++;
    }

    return [NSString stringWithFormat:@"%@ %cB", [super stringFromNumber: [NSNumber numberWithDouble: bytes]], sUnits[exponent]];
}

@end

Usage:

NSString *path = ...; // path to a file of 1,500,000 bytes
NSString *sizeString = nil;

NSNumber *sizeAttrib = [[[NSFileManager defaultManager] attributesOfItemAtPath:path error:NULL]objectForKey:NSFileSize];

SOFileSizeFormatter *sizeFormatter = [[[SOFileSizeFormatter alloc] init] autorelease];
[sizeFormatter setMaximumFractionDigits:2];

sizeString = [sizeFormatter stringFromNumber:sizeAttrib];
// sizeString ==> @"1.43 MB"

[sizeFormatter setUseBaseTenUnits:YES];
sizeString = [sizeFormatter stringFromNumber:sizeAttrib];
// sizeString ==> @"1.5 MB"
Deathbed answered 17/1, 2011 at 18:31 Comment(1)
I did this but have just two methods: stringFromSize: && stringFromSpeed:Nic
S
18

Here's my own take on the problem:

enum {
    kUnitStringBinaryUnits     = 1 << 0,
    kUnitStringOSNativeUnits   = 1 << 1,
    kUnitStringLocalizedFormat = 1 << 2
};

NSString* unitStringFromBytes(double bytes, uint8_t flags){

    static const char units[] = { '\0', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' };
    static int maxUnits = sizeof units - 1;

    int multiplier = (flags & kUnitStringOSNativeUnits && !leopardOrGreater() || flags & kUnitStringBinaryUnits) ? 1024 : 1000;
    int exponent = 0;

    while (bytes >= multiplier && exponent < maxUnits) {
        bytes /= multiplier;
        exponent++;
    }
    NSNumberFormatter* formatter = [[[NSNumberFormatter alloc] init] autorelease];
    [formatter setMaximumFractionDigits:2];
    if (flags & kUnitStringLocalizedFormat) {
        [formatter setNumberStyle: NSNumberFormatterDecimalStyle];
    }
    // Beware of reusing this format string. -[NSString stringWithFormat] ignores \0, *printf does not.
    return [NSString stringWithFormat:@"%@ %cB", [formatter stringFromNumber: [NSNumber numberWithDouble: bytes]], units[exponent]];
}

By default (if 0 is passed for flags), it will output SI units (base ten). You can set kUnitStringBinaryUnits to select binary (base two) units suitable for memory, or kUnitStringOSNativeUnits to have the unit type selected automatically based on OS version (pre-Leopard gets base two, post-Leopard gets base ten). Setting kUnitStringLocalizedFormat formats the string based on the user's current locale. For example:

unitStringFromBytes(1073741824, 0); // → "1.07 GB"
unitStringFromBytes(1073741824, kUnitStringBinaryUnits); // → "1 GB"
unitStringFromBytes(1073741824, kUnitStringOSNativeUnits | kUnitStringLocalizedFormat); // → "1.07 GB" (In Mac OS 10.6)
unitStringFromBytes(12345678901234567890123456789, kUnitStringOSNativeUnits | kUnitStringLocalizedFormat); // → "12,345.68 YB" (In Mac OS 10.6, in the US)
unitStringFromBytes(12345678901234567890123456789, kUnitStringOSNativeUnits | kUnitStringLocalizedFormat); // → "12.345,68 YB" (In Mac OS 10.6, in Spain)

Here's the helper function required for OS-native units:

BOOL leopardOrGreater(){
    static BOOL alreadyComputedOS = NO;
    static BOOL leopardOrGreater = NO;
    if (!alreadyComputedOS) {
        SInt32 majorVersion, minorVersion;
        Gestalt(gestaltSystemVersionMajor, &majorVersion);
        Gestalt(gestaltSystemVersionMinor, &minorVersion);
        leopardOrGreater = ((majorVersion == 10 && minorVersion >= 5) || majorVersion > 10);
        alreadyComputedOS = YES;
    }
    return leopardOrGreater;
}
Scarf answered 4/6, 2010 at 15:40 Comment(0)
P
6
NSString *stringFromFileSize(NSInteger theSize)
{
    /*
     From http://snippets.dzone.com/posts/show/3038 with slight modification
     */
    float floatSize = theSize;
    if (theSize<1023)
        return([NSString stringWithFormat:@"%i bytes",theSize]);
    floatSize = floatSize / 1024;
    if (floatSize<1023)
        return([NSString stringWithFormat:@"%1.1f KB",floatSize]);
    floatSize = floatSize / 1024;
    if (floatSize<1023)
        return([NSString stringWithFormat:@"%1.1f MB",floatSize]);
    floatSize = floatSize / 1024;

    return([NSString stringWithFormat:@"%1.1f GB",floatSize]);
}
Prussia answered 21/2, 2009 at 8:18 Comment(4)
I would recommend making the 'theSize' parameter be 'size_t' type, which is a 64-bit integer. The above method would fail after 2 gigabytes.Discrimination
Please note that using 1024 as the base as opposed to 1000 is not consistent with the Human Interface Guidelines, and as such, any app using this code could be rejected from the App Store.Perversity
String with format does not respect localisations (e.g. decimal separator symbols) - this should be done with a number formatter as seen aboveOssify
I agree, NSInteger will not hold an unsigned long long number and you lose control over having decimal digits. Using NSNumberFormatter is more versatile and a better overall choice.Nic
J
6

Here is a more Objective C-like function (uses NSNumber, NSArray, NSStirng, etc...) for doing this conversion.

This is based on Sidnicious's answer, so a big thanks for the initial work done there. Also based on Wikipedia articles.

Use it generally like this: [HumanReadableDataSizeHelper humanReadableSizeFromBytes:[NSNumber numberWithDouble:doubleValue]].

But, it looks like you want SI units with a 1024 multiplier so you would use it like this: [HumanReadableDataSizeHelper humanReadableSizeFromBytes:[NSNumber numberWithDouble:doubleValue] useSiPrefixes:YES useSiMultiplier:NO]

The reason I default to binary prefixes (ki, Mi) is because those seem to be the most appropriate unit prefix set to use for sizes of data on a computer. What you requested was the SI unit prefixes but using a multiplier of 1024, technically incorrect. Though I will note that SI prefixes for multiples of 1024 is fairly common and binary prefixes are not well accepted (according to Wikipedia).

HumanReadableDataSizeHelper.h

@interface HumanReadableDataSizeHelper : NSObject


/**
    @brief  Produces a string containing the largest appropriate units and the new fractional value.
    @param  sizeInBytes  The value to convert in bytes.

    This function converts the bytes value to a value in the greatest units that produces a value >= 1 and returns the new value and units as a string.

    The magnitude multiplier used is 1024 and the prefixes used are the binary prefixes (ki, Mi, ...).
 */
+ (NSString *)humanReadableSizeFromBytes:(NSNumber *)sizeInBytes;

/**
    @brief  Produces a string containing the largest appropriate units and the new fractional value.
    @param  sizeInBytes  The value to convert in bytes.
    @param  useSiPrefixes  Controls what prefix-set is used.
    @param  useSiMultiplier  Controls what magnitude multiplier is used.

    This function converts the bytes value to a value in the greatest units that produces a value >= 1 and returns the new value and units as a string.

    When useSiPrefixes is true, the prefixes used are the SI unit prefixes (k, M, ...).
    When useSiPrefixes is false, the prefixes used are the binary prefixes (ki, Mi, ...).

    When useSiMultiplier is true, the magnitude multiplier used is 1000
    When useSiMultiplier is false, the magnitude multiplier used is 1024.
 */
+ (NSString *)humanReadableSizeFromBytes:(NSNumber *)sizeInBytes  useSiPrefixes:(BOOL)useSiPrefixes  useSiMultiplier:(BOOL)useSiMultiplier;


@end

HumanReadableDataSizeHelper.m

@implementation HumanReadableDataSizeHelper


+ (NSString *)humanReadableSizeFromBytes:(NSNumber *)sizeInBytes
{
    return [self humanReadableSizeFromBytes:sizeInBytes  useSiPrefixes:NO  useSiMultiplier:NO];
}


+ (NSString *)humanReadableSizeFromBytes:(NSNumber *)sizeInBytes  useSiPrefixes:(BOOL)useSiPrefixes  useSiMultiplier:(BOOL)useSiMultiplier
{
    NSString *unitSymbol = @"B";
    NSInteger multiplier;
    NSArray *prefixes;

    if (useSiPrefixes)
    {
        /*  SI prefixes
         http://en.wikipedia.org/wiki/Kilo-
         kilobyte   (kB)    10^3    
         megabyte   (MB)    10^6    
         gigabyte   (GB)    10^9    
         terabyte   (TB)    10^12   
         petabyte   (PB)    10^15   
         exabyte    (EB)    10^18   
         zettabyte  (ZB)    10^21   
         yottabyte  (YB)    10^24   
         */

        prefixes = [NSArray arrayWithObjects: @"", @"k", @"M", @"G", @"T", @"P", @"E", @"Z", @"Y", nil];
    }
    else
    {
        /*  Binary prefixes
         http://en.wikipedia.org/wiki/Binary_prefix
         kibibyte   (KiB)   2^10 = 1.024 * 10^3
         mebibyte   (MiB)   2^20 ≈ 1.049 * 10^6
         gibibyte   (GiB)   2^30 ≈ 1.074 * 10^9
         tebibyte   (TiB)   2^40 ≈ 1.100 * 10^12
         pebibyte   (PiB)   2^50 ≈ 1.126 * 10^15
         exbibyte   (EiB)   2^60 ≈ 1.153 * 10^18
         zebibyte   (ZiB)   2^70 ≈ 1.181 * 10^21
         yobibyte   (YiB)   2^80 ≈ 1.209 * 10^24
         */

        prefixes = [NSArray arrayWithObjects: @"", @"ki", @"Mi", @"Gi", @"Ti", @"Pi", @"Ei", @"Zi", @"Yi", nil];
    }

    if (useSiMultiplier)
    {
        multiplier = 1000;
    }
    else
    {
        multiplier = 1024;
    }

    NSInteger exponent = 0;
    double size = [sizeInBytes doubleValue];

    while ( (size >= multiplier) && (exponent < [prefixes count]) )
    {
        size /= multiplier;
        exponent++;
    }

    NSNumberFormatter* formatter = [[[NSNumberFormatter alloc] init] autorelease];
    [formatter setMaximumFractionDigits:2];
    [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; // Uses localized number formats.

    NSString *sizeInUnits = [formatter stringFromNumber:[NSNumber numberWithDouble:size]];

    return [NSString stringWithFormat:@"%@ %@%@", sizeInUnits, [prefixes objectAtIndex:exponent], unitSymbol];
}


@end
Joli answered 31/12, 2011 at 19:21 Comment(0)
H
2

You can use FormatterKit and its TTTUnitOfInformationFormatter class:

https://github.com/mattt/FormatterKit

It is also available through CocoaPods with:

pod 'FormatterKit', '~> 1.1.1'
Hetti answered 29/3, 2013 at 11:13 Comment(0)
W
1
- (id)transformedValue:(id)value
{

    double convertedValue = [value doubleValue];
    int multiplyFactor = 0;

    NSArray *tokens = @[@"bytes",@"KB",@"MB",@"GB",@"TB"];

    while (convertedValue > 1024) {
        convertedValue /= 1024;
        multiplyFactor++;
    }

    return [NSString stringWithFormat:@"%4.2f %@",convertedValue, tokens[multiplyFactor]];
}
Weissman answered 12/8, 2013 at 16:46 Comment(1)
Your code is good but on OSX you have to consider multiple of 1000 instead of 1024 since hard disk manufacturer, as well for the Finder, 1 GB = 1000 MB :-)Nightshade
W
0

I know the questions is for Obj C but if anyone looking for a swift version:

 public static func fileSizeDisplay(fromBytes:Int) -> String {
        let display = ["bytes","KB","MB","GB","TB","PB"]
        var value:Double = Double(fromBytes)
        var type = 0
        while (value > 1024){
            value /= 1024
            type = type + 1

        }
        return "\(String(format:"%g", value)) \(display[type])"

    }
Would answered 27/10, 2016 at 8:28 Comment(0)
E
0

Here is an SI only double-to-human method suitable for current macOS and iOS. It converts a positive or negative double in the approximate range 10^-16 <= double <= 10^17 and returns a scaled double < 1000, a power of ten engineering exponent, and 2 SI prefix strings. Although the code works well in all my Apps, its heart is loop-less and unlike other implementations I’ve seen, so I don’t know if I’ve missed a monstrous glitch:

NSInteger exp = ceil( log10( num ) );
if ( lto ) exp += 3;
NSInteger div = pow( 10, exp );
double res = num / div;
NSInteger eng = exp / 3;
NSInteger rem = exp - ( eng * 3 );
if ( rem == 0 &&  res != 1.0 ) { eng--; rem = 3; }
res *= pow( 10, rem );

Here’s sample output showing the positive half of its range:

double n;
NSArray *engNotation;
n = 1;
for ( int i = 0; i < 19; i++ ) {
    engNotation = [self doubleToSIEngineeringNotation:n];
    NSLog(@"%20ld = %9.4f [%@,%@,%@]", (long)n, [engNotation[ D2SIENvalue ] doubleValue], engNotation[ D2SIENexponent ], engNotation[ D2SIENprefixShort ], engNotation[ D2SIENprefixLong ] );
    n *= 10;
}

Output:

                   1 =    1.0000 [0,,]
                  10 =   10.0000 [0,,]
                 100 =  100.0000 [0,,]
                1000 =    1.0000 [3,k,kilo]
               10000 =   10.0000 [3,k,kilo]
              100000 =  100.0000 [3,k,kilo]
             1000000 =    1.0000 [6,M,mega]
            10000000 =   10.0000 [6,M,mega]
           100000000 =  100.0000 [6,M,mega]
          1000000000 =    1.0000 [9,G,giga]
         10000000000 =   10.0000 [9,G,giga]
        100000000000 =  100.0000 [9,G,giga]
       1000000000000 =    1.0000 [12,T,tera]
      10000000000000 =   10.0000 [12,T,tera]
     100000000000000 =  100.0000 [12,T,tera]
    1000000000000000 =    1.0000 [15,P,peta]
   10000000000000000 =   10.0000 [15,P,peta]
  100000000000000000 =  100.0000 [15,P,peta]
 1000000000000000000 =    1.0000 [18,E,exa]

And here's the code:

typedef NS_ENUM(NSInteger, doubleToSIEngineeringNotation) {
    D2SIENvalue = 0,
    D2SIENexponent,
    D2SIENprefixShort,
    D2SIENprefixLong,
};

- (NSArray *)doubleToSIEngineeringNotation:(double)num {

// Convert a positive or negative double in the approximate range 10^-16 <= double <= 10^17
// to a scaled double < 1000 with power of ten engineering exponent and 2 SI prefix strings.
//
// EXIT:  NSArray of four items:
//  [ D2SIENvalue ]       NSNumber: the scaled double
//  [ D2SIENexponent ]    NSNumber: the power of ten engineering exponent
//  [ D2SIENprefixShort ] NSString: the SI prefix abbreviation
//  [ D2SIENprefixLong ]  NSString: the SI prefix long spelling
//
// EX:
//  NSArray *eng = [self doubleToSIEngineeringNotation:[track estimatedDataRate]];
//  NSString *dataRate = [NSString stringWithFormat:@"%.2f %@b/s", [eng[ D2SIENvalue ] doubleValue], eng[ D2SIENprefixShort ]];
//  [videoDict setObject:dataRate forKey:@"Data Rate" ];
//
//  e.g. Data Rate = 72.41 kb/s
//       Data Size =  8.91 MB
//       Weight    = 90.13 micro grams

NSArray *largeSI = @[ @[@"", @""], @[@"k", @"kilo"],  @[@"M", @"mega"],  @[@"G", @"giga"],
                      @[@"T", @"tera"], @[@"P", @"peta"], @[@"E", @"exa"],  @[@"Z", @"zetta"],
                      @[@"Y", @"yotta"], @[@"R", @"ronna"], @[@"Q", @"quetta"] ];

NSArray *smallSI = @[ @[@"", @""], @[@"m", @"milli"], @[@"µ", @"micro"], @[@"n", @"nano"],
                      @[@"p", @"pico"], @[@"f", @"femto"], @[@"a", @"atto"], @[@"z", @"zepto"],
                      @[@"y", @"yocto"], @[@"r", @"ronto"], @[@"r", @"quecto"] ];

if ( num == 0.0 ) return @[ @0.0, @0, @"", @"" ];
bool ngn = num < 0.0 ? YES : NO;
if ( ngn ) { num = -num; }
bool lto = num < 1.0 ? YES : NO;
if ( lto ) { num = 1.0 / num; }

NSInteger exp = ceil( log10( num ) );
if ( lto ) exp += 3;
NSInteger div = pow( 10, exp );
double res = num / div;
NSInteger eng = exp / 3;
NSInteger rem = exp - ( eng * 3 );
if ( rem == 0 &&  res != 1.0 ) { eng--; rem = 3; }
res *= pow( 10, rem );

if ( lto ) { res = 1.0 / res; }
if ( ngn ) res = -res;

return [NSArray arrayWithObjects:
            [NSNumber numberWithDouble:res],
            [NSNumber numberWithInteger:( (eng * 3) * (lto ? -1 : +1 ) )],
            ( lto ? smallSI[eng][0] : largeSI[eng][0] ),
            ( lto ? smallSI[eng][1] : largeSI[eng][1] ),
       nil];
}
Edp answered 2/6, 2024 at 3:47 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.