Search code examples
objective-ccocoa

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


Is there a simple way to do something like..

[NSMagicDataConverter humanStringWithBytes:20000000]

..which would return "19.1MB"?


Solution

  • 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;
    }