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)); }
Results:
Have marker iD = (null), interchangeId = 635168520811866143, long long value = 635168520811866143, doubleNumber = 6.351685208118661e + 17, doubleAsLL = 635168520811866112, CType = d, longlong = q
dict comes from NSJSONSerialization, and the original JSON source is "interchangeId":635168520811866143 . It looks like all 18 digits of the value were fixed in NSNumber, so it could not be accumulated by NSJSONSerialization as a double (which is limited to 16 decimal digits). However, objCType reports that it is a double .
We find this in the documentation for NSNumber: "The return type does not necessarily correspond to the method with which the receiver was created." Apparently, this is a "paid" (i.e., a documented error).
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 precision available? (Keep in mind that I have other values that are legal floating point, and I also need to extract them exactly.)
So far I have come up with two solutions:
The first one that does not use the knowledge of NSDecimalNumber is
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 to 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 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 because it does not need to create a new object, but it is a little worried that it depends on the NSDecimal “undocumented behavior / interface” - the field values are not documented anywhere (which I can find) and say that "private".
Both work.
Though think about it a bit . The second approach has some “boundary” problems, because you cannot easily set limits to ensure that the maximum possible 64-bit binary int will “pass” without risking losing a bit more.
Pretty unbelievable , 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 did not save the value, but there is one for which NSNumber will be essentially unequal to itself - the values are displayed in the same way, but are not registered as equal (and, of course, the value obtained as an integer).
While this works,
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 NSNumber and NSDecimalNumber.
(But generosity is still open for a better offer or improvement.)