@@ -5,11 +5,13 @@ public struct DateTime: Val {
55 public static let utcName = " UTC "
66
77 public let date : Foundation . Date
8+ public let gmtOffset : Int
89 public let timezone : String
910
10- public init ( date: Foundation . Date , gmtOffset : Int = 0 , timezone : String = Self . utcName ) {
11+ public init ( date: Foundation . Date ) {
1112 self . date = date
12- self . timezone = timezone
13+ self . gmtOffset = 0
14+ self . timezone = Self . utcName
1315 }
1416
1517 public init (
@@ -38,6 +40,7 @@ public struct DateTime: Val {
3840 throw ValError . invalidDateTimeDefinition
3941 }
4042 self . date = date
43+ self . gmtOffset = gmtOffset
4144 self . timezone = timezone
4245 }
4346
@@ -62,13 +65,16 @@ public struct DateTime: Val {
6265 throw ValError . invalidDateTimeDefinition
6366 }
6467 self . date = date
68+ self . gmtOffset = gmtOffset
6569 self . timezone = timezone
6670 }
6771
6872 public init ( _ string: String ) throws {
6973 let splits = string. split ( separator: " " )
70- let dateTimeStr = String ( splits [ 0 ] )
71- self . date = try Self . dateFromString ( dateTimeStr)
74+ let isoString = String ( splits [ 0 ] )
75+ let ( date, gmtOffset) = try Self . dateFromString ( isoString)
76+ self . date = date
77+ self . gmtOffset = gmtOffset
7278 if splits. count > 1 {
7379 self . timezone = String ( splits [ 1 ] )
7480 } else {
@@ -89,33 +95,99 @@ public struct DateTime: Val {
8995 return zinc
9096 }
9197
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+ static func dateFromString( _ isoString: String ) throws -> ( Foundation . Date , Int ) {
99+ // Must use Regex so we can preserve GMT offset details. dateFormatter doesn't give us this
100+ let expr = try NSRegularExpression ( pattern: #"(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}\.?\d*)([+-]\d{2}:\d{2}|Z)"# )
101+ guard let match = expr. firstMatch (
102+ in: isoString,
103+ range: NSRange ( isoString. startIndex..< isoString. endIndex, in: isoString)
104+ ) else {
98105 throw ValError . invalidDateTimeFormat ( isoString)
99106 }
107+
108+ guard
109+ let dateRange = Range ( match. range ( at: 1 ) , in: isoString) ,
110+ let timeRange = Range ( match. range ( at: 2 ) , in: isoString) ,
111+ let offsetRange = Range ( match. range ( at: 3 ) , in: isoString)
112+ else {
113+ throw ValError . invalidDateTimeFormat ( isoString)
114+ }
115+
116+ let date = try Date ( String ( isoString [ dateRange] ) )
117+ let time = try Time ( String ( isoString [ timeRange] ) )
118+ let offsetStr = String ( isoString [ offsetRange] )
119+
120+ let gmtOffset : Int
121+ if offsetStr == " Z " {
122+ gmtOffset = 0
123+ } else {
124+ let offsetExpr = try NSRegularExpression ( pattern: #"([+-])(\d{2}):(\d{2})"# )
125+ guard let offsetMatch = offsetExpr. firstMatch (
126+ in: offsetStr,
127+ range: NSRange ( offsetStr. startIndex..< offsetStr. endIndex, in: offsetStr)
128+ ) else {
129+ throw ValError . invalidDateTimeFormat ( isoString)
130+ }
131+
132+ guard
133+ let symbolRange = Range ( offsetMatch. range ( at: 1 ) , in: offsetStr) ,
134+ let hourRange = Range ( offsetMatch. range ( at: 2 ) , in: offsetStr) ,
135+ let minuteRange = Range ( offsetMatch. range ( at: 3 ) , in: offsetStr) ,
136+ let hour = Int ( String ( offsetStr [ hourRange] ) ) ,
137+ let minute = Int ( String ( offsetStr [ minuteRange] ) )
138+ else {
139+ throw ValError . invalidDateTimeFormat ( isoString)
140+ }
141+ let sign = String ( offsetStr [ symbolRange] ) == " + " ? 1 : - 1
142+
143+ gmtOffset = sign * ( ( hour * 60 * 60 ) + ( minute * 60 ) )
144+ }
145+
146+ let components = DateComponents (
147+ calendar: calendar,
148+ timeZone: . init( secondsFromGMT: gmtOffset) ,
149+ year: date. year,
150+ month: date. month,
151+ day: date. day,
152+ hour: time. hour,
153+ minute: time. minute,
154+ second: time. second,
155+ nanosecond: time. millisecond * 1_000_000
156+ )
157+ guard let date = components. date else {
158+ throw ValError . invalidDateTimeDefinition
159+ }
160+
161+ return ( date, gmtOffset)
162+
163+ // if let date = dateTimeFormatter.date(from: isoString) {
164+ // return date
165+ // } else if let date = dateTimeWithMillisFormatter.date(from: isoString) {
166+ // return date
167+ // } else {
168+ // throw ValError.invalidDateTimeFormat(isoString)
169+ // }
100170 }
101171
102172 private var hasMilliseconds : Bool {
103173 return calendar. component ( . nanosecond, from: date) != 0
104174 }
105- }
106-
107- /// Singleton Haystack DateTime formatter
108- var dateTimeFormatter : ISO8601DateFormatter {
109- let formatter = ISO8601DateFormatter ( )
110- formatter. formatOptions = [ . withInternetDateTime ]
111- return formatter
112- }
175+
176+ /// Singleton Haystack DateTime formatter
177+ var dateTimeFormatter : ISO8601DateFormatter {
178+ let formatter = ISO8601DateFormatter ( )
179+ formatter. formatOptions = [ . withInternetDateTime ]
180+ formatter. timeZone = . init ( secondsFromGMT : gmtOffset )
181+ return formatter
182+ }
113183
114- /// Singleton Haystack DateTime formatter with fractional second support
115- var dateTimeWithMillisFormatter : ISO8601DateFormatter {
116- let formatter = ISO8601DateFormatter ( )
117- formatter. formatOptions = [ . withInternetDateTime, . withFractionalSeconds]
118- return formatter
184+ /// Singleton Haystack DateTime formatter with fractional second support
185+ var dateTimeWithMillisFormatter : ISO8601DateFormatter {
186+ let formatter = ISO8601DateFormatter ( )
187+ formatter. formatOptions = [ . withInternetDateTime, . withFractionalSeconds]
188+ formatter. timeZone = . init( secondsFromGMT: gmtOffset)
189+ return formatter
190+ }
119191}
120192
121193var calendar = Calendar ( identifier: . gregorian)
@@ -153,7 +225,9 @@ extension DateTime {
153225
154226 let isoString = try container. decode ( String . self, forKey: . val)
155227 do {
156- self . date = try Self . dateFromString ( isoString)
228+ let ( date, gmtOffset) = try Self . dateFromString ( isoString)
229+ self . date = date
230+ self . gmtOffset = gmtOffset
157231 } catch {
158232 throw DecodingError . typeMismatch (
159233 Self . self,
0 commit comments