Skip to content

Commit fecd2a5

Browse files
committed
Correctness fixes from testing
1 parent 56e033f commit fecd2a5

File tree

3 files changed

+41
-31
lines changed

3 files changed

+41
-31
lines changed

Sources/FoundationEssentials/Formatting/DateComponents+ISO8601FormatStyle.swift

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -132,18 +132,6 @@ extension DateComponents {
132132

133133
// MARK: -
134134

135-
@_disfavoredOverload
136-
public init(dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash, dateTimeSeparator: Date.ISO8601FormatStyle.DateTimeSeparator = .standard, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) {
137-
self.dateSeparator = dateSeparator
138-
self.dateTimeSeparator = dateTimeSeparator
139-
self.timeZone = timeZone
140-
self.timeSeparator = .colon
141-
self.timeZoneSeparator = .omitted
142-
self.includingFractionalSeconds = false
143-
_calendar = Calendar(identifier: .iso8601)
144-
_calendar.timeZone = timeZone
145-
}
146-
147135
// The default is the format of RFC 3339 with no fractional seconds: "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
148136
public init(dateSeparator: Date.ISO8601FormatStyle.DateSeparator = .dash, dateTimeSeparator: Date.ISO8601FormatStyle.DateTimeSeparator = .standard, timeSeparator: Date.ISO8601FormatStyle.TimeSeparator = .colon, timeZoneSeparator: Date.ISO8601FormatStyle.TimeZoneSeparator = .omitted, includingFractionalSeconds: Bool = false, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) {
149137
self.dateSeparator = dateSeparator
@@ -623,32 +611,52 @@ extension DateComponents.ISO8601FormatStyle {
623611
}
624612

625613
if !skipDigits {
626-
// Theoretically we would disallow or require the presence of a `:` here. However, the original implementation of this style with ICU accidentally allowed either the presence or absence of the `:` to be parsed regardless of the setting. We preserve that behavior now.
614+
// The parser is tolerant to the presence or absence of the `:` in the time zone, as well as the presence or absence of minutes.
627615

628616
// parse Time Zone: ISO8601 extended hms?, with Z
629617
// examples: -08:00, -07:52:58, Z
630618
let hours = try it.digits(maxDigits: 2, input: inputString, onFailure: Date.ISO8601FormatStyle(self).format(Date.now))
631619

632-
// Expect a colon, or not
633-
if let maybeColon = it.peek(), maybeColon == UInt8(ascii: ":") {
634-
// Throw it away
635-
it.advance()
620+
// Expect a colon, or a minutes value, or the end.
621+
let expectMinutes: Bool
622+
if let next = it.peek() {
623+
if next == UInt8(ascii: ":") {
624+
// Throw it away
625+
it.advance()
626+
627+
// But we should have minutes after this
628+
expectMinutes = true
629+
} else if isASCIIDigit(next) {
630+
// This should be minutes
631+
expectMinutes = true
632+
} else {
633+
// Not a :, not a digit - end of the string
634+
expectMinutes = false
635+
}
636+
} else {
637+
expectMinutes = false
636638
}
637639

638-
let minutes = try it.digits(maxDigits: 2, input: inputString, onFailure: Date.ISO8601FormatStyle(self).format(Date.now))
639-
640-
if let maybeColon = it.peek(), maybeColon == UInt8(ascii: ":") {
641-
// Throw it away
642-
it.advance()
643-
}
644-
645-
if let secondsTens = it.peek(), isASCIIDigit(secondsTens) {
646-
// We have seconds
647-
let seconds = try it.digits(maxDigits: 2, input: inputString, onFailure: Date.ISO8601FormatStyle(self).format(Date.now))
648-
tzOffset = (hours * 3600) + (minutes * 60) + seconds
640+
if !expectMinutes {
641+
// We reached the end of the string
642+
tzOffset = hours * 3600
649643
} else {
650-
// If the next character is missing, that's allowed - the time can be something like just -0852 and then the string can end
651-
tzOffset = (hours * 3600) + (minutes * 60)
644+
// Continue on
645+
let minutes = try it.digits(maxDigits: 2, input: inputString, onFailure: Date.ISO8601FormatStyle(self).format(Date.now))
646+
647+
if let maybeColon = it.peek(), maybeColon == UInt8(ascii: ":") {
648+
// Throw it away
649+
it.advance()
650+
}
651+
652+
if let secondsTens = it.peek(), isASCIIDigit(secondsTens) {
653+
// We have seconds
654+
let seconds = try it.digits(maxDigits: 2, input: inputString, onFailure: Date.ISO8601FormatStyle(self).format(Date.now))
655+
tzOffset = (hours * 3600) + (minutes * 60) + seconds
656+
} else {
657+
// If the next character is missing, that's allowed - the time can be something like just -0852 and then the string can end
658+
tzOffset = (hours * 3600) + (minutes * 60)
659+
}
652660
}
653661
}
654662

Sources/FoundationEssentials/JSON/JSONDecoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ open class JSONDecoder {
357357
open func decode<T, C>(_ type: T.Type, from data: Data, configuration: C.Type) throws -> T where T : DecodableWithConfiguration, C : DecodingConfigurationProviding, T.DecodingConfiguration == C.DecodingConfiguration {
358358
try decode(type, from: data, configuration: C.decodingConfiguration)
359359
}
360-
360+
361361
private func _decode<T>(_ unwrap: (JSONDecoderImpl, JSONMap.Value) throws -> T, from data: Data) throws -> T {
362362
do {
363363
return try Self.withUTF8Representation(of: data) { utf8Buffer -> T in

Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleParsingTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ final class ISO8601FormatStyleParsingTests: XCTestCase {
222222
("2020-03-05T12:00:00UTC", Date.ISO8601FormatStyle().year().month().day().time(includingFractionalSeconds: false).timeSeparator(.colon).timeZone(separator: .colon)), // allow UTC
223223
("2020-03-05T12:00:00GMT", Date.ISO8601FormatStyle().year().month().day().time(includingFractionalSeconds: false).timeSeparator(.colon).timeZone(separator: .colon)), // allow GMT
224224
("2020-03-05T13:00:00UTC+1:00", Date.ISO8601FormatStyle().year().month().day().time(includingFractionalSeconds: false).timeSeparator(.colon).timeZone(separator: .colon)), // allow UTC offsets
225+
("2020-03-05T13:00:00UTC+01", Date.ISO8601FormatStyle().year().month().day().time(includingFractionalSeconds: false).timeSeparator(.colon).timeZone(separator: .colon)), // allow hours-only (2 digit)
225226
("2020-03-05T11:00:00GMT-1:00", Date.ISO8601FormatStyle().year().month().day().time(includingFractionalSeconds: false).timeSeparator(.colon).timeZone(separator: .colon)), // allow GMT offsets
226227
("2020-03-05 12:00:00+0000", Date.ISO8601FormatStyle().year().month().day().dateTimeSeparator(.space).time(includingFractionalSeconds: false).timeZone(separator: .omitted)),
227228
("2020-03-05 11:00:00-0100", Date.ISO8601FormatStyle().year().month().day().dateTimeSeparator(.space).time(includingFractionalSeconds: false).timeZone(separator: .omitted)),
@@ -336,6 +337,7 @@ final class DateISO8601FormatStylePatternMatchingTests : XCTestCase {
336337
verify("2021-07-01T23:56:32", .iso8601(timeZone: gmt))
337338
verify("2021-07-01T23:56:32Z", .iso8601(timeZone: gmt))
338339
verify("2021-07-01T15:56:32Z", .iso8601(timeZone: pst))
340+
verify("2021-07-01T15:56:32+00", .iso8601(timeZone: pst))
339341
verify("2021-07-01T15:56:32+0000", .iso8601(timeZone: pst))
340342
verify("2021-07-01T15:56:32+00:00", .iso8601(timeZone: pst))
341343
}

0 commit comments

Comments
 (0)