Spaces:
Sleeping
Sleeping
| 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))" | |
| } | |
| } | |
| } | |