get type of NSNumber
Asked Answered
E

12

43

I want to get the type of NSNumber instance.

I found out on http://www.cocoadev.com/index.pl?NSNumber this:

 NSNumber *myNum = [[NSNumber alloc] initWithBool:TRUE];

 if ([[myNum className] isEqualToString:@"NSCFNumber"]) {
  // process NSNumber as integer
 } else if  ([[myNum className] isEqualToString:@"NSCFBoolean"]) {
  // process NSNumber as boolean
 }

Ok, but this doesn't work, the [myNum className] isn't recognized by the compiler. I'm compiling for iPhone.

Earlie answered 25/3, 2010 at 19:34 Comment(3)
This is potentially fragile. NSCFNumber and NSCFBoolean are private and there's no guarantee that those will continue being the class names in the future.Islek
How about if([myNum class] == [[NSNumber numberWithBool:YES] class])Faubourg
@GlennHowes No guarantee that that will yield the correct result. You make the same assumption as @okami; that private interfaces will never change.Childers
D
75

I recommend using the -[NSNumber objCType] method.

It allows you to do:

NSNumber * n = [NSNumber numberWithBool:YES];
if (strcmp([n objCType], @encode(BOOL)) == 0) {
    NSLog(@"this is a bool");
} else if (strcmp([n objCType], @encode(int)) == 0) {
    NSLog(@"this is an int");
}

For more information on type encodings, check out the Objective-C Runtime Reference.

Dedication answered 25/3, 2010 at 19:42 Comment(7)
+1 cool tip! Strange that they don't have a method to get this out of NSNumber much easier.Annabelle
This doesn't work - at least on iOS: (lldb) p (char *)[[NSNumber numberWithBool:YES] objCType] - it encodes the bool as a char internally (which is correct at the machine level, but not at the intentional level)Pauly
This information is dated, it won't work on 64-bit iOS devices and simulators and therefore it should not be used. It can lead to very hard to find issues which will only occur on 64-bit iOS devices.Wispy
@Wispy In what way does it fail on 64-bit devices?Elfont
The documentation notes a very important special consideration: "The returned type does not necessarily match the method the number object was created with."Halsey
@EthanHolshouser On 32-bit systems, both [n objCType] and @encode(BOOL) return "c", but on 64-bit systems, @encode(BOOL) returns "B". This seems like a bug in NSNumber to me.Francklyn
It's because of how the BOOL type is defined: #if (TARGET_OS_IPHONE && __LP64__) || TARGET_OS_WATCH #define OBJC_BOOL_IS_BOOL 1 typedef bool BOOL; #else #define OBJC_BOOL_IS_CHAR 1 typedef signed char BOOL; // BOOL is explicitly signed so @encode(BOOL) == "c" rather than "C" // even if -funsigned-char is used. #endif Pilferage
S
50

You can get the type this way, no string comparisons needed:

CFNumberType numberType = CFNumberGetType((CFNumberRef)someNSNumber);

numberType will then be one of:

enum CFNumberType {
   kCFNumberSInt8Type = 1,
   kCFNumberSInt16Type = 2,
   kCFNumberSInt32Type = 3,
   kCFNumberSInt64Type = 4,
   kCFNumberFloat32Type = 5,
   kCFNumberFloat64Type = 6,
   kCFNumberCharType = 7,
   kCFNumberShortType = 8,
   kCFNumberIntType = 9,
   kCFNumberLongType = 10,
   kCFNumberLongLongType = 11,
   kCFNumberFloatType = 12,
   kCFNumberDoubleType = 13,
   kCFNumberCFIndexType = 14,
   kCFNumberNSIntegerType = 15,
   kCFNumberCGFloatType = 16,
   kCFNumberMaxType = 16
};
typedef enum CFNumberType CFNumberType;
Semolina answered 13/10, 2011 at 0:27 Comment(3)
Unfortunately, there is no kCFNumberBoolType to distinguish boolean values from characters, so this does not work for all cases.Latchet
Both kCFNumberCharType == CFNumberGetType((__bridge CFNumberRef)nsValue) and 0 == strcmp([nsValue objCType], "c") work on 32 and 64bit systems, but if you're unfortunate enough to support code that cares, I think the former "feels" marginally safer.Allelomorph
@MichaelManner you should be using Unichar and/or NSValue for storing characters, not NSNumber.Sprage
T
30

If all you want is to differentiate between booleans and anything else, you can make use of the fact that boolean NSNumbers always return a shared instance:

