Skip to content

Replaced custom DateFormatter with new ISO8601DateFormatter#998

Merged
NachoSoto merged 4 commits into
mainfrom
iso-date-formatter
Nov 30, 2021
Merged

Replaced custom DateFormatter with new ISO8601DateFormatter#998
NachoSoto merged 4 commits into
mainfrom
iso-date-formatter

Conversation

@NachoSoto

Copy link
Copy Markdown
Contributor

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.

@NachoSoto NachoSoto requested review from a team, aboedo and taquitos November 29, 2021 19:21
@NachoSoto NachoSoto changed the title Replaced custom DateFormatter with new ISO8601DateFormatter Replaced custom DateFormatter with new ISO8601DateFormatter Nov 29, 2021
Comment thread Purchases/FoundationExtensions/DateFormatter+Extensions.swift Outdated
Comment thread Purchases/Public/EntitlementInfo.swift Outdated

@taquitos taquitos left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question around public API

Comment thread Purchases/Public/CustomerInfo.swift Outdated
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`.
@NachoSoto

Copy link
Copy Markdown
Contributor Author

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[totally minor] would it make sense to create a static method that uses the default formatter to keep the same structure?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What do you mean by a static method? ISO8601DateFormatter.default() instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not exactly. What I mean is something like this:

ISO8601DateFormatter.date(from: dateString)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes, it makes sense to keep it simple. 👍🏼

@aboedo aboedo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome!

Comment thread Purchases/FoundationExtensions/DateFormatter+Extensions.swift Outdated
Comment thread Purchases/FoundationExtensions/DateFormatter+Extensions.swift Outdated
@NachoSoto NachoSoto merged commit a5d00dc into main Nov 30, 2021
@NachoSoto NachoSoto deleted the iso-date-formatter branch November 30, 2021 18:55
winstondu pushed a commit to winstondu/purchases-ios that referenced this pull request Dec 19, 2021
…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`.
taquitos pushed a commit that referenced this pull request Dec 20, 2021
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`.
@taquitos taquitos mentioned this pull request Dec 20, 2021
NachoSoto added a commit that referenced this pull request Nov 8, 2022
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
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.

replace ISO8601DateFormatter with the one from Foundation

5 participants