How to determine the true data type of NSNumber?

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.)

+5
source share
4 answers

The simple answer is: you cannot.

To do what you ask, you will need to track the exact type as you see fit. NSNumber is more of a dumb shell because it helps you use standard numbers in a more objective way (like Obj-C objects). Using a single NSNumber, -objCType is your only way. If you want it differently, you have to do it yourself.

Here are a few other discussions that might be helpful:

get type NSNumber

What is the largest value NSNumber can store?

Why longLongValue returns an invalid value

NSJSONSerialization decompresses NSNumber?

+3
source

As indicated in NSDecimalNumber.h, NSDecimalNumber always returns "d" for the return type. This is the 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 documented to return false if the conversion was lost. In the case of lossy conversion, or when you encounter NSDecimalNumber , you'll want to go back to using stringValue and then use sqlite3_bind_text to bind it (and use sqlite column convergence).

Something like that:

 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); } 
+3
source

NSJSONSerializer returns:

an integer NSNumber for integers up to 18 digits

NSDecimalNumber for integers with 19 or more digits

double NSNumber for decimal or exponent numbers

a BOOL NSNumber for true and false.

Compare directly with the global variables kCFBooleanFalse and kCFBooleanTrue (the spelling may be incorrect) to search for booleans. Check isKindOfClass: [NSDecimalNumber class] for decimal numbers; these are actually integers. Test

 strcmp (number.objCType, @encode (double)) == 0 

for double NSNumbers. This will unfortunately correspond to NSDecimalNumber, so check this first.

+2
source

Good. This is not 100% perfect, but you add some code to SBJSON to achieve what you want.

1. First add NSNumber + SBJson to the SBJSON project:

NSNumber + SBJson.h

 @interface NSNumber (SBJson) @property ( nonatomic ) BOOL isDouble ; @end 

NSNumber + SBJson.m

 #import "NSNumber+SBJSON.h" #import <objc/runtime.h> @implementation NSNumber (SBJson) static const char * kIsDoubleKey = "kIsDoubleKey" ; -(void)setIsDouble:(BOOL)b { objc_setAssociatedObject( self, kIsDoubleKey, [ NSNumber numberWithBool:b ], OBJC_ASSOCIATION_RETAIN_NONATOMIC ) ; } -(BOOL)isDouble { return [ objc_getAssociatedObject( self, kIsDoubleKey ) boolValue ] ; } @end 

2. Now find the line in SBJson4StreamParser.m, where sbjson4_token_real processed. Change the code as follows:

  case sbjson4_token_real: {
     NSNumber * number = @ (strtod (token, NULL));
     number.isDouble = YES;
     [_delegate parserFoundNumber: number];
     [_state parser: self shouldTransitionTo: tok];
     break;
 } 

Pay attention to the bold line ... this will mark the number created from the real JSON as double.

3. Finally, you can check the isDouble property on your numerical objects decoded via SBJSON

NTN

edit:

(Of course, you could generalize this and replace the added isDouble with a generic type pointer if you want)

0
source

Source: https://habr.com/ru/post/953309/


All Articles