File size: 15,346 Bytes
6ee917b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
import Foundation
import Vapor


public enum TimeError: Error {
    case InvalidDateFromat
    case InvalidDate
}

extension TimeError: AbortError {
    public var status: NIOHTTP1.HTTPResponseStatus {
        return .badRequest
    }
    
    public var reason: String {
        switch self {
        case .InvalidDateFromat:
            return "Invalid date format. Make sure to use 'YYYY-MM-DD'"
        case .InvalidDate:
            return "Invalid date"
        }
    }
}


public struct Timestamp: Hashable {
    public let timeIntervalSince1970: Int
    
    /// Hour in 0-23
    @inlinable public var hour: Int {
        timeIntervalSince1970.moduloPositive(86400) / 3600
    }
    /// Minute in 0-59
    @inlinable public var minute: Int {
        timeIntervalSince1970.moduloPositive(3600) / 60
    }
    /// Second in 0-59
    @inlinable public var second: Int {
        timeIntervalSince1970.moduloPositive(60)
    }
    
    public static func now() -> Timestamp {
        return Timestamp(Int(Date().timeIntervalSince1970))
    }
    
    /// month 1-12, day 1-31
    public init(_ year: Int, _ month: Int, _ day: Int, _ hour: Int = 0, _ minute: Int = 0, _ second: Int = 0) {
        assert(month > 0)
        assert(day > 0)
        assert(year >= 1900)
        assert(month <= 12)
        assert(day <= 31)
        var t = tm(tm_sec: Int32(second), tm_min: Int32(minute), tm_hour: Int32(hour), tm_mday: Int32(day), tm_mon: Int32(month-1), tm_year: Int32(year-1900), tm_wday: 0, tm_yday: 0, tm_isdst: 0, tm_gmtoff: 0, tm_zone: nil)
        self.timeIntervalSince1970 = timegm(&t)
    }
    
    public init(_ timeIntervalSince1970: Int) {
        self.timeIntervalSince1970 = timeIntervalSince1970
    }
    
    /// Decode strings like `20231231` or even with hours, minutes or seconds `20231231235959`
    static func from(yyyymmdd str: String) throws -> Timestamp {
        guard str.count >= 8, str.count <= 14 else {
            throw TimeError.InvalidDateFromat
        }
        guard let year = Int(str[0..<4]), year >= 1900, year <= 2200 else {
            throw TimeError.InvalidDate
        }
        guard let month = Int(str[4..<6]), month >= 1, month <= 12 else {
            throw TimeError.InvalidDate
        }
        guard let day = Int(str[6..<8]), day >= 1, day <= 31 else {
            throw TimeError.InvalidDate
        }
        if str.count < 10 {
            return Timestamp(year, month, day)
        }
        guard let hour = Int(str[8..<10]), hour >= 0, hour <= 23 else {
            throw TimeError.InvalidDate
        }
        if str.count < 12 {
            return Timestamp(year, month, day, hour)
        }
        guard let minute = Int(str[10..<12]), minute >= 0, minute <= 59 else {
            throw TimeError.InvalidDate
        }
        if str.count < 14 {
            return Timestamp(year, month, day, hour, minute)
        }
        guard let second = Int(str[12..<14]), second >= 0, second <= 59 else {
            throw TimeError.InvalidDate
        }
        return Timestamp(year, month, day, hour, minute, second)
    }
    
    public func add(_ secounds: Int) -> Timestamp {
        Timestamp(timeIntervalSince1970 + secounds)
    }
    
    public func add(days: Int) -> Timestamp {
        Timestamp(timeIntervalSince1970 + days * 86400)
    }
    
    public func add(hours: Int) -> Timestamp {
        Timestamp(timeIntervalSince1970 + hours * 3600)
    }
    
    public func floor(toNearest: Int) -> Timestamp {
        Timestamp(timeIntervalSince1970 - timeIntervalSince1970.moduloPositive(toNearest))
    }
    
    /// Floor to the nearest multiple of given hoiurs. E.g. 6 would floor hour 20 to 18.
    public func floor(toNearestHour: Int) -> Timestamp {
        self.floor(toNearest: toNearestHour * 3600)
    }
    
    public func ceil(toNearest: Int) -> Timestamp {
        Timestamp(timeIntervalSince1970.ceil(to: toNearest))
    }
    
    public func toComponents() -> IsoDate {
        return IsoDate(timeIntervalSince1970: timeIntervalSince1970)
    }
    
    public func subtract(days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0) -> Timestamp {
        let dtSeconds = seconds + minutes * 60 + hours * 3600 + days * 86400
        return Timestamp(self.timeIntervalSince1970 - dtSeconds)
    }
    
    public func olderThan(days: Int = 0, hours: Int = 0, minutes: Int = 0, seconds: Int = 0) -> Bool {
        let dtSeconds = seconds + minutes * 60 + hours * 3600 + days * 86400
        return timeIntervalSince1970 < Timestamp.now().timeIntervalSince1970 - dtSeconds
    }
    
    enum Weekday: Int8 {
        case sunday = 0
        case monday = 1
        case tuesday = 2
        case wednesday = 3
        case thursday = 4
        case friday = 5
        case saturday = 6
    }
    
    /// Day of the week
    var weekday: Weekday {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        return Weekday(rawValue: Int8(t.tm_wday))!
    }
    
    /// With format `yyyy-MM-dd'T'HH:mm'`
    var iso8601_YYYY_MM_dd_HH_mm: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        let hour = Int(t.tm_hour)
        let minute = Int(t.tm_min)
        return "\(year)-\(month.zeroPadded(len: 2))-\(day.zeroPadded(len: 2))T\(hour.zeroPadded(len: 2)):\(minute.zeroPadded(len: 2))"
    }
    
    /// With format `yyyy-MM-dd'T'HHmm'`
    var iso8601_YYYY_MM_dd_HHmm: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        let hour = Int(t.tm_hour)
        let minute = Int(t.tm_min)
        return "\(year)-\(month.zeroPadded(len: 2))-\(day.zeroPadded(len: 2))T\(hour.zeroPadded(len: 2))\(minute.zeroPadded(len: 2))"
    }
    
    /// With format `yyyyMMdd'T'HHmm'`
    var iso8601_YYYYMMddTHHmm: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        let hour = Int(t.tm_hour)
        let minute = Int(t.tm_min)
        return "\(year)\(month.zeroPadded(len: 2))\(day.zeroPadded(len: 2))T\(hour.zeroPadded(len: 2))\(minute.zeroPadded(len: 2))"
    }
    
    /// With format `yyyy-MM-dd`
    var iso8601_YYYY_MM_dd: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        return "\(year)-\(month.zeroPadded(len: 2))-\(day.zeroPadded(len: 2))"
    }
    
    /// With format `yyyyMMdd`
    var format_YYYYMMdd: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        return "\(year)\(month.zeroPadded(len: 2))\(day.zeroPadded(len: 2))"
    }
    
    /// With format `yyyy/MM/dd`
    var format_directoriesYYYYMMdd: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        return "\(year)/\(month.zeroPadded(len: 2))/\(day.zeroPadded(len: 2))"
    }
    
    /// With format `yyyyMMddHH`
    var format_YYYYMMddHH: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        let hour = Int(t.tm_hour)
        return "\(year)\(month.zeroPadded(len: 2))\(day.zeroPadded(len: 2))\(hour.zeroPadded(len: 2))"
    }
    
    /// With format `yyyyMMddHHmm`
    var format_YYYYMMddHHmm: String {
        var time = timeIntervalSince1970
        var t = tm()
        gmtime_r(&time, &t)
        let year = Int(t.tm_year+1900)
        let month = Int(t.tm_mon+1)
        let day = Int(t.tm_mday)
        let hour = Int(t.tm_hour)
        return "\(year)\(month.zeroPadded(len: 2))\(day.zeroPadded(len: 2))\(hour.zeroPadded(len: 2))\(minute.zeroPadded(len: 2))"
    }
    
    // Return hour string as 2 character
    var hh: String {
        hour.zeroPadded(len: 2)
    }
    
    // Return minute string as 2 character
    var mm: String {
        minute.zeroPadded(len: 2)
    }
    
    /// Return a new timestamp with setting the hour
    func with(hour: Int) -> Timestamp {
        return Timestamp(timeIntervalSince1970 / 86400 * 86400 + hour * 3600)
    }
    
    /// Return a new timestamp with setting the day and hour
    func with(year: Int? = nil, month: Int? = nil, day: Int? = nil) -> Timestamp {
        let date = toComponents()
        return Timestamp(year ?? date.year, month ?? date.month, day ?? date.day)
    }
}

