diff --git a/Amplify.xcodeproj/project.pbxproj b/Amplify.xcodeproj/project.pbxproj index 631f032032..fbedf694ee 100644 --- a/Amplify.xcodeproj/project.pbxproj +++ b/Amplify.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -293,6 +293,7 @@ 5C763DAE26F2D00F006650E7 /* Geo+ResultsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C763DAD26F2D00F006650E7 /* Geo+ResultsHandler.swift */; }; 5CB5DD27271707780078CCA2 /* Geo+SearchOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB5DD26271707780078CCA2 /* Geo+SearchOptions.swift */; }; 5CF43D092728C64100F636E1 /* Geo+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF43D082728C64100F636E1 /* Geo+Error.swift */; }; + 609A3CAC2B290344006830C7 /* TimeZone+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 609A3CAB2B290344006830C7 /* TimeZone+Extension.swift */; }; 6B33896823AAACC900561E5B /* ReachabilityUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B33896723AAACC900561E5B /* ReachabilityUpdate.swift */; }; 6B452B8225A7D0F600A1A811 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B452B8125A7D0F600A1A811 /* Array+Extensions.swift */; }; 6B5087BD2565E5AD000AB673 /* QueryPredicateEvaluateGeneratedDoubleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5087BC2565E5AD000AB673 /* QueryPredicateEvaluateGeneratedDoubleTests.swift */; }; @@ -1262,6 +1263,7 @@ 5C763DAD26F2D00F006650E7 /* Geo+ResultsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Geo+ResultsHandler.swift"; sourceTree = ""; }; 5CB5DD26271707780078CCA2 /* Geo+SearchOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Geo+SearchOptions.swift"; sourceTree = ""; }; 5CF43D082728C64100F636E1 /* Geo+Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Geo+Error.swift"; sourceTree = ""; }; + 609A3CAB2B290344006830C7 /* TimeZone+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeZone+Extension.swift"; sourceTree = ""; }; 614D1E66BBE236DDD4F8E2E0 /* Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsCoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsCoreTests.debug.xcconfig"; path = "Target Support Files/Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsCoreTests/Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsCoreTests.debug.xcconfig"; sourceTree = ""; }; 6B33896723AAACC900561E5B /* ReachabilityUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReachabilityUpdate.swift; sourceTree = ""; }; 6B452B8125A7D0F600A1A811 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; @@ -2872,6 +2874,7 @@ 902AE04B281304B800CD12CA /* Temporal */ = { isa = PBXGroup; children = ( + 609A3CAB2B290344006830C7 /* TimeZone+Extension.swift */, 902AE0702813052F00CD12CA /* DataStoreError+Temporal.swift */, 9091FF6A2820771B0021D8E1 /* Date.swift */, 9091FF762820771B0021D8E1 /* Date+Operation.swift */, @@ -5606,6 +5609,7 @@ 9091FF8B2820771C0021D8E1 /* TemporalOperation.swift in Sources */, 769CF2242669B1B9007843A0 /* RetryableGraphQLOperation.swift in Sources */, B4251A0124250369007F59EF /* AuthConfirmResetPasswordRequest.swift in Sources */, + 609A3CAC2B290344006830C7 /* TimeZone+Extension.swift in Sources */, FAAFAF2F23904B14002CF932 /* AtomicValue+Bool.swift in Sources */, 211FFEE326CD650500F0DB75 /* DataStoreQuerySnapshot.swift in Sources */, FA249EEB24C5FE66009B3CE8 /* AmplifyAPICategory+GraphQLBehavior+Combine.swift in Sources */, diff --git a/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift b/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift index bfe1717840..8540f066a9 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Model+DateFormatting.swift @@ -27,7 +27,7 @@ public struct ModelDateFormatting { public static let encodingStrategy: JSONEncoder.DateEncodingStrategy = { let strategy = JSONEncoder.DateEncodingStrategy.custom { date, encoder in var container = encoder.singleValueContainer() - try container.encode(Temporal.DateTime(date).iso8601String) + try container.encode(Temporal.DateTime(date, timeZone: .utc).iso8601String) } return strategy }() diff --git a/Amplify/Categories/DataStore/Model/Temporal/Date.swift b/Amplify/Categories/DataStore/Model/Temporal/Date.swift index ac8cfcdb1d..764572663e 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Date.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Date.swift @@ -21,13 +21,16 @@ extension Temporal { // Inherits documentation from `TemporalSpec` public let foundationDate: Foundation.Date + // Inherits documentation from `TemporalSpec` + public let timeZone: TimeZone? = .utc + // Inherits documentation from `TemporalSpec` public static func now() -> Self { - Temporal.Date(Foundation.Date()) + Temporal.Date(Foundation.Date(), timeZone: .utc) } // Inherits documentation from `TemporalSpec` - public init(_ date: Foundation.Date) { + public init(_ date: Foundation.Date, timeZone: TimeZone?) { self.foundationDate = Temporal .iso8601Calendar .startOfDay(for: date) diff --git a/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift b/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift index 170e7598fd..b297988357 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/DateTime.swift @@ -18,25 +18,29 @@ extension Temporal { // Inherits documentation from `TemporalSpec` public let foundationDate: Foundation.Date + // Inherits documentation from `TemporalSpec` + public let timeZone: TimeZone? + // Inherits documentation from `TemporalSpec` public static func now() -> Self { - Temporal.DateTime(Foundation.Date()) + Temporal.DateTime(Foundation.Date(), timeZone: .utc) } /// `Temporal.Time` of this `Temporal.DateTime`. public var time: Time { - Time(foundationDate) + Time(foundationDate, timeZone: timeZone) } // Inherits documentation from `TemporalSpec` - public init(_ date: Foundation.Date) { + public init(_ date: Foundation.Date, timeZone: TimeZone?) { let calendar = Temporal.iso8601Calendar let components = calendar.dateComponents( DateTime.iso8601DateComponents, from: date ) - foundationDate = calendar + self.timeZone = timeZone + self.foundationDate = calendar .date(from: components) ?? date } diff --git a/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift b/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift index ef43617c29..7d87806a7f 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/SpecBasedDateConverting.swift @@ -12,7 +12,7 @@ import Foundation @usableFromInline internal struct SpecBasedDateConverting { @usableFromInline - internal typealias DateConverter = (_ string: String, _ format: TemporalFormat?) throws -> Date + internal typealias DateConverter = (_ string: String, _ format: TemporalFormat?) throws -> (Date, TimeZone) @usableFromInline internal let convert: DateConverter @@ -28,8 +28,9 @@ internal struct SpecBasedDateConverting { internal static func `default`( iso8601String: String, format: TemporalFormat? = nil - ) throws -> Date { + ) throws -> (Date, TimeZone) { let date: Foundation.Date + let tz = TimeZone(iso8601DateString: iso8601String) ?? .utc if let format = format { date = try Temporal.date( from: iso8601String, @@ -41,6 +42,6 @@ internal struct SpecBasedDateConverting { with: TemporalFormat.sortedFormats(for: Spec.self) ) } - return date + return (date, tz) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift b/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift index 25d4674e53..f9f6dffcd4 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Temporal+Comparable.swift @@ -15,11 +15,13 @@ import Foundation extension TemporalSpec where Self: Comparable { public static func == (lhs: Self, rhs: Self) -> Bool { - return lhs.iso8601String == rhs.iso8601String + return lhs.iso8601FormattedString(format: .full, timeZone: .utc) + == rhs.iso8601FormattedString(format: .full, timeZone: .utc) } public static func < (lhs: Self, rhs: Self) -> Bool { - return lhs.iso8601String < rhs.iso8601String + return lhs.iso8601FormattedString(format: .full, timeZone: .utc) + < rhs.iso8601FormattedString(format: .full, timeZone: .utc) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift b/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift index a1be6cdb2e..e92f4f9435 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Temporal.swift @@ -27,6 +27,10 @@ public protocol TemporalSpec { /// by a Foundation `Date` instance. var foundationDate: Foundation.Date { get } + /// The timezone field is an optional field used to specify the timezone associated + /// with a particular date. + var timeZone: TimeZone? { get } + /// The ISO-8601 formatted string in the UTC `TimeZone`. /// - SeeAlso: `iso8601FormattedString(TemporalFormat, TimeZone) -> String` var iso8601String: String { get } @@ -57,7 +61,7 @@ public protocol TemporalSpec { /// Constructs a `TemporalSpec` from a `Date` object. /// - Parameter date: The `Date` instance that will be used as the reference of the /// `TemporalSpec` instance. - init(_ date: Foundation.Date) + init(_ date: Foundation.Date, timeZone: TimeZone?) /// A string representation of the underlying date formatted using ISO8601 rules. /// @@ -90,25 +94,25 @@ extension TemporalSpec { /// The ISO8601 representation of the scalar using `.full` as the format and `.utc` as `TimeZone`. /// - SeeAlso: `iso8601FormattedString(format:timeZone:)` public var iso8601String: String { - iso8601FormattedString(format: .full) + iso8601FormattedString(format: .full, timeZone: timeZone ?? .utc) } @inlinable public init(iso8601String: String, format: TemporalFormat) throws { - let date = try SpecBasedDateConverting() + let (date, tz) = try SpecBasedDateConverting() .convert(iso8601String, format) - self.init(date) + self.init(date, timeZone: tz) } @inlinable public init( iso8601String: String ) throws { - let date = try SpecBasedDateConverting() + let (date, tz) = try SpecBasedDateConverting() .convert(iso8601String, nil) - self.init(date) + self.init(date, timeZone: tz) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift b/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift index fe7ae56e4f..a413ab566d 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/TemporalOperation.swift @@ -33,6 +33,6 @@ extension TemporalSpec { """ ) } - return Self.init(date) + return Self.init(date, timeZone: timeZone) } } diff --git a/Amplify/Categories/DataStore/Model/Temporal/Time.swift b/Amplify/Categories/DataStore/Model/Temporal/Time.swift index 9d621ddb6e..619e2fd4c1 100644 --- a/Amplify/Categories/DataStore/Model/Temporal/Time.swift +++ b/Amplify/Categories/DataStore/Model/Temporal/Time.swift @@ -18,13 +18,16 @@ extension Temporal { // Inherits documentation from `TemporalSpec` public let foundationDate: Foundation.Date + // Inherits documentation from `TemporalSpec` + public let timeZone: TimeZone? = .utc + // Inherits documentation from `TemporalSpec` public static func now() -> Self { - Temporal.Time(Foundation.Date()) + Temporal.Time(Foundation.Date(), timeZone: .utc) } // Inherits documentation from `TemporalSpec` - public init(_ date: Foundation.Date) { + public init(_ date: Foundation.Date, timeZone: TimeZone?) { // Sets the date to a fixed instant so time-only operations are safe let calendar = Temporal.iso8601Calendar var components = calendar.dateComponents( diff --git a/Amplify/Categories/DataStore/Model/Temporal/TimeZone+Extension.swift b/Amplify/Categories/DataStore/Model/Temporal/TimeZone+Extension.swift new file mode 100644 index 0000000000..a08b31ec63 --- /dev/null +++ b/Amplify/Categories/DataStore/Model/Temporal/TimeZone+Extension.swift @@ -0,0 +1,148 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation + +extension TimeZone { + + @usableFromInline + internal init?(iso8601DateString: String) { + switch ISO8601TimeZonePart.from(iso8601DateString: iso8601DateString) { + case .some(.utc): + self.init(abbreviation: "UTC") + case let .some(.hh(hours: hours)): + self.init(secondsFromGMT: hours * 60 * 60) + case let .some(.hhmm(hours: hours, minutes: minutes)), + let .some(.HHMM(hours: hours, minuts: minutes)): + self.init(secondsFromGMT: hours * 60 * 60 + + (hours > 0 ? 1 : -1) * minutes * 60) + case let .some(.HHMMSS(hours: hours, minutes: minutes, seconds: seconds)): + self.init(secondsFromGMT: hours * 60 * 60 + + (hours > 0 ? 1 : -1) * minutes * 60 + + (hours > 0 ? 1 : -1) * seconds) + case .none: + return nil + } + } +} + + +/// ISO8601 Time Zone formats +/// - Note: +/// `±hh:mm:ss` is not a standard of ISO8601 date formate. It's supported by `AWSDateTime` exclusively. +/// +/// references: +/// https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators +/// https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html#graph-ql-aws-appsync-scalars +private enum ISO8601TimeZoneFormat { + case utc, hh, hhmm, HHMM, HHMMSS + + var format: String { + switch self { + case .utc: + return "Z" + case .hh: + return "±hh" + case .hhmm: + return "±hhmm" + case .HHMM: + return "±hh:mm" + case .HHMMSS: + return "±hh:mm:ss" + } + } + + var regex: NSRegularExpression? { + switch self { + case .utc: + return try? NSRegularExpression(pattern: "^Z$") + case .hh: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}$") + case .hhmm: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}\\d{2}$") + case .HHMM: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}:\\d{2}$") + case .HHMMSS: + return try? NSRegularExpression(pattern: "^[+-]\\d{2}:\\d{2}:\\d{2}$") + } + } + + var parts: [NSRange] { + switch self { + case .utc: + return [] + case .hh: + return [NSRange(location: 0, length: 3)] + case .hhmm: + return [ + NSRange(location: 0, length: 3), + NSRange(location: 3, length: 2) + ] + case .HHMM: + return [ + NSRange(location: 0, length: 3), + NSRange(location: 4, length: 2) + ] + case .HHMMSS: + return [ + NSRange(location: 0, length: 3), + NSRange(location: 4, length: 2), + NSRange(location: 7, length: 2) + ] + } + } +} + +private enum ISO8601TimeZonePart { + case utc + case hh(hours: Int) + case hhmm(hours: Int, minutes: Int) + case HHMM(hours: Int, minuts: Int) + case HHMMSS(hours: Int, minutes: Int, seconds: Int) + + static func from(iso8601DateString: String) -> ISO8601TimeZonePart? { + return tryExtract(from: iso8601DateString, with: .utc) + ?? tryExtract(from: iso8601DateString, with: .hh) + ?? tryExtract(from: iso8601DateString, with: .hhmm) + ?? tryExtract(from: iso8601DateString, with: .HHMM) + ?? tryExtract(from: iso8601DateString, with: .HHMMSS) + ?? nil + } +} + +private func tryExtract( + from dateString: String, + with format: ISO8601TimeZoneFormat +) -> ISO8601TimeZonePart? { + guard dateString.count > format.format.count else { + return nil + } + + let tz = String(dateString.dropFirst(dateString.count - format.format.count)) + + guard format.regex.flatMap({ + $0.firstMatch(in: tz, range: NSRange(location: 0, length: tz.count)) + }) != nil else { + return nil + } + + let parts = format.parts.compactMap { range in + Range(range, in: tz).flatMap { Int(tz[$0]) } + } + + guard parts.count == format.parts.count else { + return nil + } + + switch format { + case .utc: return .utc + case .hh: return .hh(hours: parts[0]) + case .hhmm: return .hhmm(hours: parts[0], minutes: parts[1]) + case .HHMM: return .HHMM(hours: parts[0], minuts: parts[1]) + case .HHMMSS: return .HHMMSS(hours: parts[0], minutes: parts[1], seconds: parts[2]) + } +} diff --git a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/ModelCompareTests.swift b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/ModelCompareTests.swift index 72857de9a6..c6a729fb90 100644 --- a/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/ModelCompareTests.swift +++ b/AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/Support/ModelCompareTests.swift @@ -211,8 +211,8 @@ class ModelCompareTests: BaseDataStoreTests { let name = "QPredGenName" let formatter = DateFormatter() formatter.dateFormat = TemporalFormat.short.dateFormat - let dateTime1 = Temporal.DateTime(formatter.date(from: "2021-09-01")!) - let dateTime2 = Temporal.DateTime(formatter.date(from: "2020-09-01")!) + let dateTime1 = Temporal.DateTime(formatter.date(from: "2021-09-01")!, timeZone: .utc) + let dateTime2 = Temporal.DateTime(formatter.date(from: "2020-09-01")!, timeZone: .utc) let qPredGen1 = QPredGen(id: id, name: name, myDateTime: dateTime1) let qPredGen2 = QPredGen(id: id, name: name, myDateTime: dateTime2) XCTAssertFalse(QPredGen.schema.compare(qPredGen1, qPredGen2)) @@ -340,8 +340,8 @@ class ModelCompareTests: BaseDataStoreTests { let artist = "Artist" let formatter = DateFormatter() formatter.dateFormat = TemporalFormat.short.dateFormat - let createdAt1 = Temporal.DateTime(formatter.date(from: "2021-09-01")!) - let createdAt2 = Temporal.DateTime(formatter.date(from: "2020-09-01")!) + let createdAt1 = Temporal.DateTime(formatter.date(from: "2021-09-01")!, timeZone: .utc) + let createdAt2 = Temporal.DateTime(formatter.date(from: "2020-09-01")!, timeZone: .utc) let recordCover1 = RecordCover(id: id, artist: artist, createdAt: createdAt1) let recordCover2 = RecordCover(id: id, artist: artist, createdAt: createdAt2) XCTAssertTrue(RecordCover.schema.compare(recordCover1, recordCover2)) diff --git a/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift b/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift index 5271453793..9f37d1a152 100644 --- a/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift +++ b/AmplifyTests/CategoryTests/DataStore/TemporalTests.swift @@ -145,7 +145,7 @@ class TemporalTests: XCTestCase { func testFullDateTimeParsingOnPST() { do { let datetime = try Temporal.DateTime(iso8601String: "2020-01-20T08:00:00.180-08:00") - XCTAssertEqual(datetime.iso8601String, "2020-01-20T16:00:00.180Z") + XCTAssertEqual(datetime.iso8601String, "2020-01-20T08:00:00.180-08:00") XCTAssertEqual(datetime.iso8601FormattedString(format: .short, timeZone: pst), "2020-01-20T08:00") XCTAssertEqual(datetime.iso8601FormattedString(format: .short, timeZone: .utc), "2020-01-20T16:00") XCTAssertEqual(datetime.iso8601FormattedString(format: .medium, timeZone: pst), "2020-01-20T08:00:00") @@ -161,6 +161,50 @@ class TemporalTests: XCTestCase { } } + /// - Given: a `DateTime` string in ISO8601 format + /// - When: + /// - the input has time zone info + /// - Then: + /// - DateTime should be parsed correctly with time zone info + /// - Date should be parsed with utc time zone + /// - Time should be parsed with utc time zone + func testConvertToIso8601String() { + do { + let datetime = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03-08:00") + XCTAssertEqual(datetime.iso8601String, "2023-11-30T11:04:03.000-08:00") + let datetime0 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03+08:00") + XCTAssertEqual(datetime0.iso8601String, "2023-11-30T11:04:03.000+08:00") + let datetime1 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03.322-0800") + XCTAssertEqual(datetime1.iso8601String, "2023-11-30T11:04:03.322-08:00") + let datetime2 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128-0830") + XCTAssertEqual(datetime2.iso8601String, "2023-11-30T14:09:27.128-08:30") + let datetime3 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128-0339") + XCTAssertEqual(datetime3.iso8601String, "2023-11-30T14:09:27.128-03:39") + let datetime4 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128-0000") + XCTAssertEqual(datetime4.iso8601String, "2023-11-30T14:09:27.128Z") + let datetime5 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03+08:00:21") + XCTAssertEqual(datetime5.iso8601String, "2023-11-30T11:03:42.000+08:00") + let datetime6 = try Temporal.DateTime(iso8601String: "2023-11-30T11:04:03-08:00:21") + XCTAssertEqual(datetime6.iso8601String, "2023-11-30T11:04:24.000-08:00") + let datetime7 = try Temporal.DateTime(iso8601String: "2023-11-30T14:09:27.128Z") + XCTAssertEqual(datetime7.iso8601String, "2023-11-30T14:09:27.128Z") + if #available(iOS 15.0, tvOS 15.0, *) { + let now = Date.now + let dateFormatter = DateFormatter() + dateFormatter.timeZone = .init(abbreviation: "HKT") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" + let datetime7 = Temporal.DateTime(now, timeZone: .init(abbreviation: "HKT")) + XCTAssertEqual(datetime7.iso8601String, "\(dateFormatter.string(from: now))+08:00") + } + let date = try Temporal.Date(iso8601String: "2023-11-30-08:00") + XCTAssertEqual(date.iso8601String, "2023-11-30Z") + let time = try Temporal.Time(iso8601String: "11:00:00.000-08:00") + XCTAssertEqual(time.iso8601String, "19:00:00.000Z") + } catch { + XCTFail(error.localizedDescription) + } + } + // MARK: - Date /// - Given: a `Date` string diff --git a/AmplifyTests/CoreTests/Model+CodableTests.swift b/AmplifyTests/CoreTests/Model+CodableTests.swift index b933d2552f..16fff6389e 100644 --- a/AmplifyTests/CoreTests/Model+CodableTests.swift +++ b/AmplifyTests/CoreTests/Model+CodableTests.swift @@ -24,7 +24,7 @@ class ModelCodableTests: XCTestCase { } func testToJSON() { - let createdAt = Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123)) + let createdAt = Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123), timeZone: .utc) let post = Post(id: "post-1", title: "title", content: "content", @@ -37,7 +37,7 @@ class ModelCodableTests: XCTestCase { XCTAssertEqual(post?.id, "post-1") XCTAssertEqual(post?.title, "title") XCTAssertEqual(post?.content, "content") - XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123))) + XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000.123), timeZone: .utc)) } func testDecodeWithoutFractionalSeconds() { @@ -45,6 +45,6 @@ class ModelCodableTests: XCTestCase { XCTAssertEqual(post?.id, "post-1") XCTAssertEqual(post?.title, "title") XCTAssertEqual(post?.content, "content") - XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000))) + XCTAssertEqual(post?.createdAt, Temporal.DateTime(Date(timeIntervalSince1970: 1_000_000), timeZone: .utc)) } }