swiftlang / swift-foundation

The Foundation project
Apache License 2.0
2.41k stars 162 forks source link

NSDecimal addition generates large number on iOS 18 #930

Open kbot opened 2 months ago

kbot commented 2 months ago

Our production application (linked with iOS 17) has been seeing reports by users updating to iOS 18 that previously valid decimal account balances are now huge numbers.

Our code for calculating the balance:

NSDecimal totalBalanceDec = {0};
    NSArray* arrBalances = (NSArray*)JSON_NULL_SAFE([responseDict objectForKey:@"balances"]);
    NSString* currencyType;
    for (NSDictionary* balance in arrBalances) {
        currencyType = JSON_NULL_SAFE([balance objectForKey:key]);
        if (currencyType && [currencyType isEqualToString:value]) {
            if ([balance objectForKey:@"value"]) {
                NSDecimal decCopy;
                NSDecimalCopy(&decCopy, &totalBalanceDec);
                NSDecimal dec2 = [[balance objectForKey:@"value"] decimalValue];
                NSDecimalAdd(&totalBalanceDec, &decCopy, &dec2, NSRoundPlain);
            }
        }
    }
    return [NSDecimalNumber decimalNumberWithDecimal:totalBalanceDec];

Sample balances response data from our backend:

balances =     (
                {
            balanceType = CREDIT;
            createdDate = 1633544314000;
            creditType = INCENTIVIZED;
            currencyType = USD;
            id = ff8081817c55441b017c56d41d5811f7;
            lastModifiedDate = 1633544314000;
            value = 0;
        },
                {
            balanceType = CREDIT;
            createdDate = 1634856010000;
            creditType = PAID;
            currencyType = USD;
            id = ff8081817ca48459017ca502ffba0288;
            lastModifiedDate = 1726768624000;
            value = "75.2";
        },
                {
            balanceType = CREDIT;
            createdDate = 1637278758000;
            creditType = EARNED;
            currencyType = USD;
            id = ff8081817d00a009017d356b33ae7c58;
            lastModifiedDate = 1725407407000;
            value = "2.22";
        }
    )

Previous to ios 18 this would have produced 77.42 but after updating to iOS 18 it looks like this: Screenshot 2024-09-19 at 12 23 59 PM

itingliu commented 1 month ago

I'm not able to when I run it with an SDK containing the ToT swift-foundation. Here's my test case

        NSString *jsonString = @"[\
        {\
            \"balanceType\" : \"CREDIT\",\
            \"createdDate\" : 1633544314000,\
            \"creditType\" : \"INCENTIVIZED\",\
            \"currencyType\" : \"USD\",\
            \"id\" : \"ff8081817c55441b017c56d41d5811f7\",\
            \"lastModifiedDate\" : 1633544314000,\
            \"value\" : 0,\
        },\
        {\
            \"balanceType\" : \"CREDIT\",\
            \"createdDate\" : 1634856010000,\
            \"creditType\" : \"PAID\",\
            \"currencyType\" : \"USD\",\
            \"id\" : \"ff8081817ca48459017ca502ffba0288\",\
            \"lastModifiedDate\" : 1726768624000,\
            \"value\" : \"75.2\",\
        },\
                {\
            \"balanceType\" : \"CREDIT\",\
            \"createdDate\" : 1637278758000,\
            \"creditType\" : \"EARNED\",\
            \"currencyType\" : \"USD\",\
            \"id\" : \"ff8081817d00a009017d356b33ae7c58\",\
            \"lastModifiedDate\" : 1725407407000,\
            \"value\" : \"2.22\",\
        }]";

        NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
        NSError *e;
        NSArray *arrBalances = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&e];
        assert(!e);
        NSDecimal totalBalanceDec = {0};
        for (NSDictionary* balance in arrBalances) {
            NSString* currencyType = [balance objectForKey:@"currencyType"];
            if (currencyType && [currencyType isEqualToString:@"USD"]) {
                if ([balance objectForKey:@"value"]) {
                    NSDecimal decCopy;
                    NSDecimalCopy(&decCopy, &totalBalanceDec);
                    NSDecimal dec2 = [[balance objectForKey:@"value"] decimalValue];
                    NSDecimalAdd(&totalBalanceDec, &decCopy, &dec2, NSRoundPlain);
                }
            }
        }
        NSDecimalNumber *num =  [NSDecimalNumber decimalNumberWithDecimal:totalBalanceDec];
        NSLog(@"num = %f", num.doubleValue); // 77.42

It's possible that it only reproduces for apps linked against 17.0 running on iOS 18 though. If you already have that dev environment setup handy, can you help us verify if that's indeed the case, and would the issue be resolved if you recompile the app?

kbot commented 1 month ago

We do not have an environment to debug on devices with iOS 18 at the moment, but I can confirm that our production app only experienced this issue for users running iOS 18.

Our immediate workaround was to remove this single dependency on NSDecimal, which resolved the issue.