Skip to content
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

fix(datastore-v1): store time zone info in Temporal.DateTime #3420

Merged
merged 1 commit into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Amplify.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1262,6 +1263,7 @@
5C763DAD26F2D00F006650E7 /* Geo+ResultsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Geo+ResultsHandler.swift"; sourceTree = "<group>"; };
5CB5DD26271707780078CCA2 /* Geo+SearchOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Geo+SearchOptions.swift"; sourceTree = "<group>"; };
5CF43D082728C64100F636E1 /* Geo+Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Geo+Error.swift"; sourceTree = "<group>"; };
609A3CAB2B290344006830C7 /* TimeZone+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeZone+Extension.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
6B33896723AAACC900561E5B /* ReachabilityUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReachabilityUpdate.swift; sourceTree = "<group>"; };
6B452B8125A7D0F600A1A811 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2872,6 +2874,7 @@
902AE04B281304B800CD12CA /* Temporal */ = {
isa = PBXGroup;
children = (
609A3CAB2B290344006830C7 /* TimeZone+Extension.swift */,
902AE0702813052F00CD12CA /* DataStoreError+Temporal.swift */,
9091FF6A2820771B0021D8E1 /* Date.swift */,
9091FF762820771B0021D8E1 /* Date+Operation.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}()
Expand Down
7 changes: 5 additions & 2 deletions Amplify/Categories/DataStore/Model/Temporal/Date.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 8 additions & 4 deletions Amplify/Categories/DataStore/Model/Temporal/DateTime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
@usableFromInline
internal struct SpecBasedDateConverting<Spec: TemporalSpec> {
@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
Expand All @@ -28,8 +28,9 @@ internal struct SpecBasedDateConverting<Spec: TemporalSpec> {
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,
Expand All @@ -41,6 +42,6 @@ internal struct SpecBasedDateConverting<Spec: TemporalSpec> {
with: TemporalFormat.sortedFormats(for: Spec.self)
)
}
return date
return (date, tz)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
16 changes: 10 additions & 6 deletions Amplify/Categories/DataStore/Model/Temporal/Temporal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<Self>()
let (date, tz) = try SpecBasedDateConverting<Self>()
.convert(iso8601String, format)

self.init(date)
self.init(date, timeZone: tz)
}

@inlinable
public init(
iso8601String: String
) throws {
let date = try SpecBasedDateConverting<Self>()
let (date, tz) = try SpecBasedDateConverting<Self>()
.convert(iso8601String, nil)

self.init(date)
self.init(date, timeZone: tz)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ extension TemporalSpec {
"""
)
}
return Self.init(date)
return Self.init(date, timeZone: timeZone)
}
}
7 changes: 5 additions & 2 deletions Amplify/Categories/DataStore/Model/Temporal/Time.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
148 changes: 148 additions & 0 deletions Amplify/Categories/DataStore/Model/Temporal/TimeZone+Extension.swift
Original file line number Diff line number Diff line change
@@ -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])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
Loading
Loading