NSNumber *num = ...;
if (num == (void*)kCFBooleanFalse || num == (void*)kCFBooleanTrue) {
   // num is boolean
} else {
   // num is not boolean
}
Tolan answered 30/4, 2013 at 9:38 Comment(11)
+1 for the ingenuity, but I wouldn't recommend it for production code (the solution is waaaay too fragile...)Reject
It depends what the failure mode is. I use it to display "TRUE" or "FALSE" for Booleans; in the unlikely case Apple changes this implementation detail, my app would display "1" or "0" instead; I can live with that. (Especially since there is no alternative besides writing your own NSNumber subclass that keeps track of the type it was created with)Tolan
Granted: your solution is the best so far and, as you correctly point out, the only one (about the NSNumber subclass: I came here because I wanted to distinguish booleans vs integers in a plist, much good it would do to me...). Also granted that in case of failure, the solution would return correct truthy and falsy values. Point is, either you care whether a value is a boolean (and you can't accept false negatives), or you don't (so why bother? :) )Reject
if(num == [NSNumbler numberWithBool:YES] || ... also worksBoz
You could even use if (num==@(YES) || num==@(NO)). But using Core Foundation constants looks really sophisticated, while comparing Objective C objects with == looks like a newbie mistake ;)Tolan
@JakobEgger Where is it documented that NSNumber always returns a shared instance for boolean values? I haven't been able to verify that. However, [num isEqual:@YES] || [num isEqual:@NO] should work in either case.Francklyn
@GregBrown To my knowledge it is not officially documented. But it is officially documented that NSNumber is toll-free bridged to CFNumberRef, and if you look at CFNumber.c it's obvious that all the CFBoolean* functions just compare against a static pointer (kCFBooleanTrue). Therefore NSNumber must return the shared instance, otherwise it would not be compatible with CFBoolean.Tolan
@GregBrown [num isEqual:@YES] will not work, because [@1 isEqual:@YES] will return true!Tolan
@JakobEgger I just discovered that. It's annoying. A number should never be logically equal to a boolean. In any case, I'm reluctant to rely on an undocumented solution. In fact, I tried your approach earlier today and Xcode generates this warning: "warning: direct comparison of a numeric literal has undefined behavior".Francklyn
The original num == (void*)kCFBooleanFalse code does not generate a warning. If you need a distinct Boolean type, and don't want to rely on an implementation detail, I recommend creating a custom class to represent boolean values.Tolan
@JakobEgger A custom class won't work for me - I need to support native Swift Bool types. The original solution may be OK for my needs, but your second suggestion does generate a warning.Francklyn
G
9

NSNumber explicitly doesn't guarantee that the returned type will match the method used to create it, so doing this at all is probably a bad idea.

However, you could probably do something like this (you could also compare to objc_getClass("NSCFNumber") etc., but this is arguably more portable):

Class boolClass = [[NSNumber numberWithBool:YES] class];
/* ... */
if([myNum isKindOfClass:boolClass]) {
  /* ... */
}
Gynophore answered 27/3, 2010 at 3:55 Comment(2)
In my tests this method fails on iPad with iOS 8.4Goggler
this is bad it is not a boolClass but numberClass with bool valueThrow
P
7

In Swift:

let numberType = CFNumberGetType(answer)

switch numberType {
case .charType:
    //Bool
case .sInt8Type, .sInt16Type, .sInt32Type, .sInt64Type, .shortType, .intType, .longType, .longLongType, .cfIndexType, .nsIntegerType:
    //Int
case .float32Type, .float64Type, .floatType, .doubleType, .cgFloatType:
    //Double
}
Pee answered 6/12, 2016 at 16:17 Comment(2)
Could be correct, but please note that Swift can distinguish between Float32 (aka Float), Float64 (aka Double) and Float80, so maybe, just maybe, in case of .float32Type and .floatType, it is a Float instead. We would need a demonstration or a reference to verify.Sprage
Well, seeing that there are only three literal initializers for NSNumber (Int, Double, Bool), I guess that this answer is as accurate as Apple Swift developers want it to be.Sprage
U
5

Use the method -[NSNumber objCType] method to get the type.

If the type's equal to @encode(BOOL), or the number itself is kCFBooleanFalse, or kCFBooleanTrue, it's a boolean.

If it's anything else but 'c', it's a number.

If it's 'c', what appears to be the only way supported way, without checking against private class names, or comparing against undocumented singletons, is to turn make an array of one element, the number, and then use NSJSONSerialization to get the string representation. Finally, check if the string representation contains the string "true" or "false". Here is the full code for checking if an NSNumber is a BOOL:

-(BOOL)isBool
{
    if(!strcmp(self.objCType, @encode(BOOL)) ||
        self == (void*)kCFBooleanFalse ||
        self == (void*)kCFBooleanTrue)
    {
        return YES;
    }

    if(strcmp(self.objCType, "c"))
    {
        return NO;
    }

    NSString * asString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:@[self] options:kNilOptions error:nil] encoding:NSUTF8StringEncoding];

    return [asString containsString:@"true"] || [asString containsString:@"false"];
}

Note that using NSJSONSerialization is slow and if @NO/@YES ever stops always equalling kCFBooleanFalse/kCFBooleanTrue, then this method probably shouldn't be used in a tight loop.

Upali answered 14/12, 2015 at 14:5 Comment(0)
D
4

The reason the compiler warns you and it doesn't work is because -[NSObject className] is declared in a category on NSObject on Mac OS X (in NSScriptClassDescription.h) and not declared on iPhone. (It doesn't support AppleScript, obviously.) NSStringFromClass([myNum class]) is what you should use to be safe across all platforms. Odds are that -className is declared as a simple wrapper around NSStringFromClass() anyway...

Dyak answered 25/3, 2010 at 19:48 Comment(0)
N
3
NSString *classString = NSStringFromClass([myNum class]);

That should ger the string you want.

Nixon answered 25/3, 2010 at 19:36 Comment(1)
isn't there a way to get the type of the NSNumber without comparing with strings? I know that there is the "objCType" method, but each time I retrieve it, it returns a different value.Earlie
I
0

To check that NSNumber contains a bool value Try this:

if (strcmp([myNumber objCType], [@(YES) objCType]) == 0)
NSLog(@"%@", [myNumber boolValue] ? @"true" : @"false");
Invar answered 27/8, 2016 at 6:19 Comment(0)
W
-1

objCType documentation states that The returned type does not necessarily match the method the number object was created with

Secondly, other methods of comparing the class of number to a given class type or assuming boolean number instances to be shared singletons are not documented behaviour.

A more(not completely though) reliable way is to depend on NSJSONSerialisation as it correctly recognises number instances created with bool and outputs true/false in json. This is something we can expect Apple to take care of while moving with new SDKs and on different architectures. Below is the code:

+(BOOL) isBoolType:(NSNumber*) number {

    NSError* err;
    NSData* jsonData = [NSJSONSerialization dataWithJSONObject:@{@"key":number}
                                                       options:0
                                                         error:&err];

    NSString* jsonString = [[NSString alloc] 
                              initWithData:jsonData
                                  encoding:NSUTF8StringEncoding];

    return [jsonString containsString:@"true"]
            || [jsonString containsString:@"false"];

}
Wisla answered 18/5, 2017 at 14:40 Comment(1)
Having the overhead of a JSON tokenizer and a long arbitrary String is not a way I would recommend to deal with Boolean detection. Jakob Egger answer is more elegant and is backed up by documentation that NSNumber is toll-free bridged to CFNumberRef, itself directly comparable to two static values.Sprage
B
-1

Swift Version

NSNumber is a class-cluster so each underlying type can be figured from the instance. This code avoids hard-coding the different NSNumber types by creating an instance of the expected type, and then comparing it against the unknown type.

extension NSNumber {
    var isBool: Bool {
        return type(of: self) == type(of: NSNumber(booleanLiteral: true))
    }
}
Bursarial answered 4/4, 2018 at 2:41 Comment(4)
This will only work to distinguish boolean from non-boolean. For instance, an NSNumber initialized from an integerLiteral and from a floatLiteral will share the same class-cluster, so your NSNumber(floatLiteral: 0.1).isInt is true. And there is a well-known NSNumber subclass that will not compare well: NSDecimalNumber(integerLiteral: 0).isInt is false.Sprage
Apple and people's frameworks are free to create additional subclasses of NSNumber supporting a boolean, so this solution shouldn't claim it's future-proof.Sprage
@Cœur thank you for the note, what would you suggest as the best way to do this?Bursarial
ChikabuZ answer (with CFNumberGetType) is currently my favourite: most accurate info we can get in the general case.Sprage
C
-2

check object is of NSNumber type :

if([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]) { //NSNumber }

Crinum answered 26/2, 2014 at 11:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.