extension Timestamp: Comparable {
    public static func < (lhs: Timestamp, rhs: Timestamp) -> Bool {
        lhs.timeIntervalSince1970 < rhs.timeIntervalSince1970
    }
}

extension Timestamp: Strideable {
    public func distance(to other: Timestamp) -> Int {
        return other.timeIntervalSince1970 - timeIntervalSince1970
    }
    
    public func advanced(by n: Int) -> Timestamp {
        return add(n)
    }
}

extension Timestamp {
    /// Parse range in format `yyymmdd-yymmdd`
    static func parseRange(yyyymmdd str: String) throws -> ClosedRange<Timestamp> {
        guard str.count == 17, str.contains("-") else {
            throw TimeError.InvalidDateFromat
        }
        let start = Timestamp(Int(str[0..<4])!, Int(str[4..<6])!, Int(str[6..<8])!)
        let end = Timestamp(Int(str[9..<13])!, Int(str[13..<15])!, Int(str[15..<17])!)
        return start...end
    }
}

extension Range where Bound == Timestamp {
    @inlinable public var durationSeconds: Int {
        upperBound.timeIntervalSince1970 - lowerBound.timeIntervalSince1970
    }
    
    @inlinable public func add(_ offset: Int) -> Range<Timestamp> {
        return lowerBound.add(offset) ..< upperBound.add(offset)
    }
    
    @inlinable public func divide(_ by: Int) -> Range<Int> {
        return lowerBound.timeIntervalSince1970 / by ..< upperBound.timeIntervalSince1970 / by
    }
    
    @inlinable public func stride(dtSeconds: Int) -> StrideTo<Timestamp> {
        return Swift.stride(from: lowerBound, to: upperBound, by: dtSeconds)
    }
    
    /// Form a timerange with dt seconds
    @inlinable public func range(dtSeconds: Int) -> TimerangeDt {
        TimerangeDt(start: lowerBound, to: upperBound, dtSeconds: dtSeconds)
    }
    
    /// Convert to a striable year month range
    @inlinable public func toYearMonth() -> Range<YearMonth> {
        lowerBound.toComponents().toYearMonth() ..< upperBound.toComponents().toYearMonth()
    }
}


/// Time with utc offset seconds
public struct TimerangeLocal {
    /// utc timestamp
    public let range: Range<Timestamp>
    
    /// seconds offset to get to local time
    public let utcOffsetSeconds: Int
}


public struct TimerangeDt: Hashable {
    public let range: Range<Timestamp>
    public let dtSeconds: Int
    
    @inlinable public var count: Int {
        return (range.upperBound.timeIntervalSince1970 - range.lowerBound.timeIntervalSince1970) / dtSeconds
    }
    
    public init(start: Timestamp, to: Timestamp, dtSeconds: Int) {
        self.range = start ..< to
        self.dtSeconds = dtSeconds
    }
    
    public init(range: Range<Timestamp>, dtSeconds: Int) {
        self.range = range
        self.dtSeconds = dtSeconds
    }
    
    public init(range: ClosedRange<Timestamp>, dtSeconds: Int) {
        self.range = range.lowerBound ..< range.upperBound.add(dtSeconds)
        self.dtSeconds = dtSeconds
    }
    
