How to determine the true data type of an NSNumber?
ios, json, nsnumber, objective-c
Solution
As documented in NSDecimalNumber.h, `NSDecimalNumber` always returns `"d"` for it's return type. This is expected behavior.
- (const char *)objCType NS_RETURNS_INNER_POINTER;
// return 'd' for double
And also in the Developer Docs:
Returns a C string containing the Objective-C type of the data contained in the
receiver, which for an NSDecimalNumber object is always “d” (for double).
`CFNumberGetValue` is documented to return false if the conversion was lossy. In the event of a lossy conversion, or when you encounter an `NSDecimalNumber`, you will want to fall back to using the stringValue and then use `sqlite3_bind_text` to bind it (and use sqlite's column affinity).
Something like this:
NSNumber *number = ...
BOOL ok = NO;
if (![number isKindOfClass:[NSDecimalNumber class]]) {
CFNumberType numberType = CFNumberGetType(number);
if (numberType == kCFNumberFloat32Type ||
numberType == kCFNumberFloat64Type ||
numberType == kCFNumberCGFloatType)
{
double value;
ok = CFNumberGetValue(number, kCFNumberFloat64Type, &value);
if (ok) {
ok = (sqlite3_bind_double(pStmt, idx, value) == SQLITE_OK);
}
} else {
SInt64 value;
ok = CFNumberGetValue(number, kCFNumberSInt64Type, &value);
if (ok) {
ok = (sqlite3_bind_int64(pStmt, idx, value) == SQLITE_OK);
}
}
}
// We had an NSDecimalNumber, or the conversion via CFNumberGetValue() was lossy.
if (!ok) {
NSString *stringValue = [number stringValue];
ok = (sqlite3_bind_text(pStmt, idx, [stringValue UTF8String], -1, SQLITE_TRANSIENT) == SQLITE_OK);
}
Problem
Consider this code: ``` NSNumber* interchangeId = dict[@"interchangeMarkerLogId"]; long long llValue = [interchangeId longLongValue]; double dValue = [interchangeId doubleValue]; NSNumber* doubleId = [NSNumber numberWithDouble:dValue]; long long llDouble = [doubleId longLongValue]; if (llValue > 1000000) { NSLog(@"Have Marker iD = %@, interchangeId = %@, long long value = %lld, doubleNumber = %@, doubleAsLL = %lld, CType = %s, longlong = %s", self.iD, interchangeId, llValue, doubleId, llDouble, [interchangeId objCType], @encode(long long)); } ``` The results: Have Marker iD = (null), interchangeId = 635168520811866143, long long value = 635168520811866143, doubleNumber = 6.351685208118661e+17, doubleAsLL = 635168520811866112, CType = d, longlong = q `dict` is coming from NSJSONSerialization, and the original JSON source data is `"interchangeId":635168520811866143`. It appears that all 18 digits of the value have been captured in the NSNumber, so it could not possibly have been accumulated by NSJSONSerialization as a `double` (which is limited to 16 decimal digits). Yet, objCType is reporting that it's a `double`. We find this in the documentation for NSNumber: "The returned type does not necessarily match the method the receiver was created with." So apparently this is a "feechure" (i.e., documented bug). So how can I determine that this value originated as an integer and not a floating point value, so I can extract it correctly, with all the available precision? (Keep in mind that I have some other values that are legitimately floating-point, and I need to extract those accurately as well.) I've come up with two solutions so far: The first, which does not make use of knowledge of NSDecimalNumber -- ``` NSString* numberString = [obj stringValue]; BOOL fixed = YES; for (int i = 0; i < numberString.length; i++) { unichar theChar = [numberString characterAtIndex:i]; if (theChar != '-' && (theChar < '0' || theChar > '9')) { fixed = NO; break; } } ``` The second, which assumes that we only need worry about NSDecimalNumber objects, and can trust the CType results from regular NSNumbers -- ``` if ([obj isKindOfClass:[NSDecimalNumber class]]) { // Need to determine if integer or floating-point. NSDecimalNumber is a subclass of NSNumber, but it always reports it's type as double. NSDecimal decimalStruct = [obj decimalValue]; // The decimal value is usually "compact", so may have a positive exponent even if integer (due to trailing zeros). "Length" is expressed in terms of 4-digit halfwords. if (decimalStruct._exponent >= 0 && decimalStruct._exponent + 4 * decimalStruct._length < 20) { sqlite3_bind_int64(pStmt, idx, [obj longLongValue]); } else { sqlite3_bind_double(pStmt, idx, [obj doubleValue]); } } else ... handle regular NSNumber by testing CType. ``` The second should be more efficient, especially since it does not need to create a new object, but is slightly worrisome in that it depends on "undocumented behavior/interface" of NSDecimal -- the meanings of the fields are not documented anywhere (that I can find) and are said to be "private". Both appear to work. Though on thinking about it a bit -- The second approach has some "boundary" problems, since one can't readily adjust the limits to assure that the maximum possible 64-bit binary int will "pass" without risking loss of a slightly larger number. Rather unbelievably, this scheme fails in some cases: ``` BOOL fixed = NO; long long llValue = [obj longLongValue]; NSNumber* testNumber = [[NSNumber alloc] initWithLongLong:llValue]; if ([testNumber isEqualToNumber:obj]) { fixed = YES; } ``` I didn't save the value, but there is one for which the NSNumber will essentially be unequal to itself -- the values both display the same but do not register as equal (and it is certain that the value originated as an integer). This appears to work, so far: ``` BOOL fixed = NO; if ([obj isKindOfClass:[NSNumber class]]) { long long llValue = [obj longLongValue]; NSNumber* testNumber = [[[obj class] alloc] initWithLongLong:llValue]; if ([testNumber isEqualToNumber:obj]) { fixed = YES; } } ``` Apparently `isEqualToNumber` does not work reliably between an NSNumber and an NSDecimalNumber. (But the bounty is still open, for the best suggestion or improvement.)