Skip to content

Commit 0a04968

Browse files
Workaround for NSDecimalNumber longLongValue issue. (#4109)
1 parent 2ebb82d commit 0a04968

File tree

2 files changed

+70
-18
lines changed

2 files changed

+70
-18
lines changed

Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -515,12 +515,40 @@ - (void)testDoublesAreRoundedProperly {
515515
XCTAssertEqualObjects([[engine serverCacheAtPath:PATH(@"foo")] dataHash], hashFor247);
516516
}
517517

518+
// NOTE: This deals with part of the NSDecimalNumber issue: namely that
519+
// [NSDecimalNumber longLongValue] is completely bonkers for decimals with
520+
// high precision. The decimal value below (close to 1000) is returned as -844!
521+
// http://www.openradar.me/radar?id=5007005597040640
522+
// This does not deal with the fact that this is not the same behavior as the
523+
// RTDB server. Given an NSDecimalNumber, the server stores decimals with the
524+
// full precision and returns these as NSDecimalNumbers too.
525+
// This means that the RTDB server can store the double value 2.47 as 2.47.
526+
// But if the double is wrapped in an NSDecimalNumber, the server apparently
527+
// stores the value 2.470000000000000512.
528+
// This means that the persistence layer will have a hard time determining whether
529+
// rounding is appropriate or not.
530+
// Similarly, as an NSDecimalNumber, the RTDB gladly stores and returns
531+
// 999.9999999999999487 without any rounding, although considered as a double,
532+
// the value will be 1000.
533+
- (void)testNSDecimalsAreRoundedProperly {
534+
FLevelDBStorageEngine *engine = [self cleanStorageEngine];
535+
id decimalValue = [NSDecimalNumber decimalNumberWithString:@"999.9999999999999487"];
536+
id expectedDecimalValue = [NSDecimalNumber decimalNumberWithString:@"1000"];
537+
[engine updateServerCache:NODE(decimalValue) atPath:PATH(@"foo") merge:NO];
538+
539+
id<FNode> actualData = [engine serverCacheAtPath:PATH(@"foo")];
540+
NSNumber *actualDecimal = [actualData val];
541+
XCTAssertEqualObjects([actualDecimal stringValue], [expectedDecimalValue stringValue]);
542+
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDecimal), kCFNumberFloat64Type);
543+
}
544+
518545
- (void)testIntegersAreReturnedsAsIntegers {
519546
id intValue = @247;
520547
id longValue = @1542405709418655810;
521548
id doubleValue = @0xFFFFFFFFFFFFFFFFUL; // This number can't be represented as a signed long.
522549

523-
id<FNode> expectedData = NODE((@{@"int" : @247, @"long" : longValue, @"double" : doubleValue}));
550+
id<FNode> expectedData =
551+
NODE((@{@"int" : intValue, @"long" : longValue, @"double" : doubleValue}));
524552
FLevelDBStorageEngine *engine = [self cleanStorageEngine];
525553
[engine updateServerCache:expectedData atPath:PATH(@"foo") merge:NO];
526554
id<FNode> actualData = [engine serverCacheAtPath:PATH(@"foo")];
@@ -532,18 +560,7 @@ - (void)testIntegersAreReturnedsAsIntegers {
532560
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualInt), kCFNumberSInt64Type);
533561
XCTAssertEqualObjects([actualLong stringValue], [longValue stringValue]);
534562
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualLong), kCFNumberSInt64Type);
535-
#ifdef TARGET_OS_IOS
536-
// Catalyst and iOS 13 use int128_t but CFNumber still calls it 64 bits.
537-
if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 13) {
538-
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberSInt64Type);
539-
} else {
540-
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberFloat64Type);
541-
}
542-
#elif TARGET_OS_MACCATALYST
543563
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberSInt64Type);
544-
#else
545-
XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberFloat64Type);
546-
#endif
547564
}
548565

549566
// TODO[offline]: Somehow test estimated server size?

Firebase/Database/Persistence/FLevelDBStorageEngine.m

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -873,12 +873,47 @@ - (NSData *)serializePrimitive:(id)value {
873873

874874
- (id)fixDoubleParsing:(id)value
875875
__attribute__((no_sanitize("float-cast-overflow"))) {
876-
// The parser for double values in JSONSerialization at the root takes some
877-
// short-cuts and delivers wrong results (wrong rounding) for some double
878-
// values, including 2.47. Because we use the exact bytes for hashing on the
879-
// server this will lead to hash mismatches. The parser of NSNumber seems to
880-
// be more in line with what the server expects, so we use that here
881-
if ([value isKindOfClass:[NSNumber class]]) {
876+
if ([value isKindOfClass:[NSDecimalNumber class]]) {
877+
// In case the value is an NSDecimalNumber, we may be dealing with
878+
// precisions that are higher than what can be represented in a double.
879+
// In this case it does not suffice to check for integral numbers by
880+
// casting the [value doubleValue] to an int64_t, because this will
881+
// cause the compared values to be rounded to double precision.
882+
// Coupled with a bug in [NSDecimalNumber longLongValue] that triggers
883+
// when converting values with high precision, this would cause
884+
// values of high precision, but with an integral 'doubleValue'
885+
// representation to be converted to bogus values.
886+
// A radar for the NSDecimalNumber issue can be found here:
887+
// http://www.openradar.me/radar?id=5007005597040640
888+
// Consider the NSDecimalNumber value: 999.9999999999999487
889+
// This number has a 'doubleValue' of 1000. Using the previous version
890+
// of this method would cause the value to be interpreted to be integral
891+
// and then the resulting value would be based on the longLongValue
892+
// which due to the NSDecimalNumber issue would turn out as -844.
893+
// By using NSDecimal logic to test for integral values,
894+
// 999.9999999999999487 will not be considered integral, and instead
895+
// of triggering the 'longLongValue' issue, it will be returned as
896+
// the 'doubleValue' representation (1000).
897+
// Please note, that even without the NSDecimalNumber issue, the
898+
// 'correct' longLongValue of 999.9999999999999487 is 999 and not 1000,
899+
// so the previous code would cause issues even without the bug
900+
// referenced in the radar.
901+
NSDecimal original = [(NSDecimalNumber *)value decimalValue];
902+
NSDecimal rounded;
903+
NSDecimalRound(&rounded, &original, 0, NSRoundPlain);
904+
if (NSDecimalCompare(&original, &rounded) != NSOrderedSame) {
905+
NSString *doubleString = [value stringValue];
906+
return [NSNumber numberWithDouble:[doubleString doubleValue]];
907+
} else {
908+
return [NSNumber numberWithLongLong:[value longLongValue]];
909+
}
910+
} else if ([value isKindOfClass:[NSNumber class]]) {
911+
// The parser for double values in JSONSerialization at the root takes
912+
// some short-cuts and delivers wrong results (wrong rounding) for some
913+
// double values, including 2.47. Because we use the exact bytes for
914+
// hashing on the server this will lead to hash mismatches. The parser
915+
// of NSNumber seems to be more in line with what the server expects, so
916+
// we use that here
882917
CFNumberType type = CFNumberGetType((CFNumberRef)value);
883918
if (type == kCFNumberDoubleType || type == kCFNumberFloatType) {
884919
// The NSJSON parser returns all numbers as double values, even

0 commit comments

Comments
 (0)