    public init(start: Timestamp, nTime: Int, dtSeconds: Int) {
        self.range = start ..< start.add(nTime * dtSeconds)
        self.dtSeconds = dtSeconds
    }
    
    /// devide time by dtSeconds
    @inlinable public func toIndexTime() -> Range<Int> {
        return range.lowerBound.timeIntervalSince1970 / dtSeconds ..< range.upperBound.timeIntervalSince1970 / dtSeconds
    }
    
    @inlinable public func index(of: Timestamp) -> Int? {
        let index = (of.timeIntervalSince1970 - range.lowerBound.timeIntervalSince1970) / dtSeconds
        return index < 0 || index >= count ? nil : index
    }
    
    @inlinable public func add(_ seconds: Int) -> TimerangeDt {
        return range.add(seconds).range(dtSeconds: dtSeconds)
    }
    
    func with(dtSeconds: Int) -> TimerangeDt {
        return TimerangeDt(range: range, dtSeconds: dtSeconds)
    }
    
    /// Format to a nice string like `2022-06-30 to 2022-07-13`
    func prettyString() -> String {
        /// Closed range end
        let end = range.upperBound.add(-1 * dtSeconds)
        if dtSeconds == 86400 {
            return "\(range.lowerBound.iso8601_YYYY_MM_dd) to \(end.iso8601_YYYY_MM_dd)"
        }
        if dtSeconds == 3600 {
            return "\(range.lowerBound.iso8601_YYYY_MM_dd_HH_mm) to \(end.iso8601_YYYY_MM_dd_HH_mm) (1-hourly)"
        }
        if dtSeconds == 3 * 3600 {
            return "\(range.lowerBound.iso8601_YYYY_MM_dd_HH_mm) to \(end.iso8601_YYYY_MM_dd_HH_mm) (3-hourly)"
        }
        return "\(range.lowerBound.iso8601_YYYY_MM_dd_HH_mm) to \(end.iso8601_YYYY_MM_dd_HH_mm) (dt=\(dtSeconds))"
    }
    
    /// Convert to a striable year month range
    @inlinable public func toYearMonth() -> Range<YearMonth> {
        return range.toYearMonth()
    }
}

extension TimerangeDt: Sequence {
    public func makeIterator() -> StrideToIterator<Timestamp> {
        range.stride(dtSeconds: dtSeconds).makeIterator()
    }
}


public extension Sequence where Element == Timestamp {
    /// With format `yyyy-MM-dd'T'HH:mm'`
    var iso8601_YYYYMMddHHmm: [String] {
        var time = 0
        var t = tm()
        var dateCalculated = Int.min
        return map {
            // only do date calculation if the actual date changes
            if dateCalculated != $0.timeIntervalSince1970 - $0.timeIntervalSince1970.moduloPositive(86400)  {
                time = $0.timeIntervalSince1970
                dateCalculated = $0.timeIntervalSince1970 - $0.timeIntervalSince1970.moduloPositive(86400)
                gmtime_r(&time, &t)
            }
            let year = Int(t.tm_year+1900)
            let month = Int(t.tm_mon+1)
            let day = Int(t.tm_mday)
            
            let hour = $0.hour
            let minute = $0.minute
            return "\(year)-\(month.zeroPadded(len: 2))-\(day.zeroPadded(len: 2))T\(hour.zeroPadded(len: 2)):\(minute.zeroPadded(len: 2))"
        }
    }
    
    /// With format `yyyy-MM-dd`
    var iso8601_YYYYMMdd: [String] {
        var time = 0
        var t = tm()
        return map {
            time = $0.timeIntervalSince1970
            gmtime_r(&time, &t)
            let year = Int(t.tm_year+1900)
            let month = Int(t.tm_mon+1)
            let day = Int(t.tm_mday)
            return "\(year)-\(month.zeroPadded(len: 2))-\(day.zeroPadded(len: 2))"
        }
    }
}