Skip to content

JSONSerialization: Add missing numeric types #1197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Sep 5, 2017

Conversation

spevans
Copy link
Contributor

@spevans spevans commented Sep 1, 2017

  • Add support for Int8/Int16/Int32/Int64, UInt8/UInt16/UInt32/UInt64,
    Decimal and NSDecimalNumber.
  • Improve serializeInteger performance to avoid overall speed regression.
  • Move JSONSerialization tests into correct testfile and use Int64 to stop tests
    breaking on 32bit platforms.
  • Add tests for isValidJSONObject() to test the support of new types.
  • Add tests for Float, Double, Decimal and NSDecimalNumber.

This PR does not address the output format of Float and Double, which are not currently the same as Foundation. That will be addressed in a follow-up PR

- Add support for Int8/Int16/Int32/Int64, UInt8/UInt16/UInt32/UInt64,
  Decimal and NSDecimalNumber.

- Improve serializeInteger performance to avoid overall speed regression.
- Move JSONSerialization tests into correct testfile and use Int64 to stop tests
  breaking on 32bit platforms.

- Add tests for isValidJSONObject() to test the support of new types.

- Add tests for Float, Double, Decimal and NSDecimalNumber.
@@ -63,7 +63,9 @@ open class JSONSerialization : NSObject {
}

// object is Swift.String, NSNull, Int, Bool, or UInt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this comment, which is now wrong (and was pointless anyway)?

}

writer(stringResult)
let output = String(cString: Array(buffer.suffix(from: pos)))
Copy link
Contributor

@ianpartridge ianpartridge Sep 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this do two copies? One to create a new Array and another to create the String. Can we avoid at least one of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I was allocating a buffer once in the JSONWriter() and using String(bytesNoCopy:) but even that does a copy and then there is another one when the string is appended to the JSON output.
So I decided not to do anything too complicated in this PR and just copied this from Decimal.

In a followup PR I will try converting JSONWriter to allocate the output buffer at the start and just have the serialise methods write directly into it and hopefully avoid any copying.

@djones6 Ran some tests against this change and confirmed that it is faster than the current method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing directly into the output buffer sounds much better - thanks.

writer(_serializationString(for: num))
if CFNumberIsFloatType(num._cfObject) {
if !num.doubleValue.isFinite {
throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.propertyListReadCorrupt.rawValue, userInfo: ["NSDebugDescription" : "Number cannot be infinity or NaN"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should say "Number must be finite" now. Or is the string exactly matching Darwin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it to emit the same message as Darwin

Copy link
Contributor

@ianpartridge ianpartridge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few comments.

@ianpartridge
Copy link
Contributor

@swift-ci please test

@ianpartridge
Copy link
Contributor

@swift-ci please test

let intMax:Int = Int.max
let uintMin:UInt = UInt.min
let uintMax:UInt = UInt.max
let intMin = Int64.min
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this would be better called int64Min now. No big deal though.

@spevans spevans requested a review from phausler September 4, 2017 11:45
@parkera parkera requested a review from itaiferber September 5, 2017 17:14
Copy link
Contributor

@itaiferber itaiferber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable overall — thanks for going through and doing this. Some minor comments below, but the major things I think we should fix are taking out support for infinity and NaN, and writing out floating-point values in scientific notation.

array.append(number % 10)
private func serializeInteger<T: UnsignedInteger>(value: T, isNegative: Bool = false) {
let maxIntLength = 22 // 20 digits in UInt64 + optional sign + trailing '\0'
let ZERO: CChar = 0x30 // ASCII '0' == 0x30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a purely stylistic standpoint, I don't think we really use all caps anywhere to indicate a constant. I think this would read more clearly as just asciiZero (and MINUS would read a bit better as asciiMinus).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually copied this from Decimal but I will tidy it up.

if !dv.isFinite {
let value: String
if dv.isNaN {
value = "NaN"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inconsistent with NSJSONSerialization, which does not support NaN or infinite values. This should arguably preconditionFailure/fatalError.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Seeing also as we claim in isValidJSONValue that infinite numbers/NaN are not valid, we shouldn't support them here.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually to exactly match the error messaging on Darwin, not to serialise the value. value is used in the NSError below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're totally right, of course — I'd misread this. 😬


func test_serialize_Float() {
XCTAssertEqual(try trySerialize([-Float.leastNonzeroMagnitude, Float.leastNonzeroMagnitude]), "[-0,0]")
XCTAssertEqual(try trySerialize([-Float.greatestFiniteMagnitude]), "[-340282346638529000000000000000000000000]")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value (and all floating-point values below, actually) is inconsistent with NSJSONSerialization, which outputs floating-point numbers in scientific notation (via snprintf("%*.g", DBL_DECIMAL_DIG, ...)). We should likely bridge that gap.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try and play around with the Float and Double output formats but it broke some other tests. I will see if I can find a better solution - I was actually intending to do a follow-up PR to fix up the output formatting.

let invalid = number.doubleValue.isInfinite || number.doubleValue.isNaN
return !invalid
if CFNumberIsFloatType(number._cfObject) {
return number.doubleValue.isFinite
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line made me do a double-take for a moment because at first glance, it's hard to visually distinguish between number.doubleValue.isFinite and number.doubleValue.isInfinite. Do you mind just calling this out with a comment saying something like "All floating-point numbers except for NaN and infinity are valid." (or similar)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will actually put it back to how it was but store the doubleValue in a local since getting the doubleValue twice involved into 2 calls into CFNumber so was a significant speedup.

XCTAssertTrue(JSONSerialization.isValidJSONObject([Decimal(123), Decimal(Double.leastNonzeroMagnitude)]))

XCTAssertFalse(JSONSerialization.isValidJSONObject(Float.nan))
XCTAssertFalse(JSONSerialization.isValidJSONObject(Float.infinity))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness' sake, it's worth also asserting this on negative infinity (below as well).

@spevans
Copy link
Contributor Author

spevans commented Sep 5, 2017

I have fixed all of the outstanding comments expect for the Float/Double output format which I will fix in a followup PR.

Copy link
Contributor

@itaiferber itaiferber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; thanks!

@itaiferber
Copy link
Contributor

@swift-ci Please test

@spevans
Copy link
Contributor Author

spevans commented Sep 5, 2017

@itaiferber Looks like the test worked on CI, is this ok to merge now?

@itaiferber
Copy link
Contributor

itaiferber commented Sep 5, 2017

@spevans Which builder did this build on? I don't see any GitHub CI state for it (likely a GitHub issue)

@itaiferber itaiferber merged commit 7a5af0f into swiftlang:master Sep 5, 2017
@spevans spevans deleted the pr_json_serialise_more_types branch September 19, 2017 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants