Replaced custom DateFormatter with new ISO8601DateFormatter#998
Conversation
DateFormatter with new ISO8601DateFormatter
taquitos
left a comment
There was a problem hiding this comment.
Question around public API
Fixes #988. Unfortunately `ISO8601DateFormatter` inherits from `Formatter`, and not `DateFormatter`, so this introduces a new protocol `DateFormatterType` that both types can implement. Another unfortunate difference with the new type compared to the old implementation is that milliseconds are either optional or mandatory. For that reason, I added `ISO8601DateFormatter.default` which replicates the same behavior by deferring `date(from: String) -> Date` to one formatter or the other. I added tests to cover this behavior directly, but it was only thanks to the existing `ArraySlice` tests that I became aware of this requirement. Additionally, I made `Transaction.init(with:productId:dateFormatter)` not `public` and not `@objc` so it can take a new `DateFormatterType`. The method didn't need to be `public`, it was just a pre-Swift-migration leftover. As a bonus I changed some of the tests in `DateFormatter+ExtensionsTests` to use `XCTUnwrap`.
602a6a1 to
a6aa901
Compare
|
This should be ready for review now :) |
| guard let dateString = String(bytes: Array(self), encoding: .ascii) else { return nil } | ||
| return DateFormatter.date(fromISO8601SecondsOrMillisecondsString: dateString) | ||
|
|
||
| return ISO8601DateFormatter.default.date(from: dateString) |
There was a problem hiding this comment.
[totally minor] would it make sense to create a static method that uses the default formatter to keep the same structure?
There was a problem hiding this comment.
What do you mean by a static method? ISO8601DateFormatter.default() instead?
There was a problem hiding this comment.
Not exactly. What I mean is something like this:
ISO8601DateFormatter.date(from: dateString)There was a problem hiding this comment.
I see. I'm not against it, but it can be confusing to have 2 ways of doing the same thing. It always leads to the question: "what's the difference?". Having only one API solves that problem.
There was a problem hiding this comment.
Yes, it makes sense to keep it simple. 👍🏼
…nueCat#998) Fixes RevenueCat#988. Unfortunately `ISO8601DateFormatter` inherits from `Formatter`, and not `DateFormatter`, so this introduces a new protocol `DateFormatterType` that both types can implement. Another unfortunate difference with the new type compared to the old implementation is that milliseconds are either optional or mandatory. For that reason, I added `ISO8601DateFormatter.default` which replicates the same behavior by deferring `date(from: String) -> Date` to one formatter or the other. I added tests to cover this behavior directly, but it was only thanks to the existing `ArraySlice` tests that I became aware of this requirement. Additionally, I made `Transaction.init(with:productId:dateFormatter)` not `public` and not `@objc` so it can take a new `DateFormatterType`. The method didn't need to be `public`, it was just a pre-Swift-migration leftover. As a bonus I changed some of the tests in `DateFormatter+ExtensionsTests` to use `XCTUnwrap`.
Fixes #988. Unfortunately `ISO8601DateFormatter` inherits from `Formatter`, and not `DateFormatter`, so this introduces a new protocol `DateFormatterType` that both types can implement. Another unfortunate difference with the new type compared to the old implementation is that milliseconds are either optional or mandatory. For that reason, I added `ISO8601DateFormatter.default` which replicates the same behavior by deferring `date(from: String) -> Date` to one formatter or the other. I added tests to cover this behavior directly, but it was only thanks to the existing `ArraySlice` tests that I became aware of this requirement. Additionally, I made `Transaction.init(with:productId:dateFormatter)` not `public` and not `@objc` so it can take a new `DateFormatterType`. The method didn't need to be `public`, it was just a pre-Swift-migration leftover. As a bonus I changed some of the tests in `DateFormatter+ExtensionsTests` to use `XCTUnwrap`.
Fixes [TRIAGE-172] See stacktrace: ``` Fatal Exception: NSInternalInconsistencyException Invalid parameter not satisfying: formatOptions == 0 || !(formatOptions & ~(NSISO8601DateFormatWithYear | NSISO8601DateFormatWithMonth | NSISO8601DateFormatWithWeekOfYear | NSISO8601DateFormatWithDay | NSISO8601DateFormatWithTime | NSISO8601DateFormatWithTimeZone | NSISO8601DateFormatWithSpaceBetweenDateAndTime | NSISO8601DateFormatWithDashSeparatorInDate | NSISO8601DateFormatWithColonSeparatorInTime | NSISO8601DateFormatWithColonSeparatorInTimeZone | NSISO8601DateFormatWithFullDate | NSISO8601DateFormatWithFullTime | NSISO8601DateFormatWithInternetDateTime)) Fatal Exception: NSInternalInconsistencyException 0 CoreFoundation 0x141d04 __exceptionPreprocess 1 libobjc.A.dylib 0x8528 objc_exception_throw 2 CoreFoundation 0x141bd8 +[NSException raise:format:] 3 Foundation 0xacc24 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] 4 Foundation 0xe4794 -[NSISO8601DateFormatter setFormatOptions:] 5 RevenueCat 0x4795c one-time initialization function for withMilliseconds 6 libdispatch.dylib 0x1048 _dispatch_client_callout 7 libdispatch.dylib 0x4710 dispatch_once_f$VARIANT$mp 8 RevenueCat 0x47b0c date(from:) in Formatter #1 in closure #1 in variable initialization expression of static NSISO8601DateFormatter.default + 60 (DateFormatter+Extensions.swift:60) 9 RevenueCat 0x47b68 protocol witness for DateFormatterType.date(from:) in conformance Formatter #1 in closure #1 in variable initialization expression of static NSISO8601DateFormatter.default (<compiler-generated>) 10 RevenueCat 0xe6b8 AppleReceiptBuilder.build(fromContainer:) (AppleReceiptBuilder.swift) ``` This is the key part: `formatOptions == 0 || !(formatOptions & ~(NSISO8601DateFormatWithYear | NSISO8601DateFormatWithMonth | NSISO8601DateFormatWithWeekOfYear | NSISO8601DateFormatWithDay | NSISO8601DateFormatWithTime | NSISO8601DateFormatWithTimeZone | NSISO8601DateFormatWithSpaceBetweenDateAndTime | NSISO8601DateFormatWithDashSeparatorInDate | NSISO8601DateFormatWithColonSeparatorInTime | NSISO8601DateFormatWithColonSeparatorInTimeZone | NSISO8601DateFormatWithFullDate | NSISO8601DateFormatWithFullTime | NSISO8601DateFormatWithInternetDateTime))` This is coming from the following code: ```swift static let withMilliseconds: DateFormatterType = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [ .withInternetDateTime, .withFractionalSeconds ] return formatter }() ``` Even though `.withFractionalSeconds` is available in iOS 11, that assertion seems wrong, as it's treating that option as invalid. This was a regression in #998, almost a year ago, but since we have very little coverage on iOS 11 we never noticed until now. Unfortunately it seems like we won't be able to run iOS 11 tests in CI (#2036), but this change is fairly safe, it just reverts to using the old `DateFormatter` for older versions. [TRIAGE-172]: https://revenuecats.atlassian.net/browse/TRIAGE-172?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
Fixes #988.
Unfortunately
ISO8601DateFormatterinherits fromFormatter, and notDateFormatter, so this introduces a new protocolDateFormatterTypethat both types can implement.Another unfortunate difference with the new type compared to the old implementation is that milliseconds are either optional or mandatory.
For that reason, I added
ISO8601DateFormatter.defaultwhich replicates the same behavior by deferringdate(from: String) -> Dateto one formatter or the other.I added tests to cover this behavior directly, but it was only thanks to the existing
ArraySlicetests that I became aware of this requirement.Additionally, I made
Transaction.init(with:productId:dateFormatter)notpublicand not@objcso it can take a newDateFormatterType.The method didn't need to be
public, it was just a pre-Swift-migration leftover.As a bonus I changed some of the tests in
DateFormatter+ExtensionsTeststo useXCTUnwrap.