-
Notifications
You must be signed in to change notification settings - Fork 460
/
Google_Protobuf_Timestamp+Extensions.swift
340 lines (302 loc) · 12.1 KB
/
Google_Protobuf_Timestamp+Extensions.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
// Sources/SwiftProtobuf/Google_Protobuf_Timestamp+Extensions.swift - Timestamp extensions
//
// Copyright (c) 2014 - 2016 Apple Inc. and the project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See LICENSE.txt for license information:
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
//
// -----------------------------------------------------------------------------
///
/// Extend the generated Timestamp message with customized JSON coding,
/// arithmetic operations, and convenience methods.
///
// -----------------------------------------------------------------------------
import Foundation
private let minTimestampSeconds: Int64 = -62_135_596_800 // 0001-01-01T00:00:00Z
private let maxTimestampSeconds: Int64 = 253_402_300_799 // 9999-12-31T23:59:59Z
// TODO: Add convenience methods to interoperate with standard
// date/time classes: an initializer that accepts Unix timestamp as
// Int or Double, an easy way to convert to/from Foundation's
// NSDateTime (on Apple platforms only?), others?
// Parse an RFC3339 timestamp into a pair of seconds-since-1970 and nanos.
private func parseTimestamp(s: String) throws -> (Int64, Int32) {
// Convert to an array of integer character values
let value = s.utf8.map { Int($0) }
if value.count < 20 {
throw JSONDecodingError.malformedTimestamp
}
// Since the format is fixed-layout, we can just decode
// directly as follows.
let zero = Int(48)
let nine = Int(57)
let dash = Int(45)
let colon = Int(58)
let plus = Int(43)
let letterT = Int(84)
let letterZ = Int(90)
let period = Int(46)
func fromAscii2(_ digit0: Int, _ digit1: Int) throws -> Int {
if digit0 < zero || digit0 > nine || digit1 < zero || digit1 > nine {
throw JSONDecodingError.malformedTimestamp
}
return digit0 * 10 + digit1 - 528
}
func fromAscii4(
_ digit0: Int,
_ digit1: Int,
_ digit2: Int,
_ digit3: Int
) throws -> Int {
if digit0 < zero || digit0 > nine
|| digit1 < zero || digit1 > nine
|| digit2 < zero || digit2 > nine
|| digit3 < zero || digit3 > nine
{
throw JSONDecodingError.malformedTimestamp
}
return digit0 * 1000 + digit1 * 100 + digit2 * 10 + digit3 - 53328
}
// Year: 4 digits followed by '-'
let year = try fromAscii4(value[0], value[1], value[2], value[3])
if value[4] != dash || year < Int(1) || year > Int(9999) {
throw JSONDecodingError.malformedTimestamp
}
// Month: 2 digits followed by '-'
let month = try fromAscii2(value[5], value[6])
if value[7] != dash || month < Int(1) || month > Int(12) {
throw JSONDecodingError.malformedTimestamp
}
// Day: 2 digits followed by 'T'
let mday = try fromAscii2(value[8], value[9])
if value[10] != letterT || mday < Int(1) || mday > Int(31) {
throw JSONDecodingError.malformedTimestamp
}
// Hour: 2 digits followed by ':'
let hour = try fromAscii2(value[11], value[12])
if value[13] != colon || hour > Int(23) {
throw JSONDecodingError.malformedTimestamp
}
// Minute: 2 digits followed by ':'
let minute = try fromAscii2(value[14], value[15])
if value[16] != colon || minute > Int(59) {
throw JSONDecodingError.malformedTimestamp
}
// Second: 2 digits (following char is checked below)
let second = try fromAscii2(value[17], value[18])
if second > Int(61) {
throw JSONDecodingError.malformedTimestamp
}
// timegm() is almost entirely useless. It's nonexistent on
// some platforms, broken on others. Everything else I've tried
// is even worse. Hence the code below.
// (If you have a better way to do this, try it and see if it
// passes the test suite on both Linux and OS X.)
// Day of year
let mdayStart: [Int] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
var yday = Int64(mdayStart[month - 1])
let isleap = (year % 400 == 0) || ((year % 100 != 0) && (year % 4 == 0))
if isleap && (month > 2) {
yday += 1
}
yday += Int64(mday - 1)
// Days since start of epoch (including leap days)
var daysSinceEpoch = yday
daysSinceEpoch += Int64(365 * year) - Int64(719527)
daysSinceEpoch += Int64((year - 1) / 4)
daysSinceEpoch -= Int64((year - 1) / 100)
daysSinceEpoch += Int64((year - 1) / 400)
// Second within day
var daySec = Int64(hour)
daySec *= 60
daySec += Int64(minute)
daySec *= 60
daySec += Int64(second)
// Seconds since start of epoch
let t = daysSinceEpoch * Int64(86400) + daySec
// After seconds, comes various optional bits
var pos = 19
var nanos: Int32 = 0
if value[pos] == period { // "." begins fractional seconds
pos += 1
var digitValue = 100_000_000
while pos < value.count && value[pos] >= zero && value[pos] <= nine {
nanos += Int32(digitValue * (value[pos] - zero))
digitValue /= 10
pos += 1
}
}
var seconds: Int64 = 0
// "Z" or "+" or "-" starts Timezone offset
if pos >= value.count {
throw JSONDecodingError.malformedTimestamp
} else if value[pos] == plus || value[pos] == dash {
if pos + 6 > value.count {
throw JSONDecodingError.malformedTimestamp
}
let hourOffset = try fromAscii2(value[pos + 1], value[pos + 2])
let minuteOffset = try fromAscii2(value[pos + 4], value[pos + 5])
if hourOffset > Int(13) || minuteOffset > Int(59) || value[pos + 3] != colon {
throw JSONDecodingError.malformedTimestamp
}
var adjusted: Int64 = t
if value[pos] == plus {
adjusted -= Int64(hourOffset) * Int64(3600)
adjusted -= Int64(minuteOffset) * Int64(60)
} else {
adjusted += Int64(hourOffset) * Int64(3600)
adjusted += Int64(minuteOffset) * Int64(60)
}
seconds = adjusted
pos += 6
} else if value[pos] == letterZ { // "Z" indicator for UTC
seconds = t
pos += 1
} else {
throw JSONDecodingError.malformedTimestamp
}
if pos != value.count {
throw JSONDecodingError.malformedTimestamp
}
guard seconds >= minTimestampSeconds && seconds <= maxTimestampSeconds else {
throw JSONDecodingError.malformedTimestamp
}
return (seconds, nanos)
}
private func formatTimestamp(seconds: Int64, nanos: Int32) -> String? {
let (seconds, nanos) = normalizeForTimestamp(seconds: seconds, nanos: nanos)
guard seconds >= minTimestampSeconds && seconds <= maxTimestampSeconds else {
return nil
}
let (hh, mm, ss) = timeOfDayFromSecondsSince1970(seconds: seconds)
let (YY, MM, DD) = gregorianDateFromSecondsSince1970(seconds: seconds)
let dateString = "\(fourDigit(YY))-\(twoDigit(MM))-\(twoDigit(DD))"
let timeString = "\(twoDigit(hh)):\(twoDigit(mm)):\(twoDigit(ss))"
let nanosString = nanosToString(nanos: nanos) // Includes leading '.' if needed
return "\(dateString)T\(timeString)\(nanosString)Z"
}
extension Google_Protobuf_Timestamp {
/// Creates a new `Google_Protobuf_Timestamp` equal to the given number of
/// seconds and nanoseconds.
///
/// - Parameter seconds: The number of seconds.
/// - Parameter nanos: The number of nanoseconds.
public init(seconds: Int64 = 0, nanos: Int32 = 0) {
self.init()
self.seconds = seconds
self.nanos = nanos
}
}
extension Google_Protobuf_Timestamp: _CustomJSONCodable {
mutating func decodeJSON(from decoder: inout JSONDecoder) throws {
let s = try decoder.scanner.nextQuotedString()
(seconds, nanos) = try parseTimestamp(s: s)
}
func encodedJSONString(options: JSONEncodingOptions) throws -> String {
if let formatted = formatTimestamp(seconds: seconds, nanos: nanos) {
return "\"\(formatted)\""
} else {
throw JSONEncodingError.timestampRange
}
}
}
extension Google_Protobuf_Timestamp {
/// Creates a new `Google_Protobuf_Timestamp` initialized relative to 00:00:00
/// UTC on 1 January 1970 by a given number of seconds.
///
/// - Parameter timeIntervalSince1970: The `TimeInterval`, interpreted as
/// seconds relative to 00:00:00 UTC on 1 January 1970.
public init(timeIntervalSince1970: TimeInterval) {
let sd = floor(timeIntervalSince1970)
let nd = round((timeIntervalSince1970 - sd) * TimeInterval(nanosPerSecond))
let (s, n) = normalizeForTimestamp(seconds: Int64(sd), nanos: Int32(nd))
self.init(seconds: s, nanos: n)
}
/// Creates a new `Google_Protobuf_Timestamp` initialized relative to 00:00:00
/// UTC on 1 January 2001 by a given number of seconds.
///
/// - Parameter timeIntervalSinceReferenceDate: The `TimeInterval`,
/// interpreted as seconds relative to 00:00:00 UTC on 1 January 2001.
public init(timeIntervalSinceReferenceDate: TimeInterval) {
let sd = floor(timeIntervalSinceReferenceDate)
let nd = round(
(timeIntervalSinceReferenceDate - sd) * TimeInterval(nanosPerSecond)
)
// The addition of timeIntervalBetween1970And... is deliberately delayed
// until the input is separated into an integer part and a fraction
// part, so that we don't unnecessarily lose precision.
let (s, n) = normalizeForTimestamp(
seconds: Int64(sd) + Int64(Date.timeIntervalBetween1970AndReferenceDate),
nanos: Int32(nd)
)
self.init(seconds: s, nanos: n)
}
/// Creates a new `Google_Protobuf_Timestamp` initialized to the same time as
/// the given `Date`.
///
/// - Parameter date: The `Date` with which to initialize the timestamp.
public init(date: Date) {
// Note: Internally, Date uses the "reference date," not the 1970 date.
// We use it when interacting with Dates so that Date doesn't perform
// any double arithmetic on our behalf, which might cost us precision.
self.init(
timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate
)
}
/// The interval between the timestamp and 00:00:00 UTC on 1 January 1970.
public var timeIntervalSince1970: TimeInterval {
TimeInterval(self.seconds) + TimeInterval(self.nanos) / TimeInterval(nanosPerSecond)
}
/// The interval between the timestamp and 00:00:00 UTC on 1 January 2001.
public var timeIntervalSinceReferenceDate: TimeInterval {
TimeInterval(
self.seconds - Int64(Date.timeIntervalBetween1970AndReferenceDate)
) + TimeInterval(self.nanos) / TimeInterval(nanosPerSecond)
}
/// A `Date` initialized to the same time as the timestamp.
public var date: Date {
Date(
timeIntervalSinceReferenceDate: self.timeIntervalSinceReferenceDate
)
}
}
private func normalizeForTimestamp(
seconds: Int64,
nanos: Int32
) -> (seconds: Int64, nanos: Int32) {
// The Timestamp spec says that nanos must be in the range [0, 999999999),
// as in actual modular arithmetic.
let s = seconds + Int64(div(nanos, nanosPerSecond))
let n = mod(nanos, nanosPerSecond)
return (seconds: s, nanos: n)
}
public func + (
lhs: Google_Protobuf_Timestamp,
rhs: Google_Protobuf_Duration
) -> Google_Protobuf_Timestamp {
let (s, n) = normalizeForTimestamp(
seconds: lhs.seconds + rhs.seconds,
nanos: lhs.nanos + rhs.nanos
)
return Google_Protobuf_Timestamp(seconds: s, nanos: n)
}
public func + (
lhs: Google_Protobuf_Duration,
rhs: Google_Protobuf_Timestamp
) -> Google_Protobuf_Timestamp {
let (s, n) = normalizeForTimestamp(
seconds: lhs.seconds + rhs.seconds,
nanos: lhs.nanos + rhs.nanos
)
return Google_Protobuf_Timestamp(seconds: s, nanos: n)
}
public func - (
lhs: Google_Protobuf_Timestamp,
rhs: Google_Protobuf_Duration
) -> Google_Protobuf_Timestamp {
let (s, n) = normalizeForTimestamp(
seconds: lhs.seconds - rhs.seconds,
nanos: lhs.nanos - rhs.nanos
)
return Google_Protobuf_Timestamp(seconds: s, nanos: n)
}