Skip to content

Commit 0bf4b43

Browse files
feature: DateTime timezone support
1 parent 64360a5 commit 0bf4b43

File tree

3 files changed

+100
-21
lines changed

3 files changed

+100
-21
lines changed

Sources/Haystack/DateTime.swift

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,106 @@ import Foundation
22

33
public struct DateTime: Val {
44
public static var valType: ValType { .DateTime }
5-
public static let gmtName = "GMT"
5+
public static let utcName = "UTC"
66

77
public let date: Foundation.Date
8-
public let timezone: String // TODO: Align with Foundation.TimeZone
8+
public let timezone: String
99

10-
public init(date: Foundation.Date, timezone: String) {
10+
public init(date: Foundation.Date, gmtOffset: Int = 0, timezone: String = Self.utcName) {
1111
self.date = date
1212
self.timezone = timezone
1313
}
1414

15+
public init(
16+
year: Int,
17+
month: Int,
18+
day: Int,
19+
hour: Int = 0,
20+
minute: Int = 0,
21+
second: Int = 0,
22+
millisecond: Int = 0,
23+
gmtOffset: Int = 0,
24+
timezone: String = Self.utcName
25+
) throws {
26+
let components = DateComponents(
27+
calendar: calendar,
28+
timeZone: .init(secondsFromGMT: gmtOffset),
29+
year: year,
30+
month: month,
31+
day: day,
32+
hour: hour,
33+
minute: minute,
34+
second: second,
35+
nanosecond: millisecond * 1_000_000
36+
)
37+
guard let date = components.date else {
38+
throw ValError.invalidDateTimeDefinition
39+
}
40+
self.date = date
41+
self.timezone = timezone
42+
}
43+
44+
public init(
45+
date: Date,
46+
time: Time,
47+
gmtOffset: Int = 0,
48+
timezone: String = Self.utcName
49+
) throws {
50+
let components = DateComponents(
51+
calendar: calendar,
52+
timeZone: .init(secondsFromGMT: gmtOffset),
53+
year: date.year,
54+
month: date.month,
55+
day: date.day,
56+
hour: time.hour,
57+
minute: time.minute,
58+
second: time.second,
59+
nanosecond: time.millisecond * 1_000_000
60+
)
61+
guard let date = components.date else {
62+
throw ValError.invalidDateTimeDefinition
63+
}
64+
self.date = date
65+
self.timezone = timezone
66+
}
67+
68+
public init(_ string: String) throws {
69+
let splits = string.split(separator: " ")
70+
let dateTimeStr = String(splits[0])
71+
self.date = try Self.dateFromString(dateTimeStr)
72+
if splits.count > 1 {
73+
self.timezone = String(splits[1])
74+
} else {
75+
self.timezone = Self.utcName
76+
}
77+
}
78+
1579
public func toZinc() -> String {
16-
let formatter = ISO8601DateFormatter()
17-
formatter.formatOptions = [.withInternetDateTime]
18-
var zinc = formatter.string(from: date)
19-
if timezone != Self.gmtName {
80+
var zinc: String
81+
if hasMilliseconds {
82+
zinc = dateTimeWithMillisFormatter.string(from: date)
83+
} else {
84+
zinc = dateTimeFormatter.string(from: date)
85+
}
86+
if timezone != Self.utcName {
2087
zinc += " \(timezone)"
2188
}
2289
return zinc
2390
}
91+
92+
static func dateFromString(_ isoString: String) throws -> Foundation.Date {
93+
if let date = dateTimeFormatter.date(from: isoString) {
94+
return date
95+
} else if let date = dateTimeWithMillisFormatter.date(from: isoString) {
96+
return date
97+
} else {
98+
throw ValError.invalidDateTimeFormat(isoString)
99+
}
100+
}
101+
102+
private var hasMilliseconds: Bool {
103+
return calendar.component(.nanosecond, from: date) != 0
104+
}
24105
}
25106

26107
/// Singleton Haystack DateTime formatter
@@ -30,13 +111,15 @@ var dateTimeFormatter: ISO8601DateFormatter {
30111
return formatter
31112
}
32113

33-
/// Singleton Haystack DateTime formatter
114+
/// Singleton Haystack DateTime formatter with fractional second support
34115
var dateTimeWithMillisFormatter: ISO8601DateFormatter {
35116
let formatter = ISO8601DateFormatter()
36117
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
37118
return formatter
38119
}
39120

121+
var calendar = Calendar(identifier: .gregorian)
122+
40123
/// See https://project-haystack.org/doc/docHaystack/Json#dateTime
41124
extension DateTime {
42125
static let kindValue = "dateTime"
@@ -69,11 +152,9 @@ extension DateTime {
69152
}
70153

71154
let isoString = try container.decode(String.self, forKey: .val)
72-
if let date = dateTimeFormatter.date(from: isoString) {
73-
self.date = date
74-
} else if let date = dateTimeWithMillisFormatter.date(from: isoString) {
75-
self.date = date
76-
} else {
155+
do {
156+
self.date = try Self.dateFromString(isoString)
157+
} catch {
77158
throw DecodingError.typeMismatch(
78159
Self.self,
79160
.init(
@@ -83,7 +164,7 @@ extension DateTime {
83164
)
84165
}
85166

86-
let timezone = (try? container.decode(String.self, forKey: .tz)) ?? Self.gmtName
167+
let timezone = (try? container.decode(String.self, forKey: .tz)) ?? Self.utcName
87168
self.timezone = timezone
88169
}
89170

@@ -97,12 +178,8 @@ extension DateTime {
97178
isoString = dateTimeFormatter.string(from: self.date)
98179
}
99180
try container.encode(isoString, forKey: .val)
100-
if timezone != DateTime.gmtName {
181+
if timezone != DateTime.utcName {
101182
try container.encode(timezone, forKey: .tz)
102183
}
103184
}
104-
105-
private var hasMilliseconds: Bool {
106-
return date.timeIntervalSinceReferenceDate.remainder(dividingBy: 1.0) != 0.0
107-
}
108185
}

Sources/Haystack/ValError.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
public enum ValError: Error {
33
case invalidDateDefinition
44
case invalidDateFormat(String)
5+
case invalidDateTimeDefinition
6+
case invalidDateTimeFormat(String)
57
case invalidTimeFormat(String)
68
}

Tests/HaystackTests/DateTimeTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ final class DateTimeTests: XCTestCase {
55
func testJsonCoding() throws {
66
let value = DateTime(
77
date: Date(timeIntervalSince1970: 0.458),
8-
timezone: DateTime.gmtName
8+
timezone: DateTime.utcName
99
)
1010
let jsonString = #"{"_kind":"dateTime","val":"1970-01-01T00:00:00.458Z"}"#
1111

@@ -46,7 +46,7 @@ final class DateTimeTests: XCTestCase {
4646
XCTAssertEqual(
4747
DateTime(
4848
date: Date(timeIntervalSince1970: 0),
49-
timezone: DateTime.gmtName
49+
timezone: DateTime.utcName
5050
).toZinc(),
5151
"1970-01-01T00:00:00Z"
5252
)

0 commit comments

Comments
 (0)