/src/swift-protobuf/Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift
Line | Count | Source |
1 | | // Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift - Extensions for Duration type |
2 | | // |
3 | | // Copyright (c) 2014 - 2016 Apple Inc. and the project authors |
4 | | // Licensed under Apache License v2.0 with Runtime Library Exception |
5 | | // |
6 | | // See LICENSE.txt for license information: |
7 | | // https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt |
8 | | // |
9 | | // ----------------------------------------------------------------------------- |
10 | | /// |
11 | | /// Extends the generated Duration struct with various custom behaviors: |
12 | | /// * JSON coding and decoding |
13 | | /// * Arithmetic operations |
14 | | /// |
15 | | // ----------------------------------------------------------------------------- |
16 | | |
17 | | #if canImport(FoundationEssentials) |
18 | | import FoundationEssentials |
19 | | #else |
20 | | import Foundation |
21 | | #endif |
22 | | |
23 | | private let minDurationSeconds: Int64 = -maxDurationSeconds |
24 | | private let maxDurationSeconds: Int64 = 315_576_000_000 |
25 | | private let minDurationNanos: Int32 = -maxDurationNanos |
26 | | private let maxDurationNanos: Int32 = 999_999_999 |
27 | | |
28 | 4.95k | private func parseDuration(text: String) throws -> (Int64, Int32) { |
29 | 4.95k | var digits = [Character]() |
30 | 4.95k | var digitCount = 0 |
31 | 4.95k | var total = 0 |
32 | 4.95k | var chars = text.makeIterator() |
33 | 4.95k | var seconds: Int64? |
34 | 4.95k | var nanos: Int32 = 0 |
35 | 4.95k | var isNegative = false |
36 | 3.41M | while let c = chars.next() { |
37 | 3.41M | switch c { |
38 | 3.41M | case "-": |
39 | 2.09k | // Only accept '-' as very first character |
40 | 2.09k | if total > 0 { |
41 | 7 | throw JSONDecodingError.malformedDuration |
42 | 2.08k | } |
43 | 2.08k | digits.append(c) |
44 | 2.08k | isNegative = true |
45 | 3.41M | case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": |
46 | 3.40M | digits.append(c) |
47 | 3.40M | digitCount += 1 |
48 | 3.41M | case ".": |
49 | 3.25k | if let _ = seconds { |
50 | 2 | throw JSONDecodingError.malformedDuration |
51 | 3.24k | } |
52 | 3.24k | let digitString = String(digits) |
53 | 3.24k | if let s = Int64(digitString), |
54 | 3.24k | s >= minDurationSeconds && s <= maxDurationSeconds |
55 | 3.24k | { |
56 | 3.16k | seconds = s |
57 | 3.16k | } else { |
58 | 86 | throw JSONDecodingError.malformedDuration |
59 | 3.16k | } |
60 | 3.16k | digits.removeAll() |
61 | 3.16k | digitCount = 0 |
62 | 3.41M | case "s": |
63 | 4.51k | if let _ = seconds { |
64 | 3.10k | // Seconds already set, digits holds nanos. The fraction must |
65 | 3.10k | // have between 1 and 9 digits; matches the protobuf grammar |
66 | 3.10k | // and the reference JSON parser, which reject an empty |
67 | 3.10k | // fraction or more than nanosecond precision. |
68 | 3.10k | if digitCount < 1 || digitCount > 9 { |
69 | 31 | throw JSONDecodingError.malformedDuration |
70 | 3.07k | } |
71 | 19.8k | while digitCount < 9 { |
72 | 16.7k | digits.append(Character("0")) |
73 | 16.7k | digitCount += 1 |
74 | 16.7k | } |
75 | 3.07k | let digitString = String(digits) |
76 | 3.07k | if let rawNanos = Int32(digitString) { |
77 | 3.07k | if isNegative { |
78 | 1.31k | nanos = -rawNanos |
79 | 1.75k | } else { |
80 | 1.75k | nanos = rawNanos |
81 | 1.75k | } |
82 | 3.07k | } else { |
83 | 0 | throw JSONDecodingError.malformedDuration |
84 | 3.07k | } |
85 | 3.07k | } else { |
86 | 1.41k | // No fraction, we just have an integral number of seconds |
87 | 1.41k | let digitString = String(digits) |
88 | 1.41k | if let s = Int64(digitString), |
89 | 1.41k | s >= minDurationSeconds && s <= maxDurationSeconds |
90 | 1.41k | { |
91 | 1.31k | seconds = s |
92 | 1.31k | } else { |
93 | 99 | throw JSONDecodingError.malformedDuration |
94 | 1.31k | } |
95 | 4.38k | } |
96 | 4.38k | // Fail if there are characters after 's' |
97 | 4.38k | if chars.next() != nil { |
98 | 32 | throw JSONDecodingError.malformedDuration |
99 | 4.35k | } |
100 | 4.35k | return (seconds!, nanos) |
101 | 3.41M | default: |
102 | 156 | throw JSONDecodingError.malformedDuration |
103 | 3.41M | } |
104 | 3.40M | total += 1 |
105 | 3.40M | } |
106 | 185 | throw JSONDecodingError.malformedDuration |
107 | 4.95k | } |
108 | | |
109 | 2.36k | private func formatDuration(seconds: Int64, nanos: Int32) -> String? { |
110 | 2.36k | // Upstream's json file unparse.cc:WriteDuration() for these checks for reference. |
111 | 2.36k | |
112 | 2.36k | // Range check... |
113 | 2.36k | guard |
114 | 2.36k | (seconds >= minDurationSeconds && seconds <= maxDurationSeconds) |
115 | 2.36k | && (nanos >= minDurationNanos && nanos <= maxDurationNanos) |
116 | 2.36k | else { |
117 | 0 | return nil |
118 | 2.36k | } |
119 | 2.36k | // Either seconds or nanos has to be zero otherwise the signs must match. |
120 | 2.36k | guard (seconds == 0) || (nanos == 0) || ((seconds < 0) == (nanos < 0)) else { |
121 | 0 | return nil |
122 | 2.36k | } |
123 | 2.36k | let nanosString = nanosToString(nanos: nanos) // Includes leading '.' if needed |
124 | 2.36k | if seconds == 0 && nanos < 0 { |
125 | 391 | return "-0\(nanosString)s" |
126 | 1.97k | } |
127 | 1.97k | return "\(seconds)\(nanosString)s" |
128 | 2.36k | } |
129 | | |
130 | | extension Google_Protobuf_Duration { |
131 | | /// Creates a new `Google_Protobuf_Duration` equal to the given number of |
132 | | /// seconds and nanoseconds. |
133 | | /// |
134 | | /// - Parameter seconds: The number of seconds. |
135 | | /// - Parameter nanos: The number of nanoseconds. |
136 | 0 | public init(seconds: Int64 = 0, nanos: Int32 = 0) { |
137 | 0 | self.init() |
138 | 0 | self.seconds = seconds |
139 | 0 | self.nanos = nanos |
140 | 0 | } |
141 | | } |
142 | | |
143 | | extension Google_Protobuf_Duration: _CustomJSONCodable { |
144 | 3.50k | mutating func decodeJSON(from decoder: inout JSONDecoder) throws { |
145 | 3.50k | let s = try decoder.scanner.nextQuotedString() |
146 | 3.48k | (seconds, nanos) = try parseDuration(text: s) |
147 | 3.07k | } |
148 | 1.53k | func encodedJSONString(options: JSONEncodingOptions) throws -> String { |
149 | 1.53k | if let formatted = formatDuration(seconds: seconds, nanos: nanos) { |
150 | 1.53k | return "\"\(formatted)\"" |
151 | 1.53k | } else { |
152 | 0 | throw JSONEncodingError.durationRange |
153 | 0 | } |
154 | 1.53k | } |
155 | | } |
156 | | |
157 | | extension Google_Protobuf_Duration: ExpressibleByFloatLiteral { |
158 | | public typealias FloatLiteralType = Double |
159 | | |
160 | | /// Creates a new `Google_Protobuf_Duration` from a floating point literal |
161 | | /// that is interpreted as a duration in seconds, rounded to the nearest |
162 | | /// nanosecond. |
163 | 0 | public init(floatLiteral value: Double) { |
164 | 0 | self.init(rounding: value, rule: .toNearestOrAwayFromZero) |
165 | 0 | } |
166 | | } |
167 | | |
168 | | extension Google_Protobuf_Duration { |
169 | | #if !REMOVE_DEPRECATED_APIS |
170 | | /// Creates a new `Google_Protobuf_Duration` that is equal to the given |
171 | | /// `TimeInterval` (measured in seconds), rounded to the nearest nanosecond. |
172 | | /// |
173 | | /// - Parameter timeInterval: The `TimeInterval`. |
174 | | @available(*, deprecated, renamed: "init(rounding:rule:)") |
175 | 0 | public init(timeInterval: TimeInterval) { |
176 | 0 | self.init(rounding: timeInterval, rule: .toNearestOrAwayFromZero) |
177 | 0 | } |
178 | | #endif // !REMOVE_DEPRECATED_APIS |
179 | | |
180 | | /// Creates a new `Google_Protobuf_Duration` that is equal to the given |
181 | | /// `TimeInterval` (measured in seconds), rounded to the nearest nanosecond |
182 | | /// according to the given rounding rule. |
183 | | /// |
184 | | /// - Parameters: |
185 | | /// - timeInterval: The `TimeInterval`. |
186 | | /// - rule: The rounding rule to use. |
187 | | public init( |
188 | | rounding timeInterval: TimeInterval, |
189 | | rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero |
190 | 0 | ) { |
191 | 0 | let sd = Int64(timeInterval) |
192 | 0 | let nd = ((timeInterval - Double(sd)) * TimeInterval(nanosPerSecond)).rounded(rule) |
193 | 0 | // Normalize is here incase things round up to a full second worth of nanos. |
194 | 0 | let (s, n) = normalizeForDuration(seconds: sd, nanos: Int32(nd)) |
195 | 0 | self.init(seconds: s, nanos: n) |
196 | 0 | } |
197 | | |
198 | | /// The `TimeInterval` (measured in seconds) equal to this duration. |
199 | 0 | public var timeInterval: TimeInterval { |
200 | 0 | TimeInterval(self.seconds) + TimeInterval(self.nanos) / TimeInterval(nanosPerSecond) |
201 | 0 | } |
202 | | } |
203 | | |
204 | | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) |
205 | | extension Google_Protobuf_Duration { |
206 | | /// Creates a new `Google_Protobuf_Duration` by rounding a `Duration` to |
207 | | /// the nearest nanosecond according to the given rounding rule. |
208 | | /// |
209 | | /// - Parameters: |
210 | | /// - duration: The `Duration`. |
211 | | /// - rule: The rounding rule to use. |
212 | | public init( |
213 | | rounding duration: Duration, |
214 | | rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero |
215 | 0 | ) { |
216 | 0 | let secs = duration.components.seconds |
217 | 0 | let attos = duration.components.attoseconds |
218 | 0 | let fracNanos = |
219 | 0 | (Double(attos % attosPerNanosecond) / Double(attosPerNanosecond)).rounded(rule) |
220 | 0 | let nanos = Int32(attos / attosPerNanosecond) + Int32(fracNanos) |
221 | 0 | // Normalize is here incase things round up to a full second worth of nanos. |
222 | 0 | let (s, n) = normalizeForDuration(seconds: secs, nanos: nanos) |
223 | 0 | self.init(seconds: s, nanos: n) |
224 | 0 | } |
225 | | } |
226 | | |
227 | | @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) |
228 | | extension Duration { |
229 | | /// Creates a new `Duration` that is equal to the given duration. |
230 | | /// |
231 | | /// Swift `Duration` has a strictly higher precision than `Google_Protobuf_Duration` |
232 | | /// (attoseconds vs. nanoseconds, respectively), so this conversion is always |
233 | | /// value-preserving. |
234 | | /// |
235 | | /// - Parameters: |
236 | | /// - duration: The `Google_Protobuf_Duration`. |
237 | 0 | public init(_ duration: Google_Protobuf_Duration) { |
238 | 0 | self.init( |
239 | 0 | secondsComponent: duration.seconds, |
240 | 0 | attosecondsComponent: Int64(duration.nanos) * attosPerNanosecond |
241 | 0 | ) |
242 | 0 | } |
243 | | } |
244 | | |
245 | | private func normalizeForDuration( |
246 | | seconds: Int64, |
247 | | nanos: Int32 |
248 | 0 | ) -> (seconds: Int64, nanos: Int32) { |
249 | 0 | var s = seconds |
250 | 0 | var n = nanos |
251 | 0 |
|
252 | 0 | // If the magnitude of n exceeds a second then |
253 | 0 | // we need to factor it into s instead. |
254 | 0 | if n >= nanosPerSecond || n <= -nanosPerSecond { |
255 | 0 | s += Int64(n / nanosPerSecond) |
256 | 0 | n = n % nanosPerSecond |
257 | 0 | } |
258 | 0 |
|
259 | 0 | // The Duration spec says that when s != 0, s and |
260 | 0 | // n must have the same sign. |
261 | 0 | if s > 0 && n < 0 { |
262 | 0 | n += nanosPerSecond |
263 | 0 | s -= 1 |
264 | 0 | } else if s < 0 && n > 0 { |
265 | 0 | n -= nanosPerSecond |
266 | 0 | s += 1 |
267 | 0 | } |
268 | 0 |
|
269 | 0 | return (seconds: s, nanos: n) |
270 | 0 | } |
271 | | |
272 | | public prefix func - ( |
273 | | operand: Google_Protobuf_Duration |
274 | 0 | ) -> Google_Protobuf_Duration { |
275 | 0 | // This gets normalized (thus allowing an otherwise non-logical input) so it matches what would |
276 | 0 | // happen if doing `Duration(0,0) - operand` because that has to normalize to handle roll |
277 | 0 | // over/under. |
278 | 0 | let (s, n) = normalizeForDuration( |
279 | 0 | seconds: -operand.seconds, |
280 | 0 | nanos: -operand.nanos |
281 | 0 | ) |
282 | 0 | return Google_Protobuf_Duration(seconds: s, nanos: n) |
283 | 0 | } |
284 | | |
285 | | public func + ( |
286 | | lhs: Google_Protobuf_Duration, |
287 | | rhs: Google_Protobuf_Duration |
288 | 0 | ) -> Google_Protobuf_Duration { |
289 | 0 | let (s, n) = normalizeForDuration( |
290 | 0 | seconds: lhs.seconds + rhs.seconds, |
291 | 0 | nanos: lhs.nanos + rhs.nanos |
292 | 0 | ) |
293 | 0 | return Google_Protobuf_Duration(seconds: s, nanos: n) |
294 | 0 | } |
295 | | |
296 | | public func - ( |
297 | | lhs: Google_Protobuf_Duration, |
298 | | rhs: Google_Protobuf_Duration |
299 | 0 | ) -> Google_Protobuf_Duration { |
300 | 0 | let (s, n) = normalizeForDuration( |
301 | 0 | seconds: lhs.seconds - rhs.seconds, |
302 | 0 | nanos: lhs.nanos - rhs.nanos |
303 | 0 | ) |
304 | 0 | return Google_Protobuf_Duration(seconds: s, nanos: n) |
305 | 0 | } |
306 | | |
307 | | public func - ( |
308 | | lhs: Google_Protobuf_Timestamp, |
309 | | rhs: Google_Protobuf_Timestamp |
310 | 0 | ) -> Google_Protobuf_Duration { |
311 | 0 | let (s, n) = normalizeForDuration( |
312 | 0 | seconds: lhs.seconds - rhs.seconds, |
313 | 0 | nanos: lhs.nanos - rhs.nanos |
314 | 0 | ) |
315 | 0 | return Google_Protobuf_Duration(seconds: s, nanos: n) |
316 | 0 | } |