taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }