Skip to content

Commit

Permalink
fix(datastore-v1): store time zone info in Temporal.DateTime
Browse files Browse the repository at this point in the history
  • Loading branch information
5d committed Dec 12, 2023
1 parent d06905f commit 10c0b5f
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 30 deletions.
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

0 comments on commit 10c0b5f

Please sign in to comment.