Time.swift (10147B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Jonathan Buchanan 7 * @author Marc Stibane 8 */ 9 import Foundation 10 11 /// Errors for `Timestamp`. 12 enum TimestampError: Error { 13 /// The string cannot be parsed to create an `Amount`. 14 case invalidStringRepresentation 15 16 /// Invalid arguments were supplied to an arithmetic operation. 17 case invalidArithmeticArguments 18 19 /// The value `never` cannot be returned as UInt64. 20 case invalidUInt64Value 21 22 /// The number of seconds is outside the range the protocol allows. 23 case invalidSecondsValue 24 } 25 // MARK: - 26 /// A point in time, represented by milliseconds from January 1, 1970.. 27 public enum Timestamp: Codable, Hashable, Sendable { 28 case milliseconds(UInt64) 29 case never 30 31 enum CodingKeys: String, CodingKey { 32 case t_s = "t_s" 33 case t_ms = "t_ms" 34 } 35 36 /// The protocol declares every timestamp as an integer capped at 2^53-1. 37 static let maxSeconds: UInt64 = (1 << 53) - 1 38 39 public init(from decoder: Decoder) throws { 40 let container = try decoder.container(keyedBy: CodingKeys.self) 41 if let seconds: UInt64 = try? container.decode(UInt64.self, forKey: .t_s) { 42 // Reject out of range instead of letting `seconds * 1000` overflow, which 43 // would trap - a crash no do/catch around the decode could survive. 44 guard seconds <= Self.maxSeconds else { throw TimestampError.invalidSecondsValue } 45 self = Timestamp.milliseconds(seconds * 1000) 46 return 47 } 48 do { 49 self = Timestamp.milliseconds(try container.decode(UInt64.self, forKey: .t_ms)) 50 } catch { // rethrows or never 51 let stringValue = try container.decode(String.self, forKey: .t_s) 52 if stringValue == "never" { 53 self = Timestamp.never 54 } else { 55 throw TimestampError.invalidStringRepresentation 56 } 57 } 58 } 59 public func hash(into hasher: inout Hasher) { 60 switch self { 61 case .milliseconds(let t_ms): 62 hasher.combine(t_ms) 63 case .never: 64 hasher.combine("never") 65 } 66 } 67 public func encode(to encoder: Encoder) throws { 68 var value = encoder.container(keyedBy: CodingKeys.self) 69 switch self { 70 case .milliseconds(let t_ms): 71 try value.encode(t_ms / 1000, forKey: .t_s) 72 case .never: 73 try value.encode("never", forKey: .t_s) 74 } 75 } 76 } 77 78 extension Timestamp: Comparable { 79 public static func < (lhs: Timestamp, rhs: Timestamp) -> Bool { 80 switch lhs { 81 case .milliseconds(let lhs_ms): 82 switch rhs { 83 case .milliseconds(let rhs_ms): 84 return lhs_ms < rhs_ms 85 case .never: return true 86 } 87 case .never: break 88 } 89 return false 90 } 91 public static func == (lhs: Timestamp, rhs: Timestamp) -> Bool { 92 switch lhs { 93 case .never: 94 switch rhs { 95 case .never: return true 96 case .milliseconds(_): break 97 } 98 case .milliseconds(let lhs_ms): 99 switch rhs { 100 case .milliseconds(let rhs_ms): 101 return lhs_ms == rhs_ms 102 case .never: break 103 } 104 } 105 return false 106 } 107 } 108 109 extension Timestamp { 110 /// make a Timestamp for `now` 111 public static func now() -> Timestamp { 112 return Timestamp.milliseconds(Date().millisecondsSince1970) 113 } 114 115 public static func tomorrow() -> Timestamp { 116 return Timestamp.inSomeDays(1) 117 } 118 119 public static func inSomeDays(_ days: UInt) -> Timestamp { 120 let now = Date().millisecondsSince1970 121 let seconds: UInt64 = 60 * 60 * 24 122 return Timestamp.milliseconds(now + (UInt64(days) * seconds * 1000)) 123 } 124 125 public static func inSomeMinutes(_ minutes: UInt) -> Timestamp { 126 let now = Date().millisecondsSince1970 127 let seconds: UInt64 = 60 128 return Timestamp.milliseconds(now + (UInt64(minutes) * seconds * 1000)) 129 } 130 131 /// convenience initializer from UInt64 (milliseconds from January 1, 1970) 132 public init(from: UInt64) { 133 self = Timestamp.milliseconds(from) 134 } 135 136 /// switch-case the enum here so the caller function doesn't have to 137 public func milliseconds() throws -> UInt64 { 138 switch self { 139 case .milliseconds(let t_ms): 140 return t_ms 141 case .never: 142 throw TimestampError.invalidUInt64Value 143 } 144 } 145 public func duration() throws -> Duration { 146 do { 147 let result: Duration = try self - Timestamp.now() 148 return result 149 } catch { 150 throw TimestampError.invalidUInt64Value 151 } 152 } 153 } 154 // MARK: - 155 /// extend iOS Date to work with milliseconds since 1970 156 extension Date { 157 public var millisecondsSince1970: UInt64 { 158 UInt64((timeIntervalSince1970 * 1000.0).rounded()) 159 } 160 161 public init(milliseconds: UInt64) { 162 self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000) 163 } 164 } 165 // MARK: - 166 /// A duration of time, measured in microseconds. 167 public enum Duration: Codable, Hashable, Sendable { 168 case microseconds(UInt64) 169 case forever 170 171 enum CodingKeys: String, CodingKey { 172 case d_us = "d_us" 173 } 174 175 public init(from decoder: Decoder) throws { 176 let container = try decoder.container(keyedBy: CodingKeys.self) 177 do { 178 self = Duration.microseconds(try container.decode(UInt64.self, forKey: .d_us)) 179 } catch { // rethrows or never 180 let stringValue = try container.decode(String.self, forKey: .d_us) 181 if stringValue == "forever" { 182 self = Duration.forever 183 } else { 184 throw TimestampError.invalidStringRepresentation 185 } 186 } 187 } 188 public func hash(into hasher: inout Hasher) { 189 switch self { 190 case .microseconds(let d_us): 191 hasher.combine(d_us) 192 case .forever: 193 hasher.combine("forever") 194 } 195 } 196 public func encode(to encoder: Encoder) throws { 197 var value = encoder.container(keyedBy: CodingKeys.self) 198 switch self { 199 case .microseconds(let d_us): 200 try value.encode(d_us, forKey: .d_us) 201 case .forever: 202 try value.encode("forever", forKey: .d_us) 203 } 204 } 205 206 public func microseconds() throws -> UInt64 { 207 switch self { 208 case .microseconds(let d_us): 209 return d_us 210 case .forever: 211 throw TimestampError.invalidUInt64Value 212 } 213 } 214 215 public func toMilliseconds() -> UInt64 { 216 switch self { 217 case .microseconds(let d_us): 218 return d_us / 1000 219 case .forever: 220 return .max 221 } 222 } 223 public func toSeconds() -> UInt64 { 224 switch self { 225 case .microseconds(let d_us): 226 return d_us / 1_000_000 227 case .forever: 228 return .max 229 } 230 } 231 public func toMinutes() -> UInt64 { 232 switch self { 233 case .microseconds(let d_us): 234 return d_us / 60_000_000 235 case .forever: 236 return .max 237 } 238 } 239 public func toHours() -> UInt64 { 240 switch self { 241 case .microseconds(let d_us): 242 return d_us / 3_600_000_000 243 case .forever: 244 return .max 245 } 246 } 247 public func toDays() -> UInt64 { 248 switch self { 249 case .microseconds(let d_us): 250 return d_us / 86_400_000_000 251 case .forever: 252 return .max 253 } 254 } 255 } 256 257 /// Addition of a timestamp and a duration. 258 public func +(lhs: Timestamp, rhs: Duration) -> Timestamp { 259 switch lhs { 260 case .milliseconds(let lhs_ms): 261 switch rhs { 262 case .microseconds(let rhs_us): 263 let result = lhs_ms.addingReportingOverflow(rhs_us / 1000) 264 if result.overflow { 265 return Timestamp.never 266 } else { 267 return Timestamp.milliseconds(result.partialValue) 268 } 269 case .forever: 270 return Timestamp.never 271 } 272 case .never: 273 return Timestamp.never 274 } 275 } 276 277 /// Subtraction of a duration from a timestamp. 278 public func -(lhs: Timestamp, rhs: Duration) -> Timestamp { 279 switch lhs { 280 case .milliseconds(let lhs_ms): 281 switch rhs { 282 case .microseconds(let rhs_us): 283 let result = lhs_ms.subtractingReportingOverflow(rhs_us / 1000) 284 if result.overflow { 285 return Timestamp.milliseconds(0) 286 } else { 287 return Timestamp.milliseconds(result.partialValue) 288 } 289 case .forever: 290 return Timestamp.milliseconds(0) 291 } 292 case .never: 293 return Timestamp.never 294 } 295 } 296 297 /// Calculates the difference between two timestamps. 298 public func -(lhs: Timestamp, rhs: Timestamp) throws -> Duration { 299 switch lhs { 300 case .milliseconds(let lhs_ms): 301 switch rhs { 302 case .milliseconds(let rhs_ms): 303 if lhs_ms < rhs_ms { 304 return Duration.microseconds(0) 305 } else { 306 // A difference of more than ~585000 years cannot be expressed in 307 // microseconds. Saturate like + and - with a Duration do, don't trap. 308 let result = (lhs_ms - rhs_ms).multipliedReportingOverflow(by: 1000) 309 if result.overflow { 310 return Duration.forever 311 } else { 312 return Duration.microseconds(result.partialValue) 313 } 314 } 315 case .never: 316 throw TimestampError.invalidArithmeticArguments 317 } 318 case .never: 319 return Duration.forever 320 } 321 }