1

I am looking for a Swifty way to generate a timestamp.

My macOS app logs some data and stamps it with the time the data was created. The data will then be sent across the network (as Data) to be reconstructed on an iPad.

Is there any Swift class that will work to generate the timestamp? NSDate? NSTimeIntervalSince1970? CFAbsoluteTimeGetCurrent()

The requirements are:

  1. Store the timestamp in as few bytes as possible (pref. Int)
  2. Have some semblance to real Earth time (I'd rather not generate my own time format)
  3. Millisecond accuracy
  4. Fast to construct
  5. iOS 9+, macOS 10.10+
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
MH175
  • 2,234
  • 1
  • 19
  • 35
  • You will need at least 8 bytes and you should save it as a Double. – Leo Dabus Nov 27 '17 at 01:00
  • 1
    if you really must save space then you can do this. I personally hate it whenever anyone gives me unix time as 64bit seconds since 1970, because thats not a real date; it has no time zone info for instance and nobody knows what the hell it is when you are looking at the logs trying to debug it. I prefer ISO8601 for interchange and swift now has ISO8601DateFormatter. You can store it internally howwver you want, but I always prefer string dates over numbers for the API interchange. – Josh Homann Nov 27 '17 at 01:08
  • 1
    @JoshHomann But seconds since 1970 is a standard way to represent a date. And you don't need a timezone since `Date` has no timezone. Sending and receiving seconds since 1970 is vastly more compact and efficient than creating and parsing an iOS8601 string. It's up to the OP to decide if that efficiency is important or not. – rmaddy Nov 27 '17 at 01:13

3 Answers3

3

You can send your Date converting it to Data (8-bytes floating point) and back to Date as follow:

extension Numeric {
    var data: Data {
        var source = self
        return .init(bytes: &source, count: MemoryLayout<Self>.size)
    }
    init<D: DataProtocol>(_ data: D) {
        var value: Self = .zero
        let size = withUnsafeMutableBytes(of: &value, { data.copyBytes(to: $0)} )
        assert(size == MemoryLayout.size(ofValue: value))
        self = value
    }
}

extension UInt64 {
    var bitPattern: Double { .init(bitPattern: self) }
}

extension Date {
    var data: Data { timeIntervalSinceReferenceDate.bitPattern.littleEndian.data }
    init<D: DataProtocol>(data: D) {
        self.init(timeIntervalSinceReferenceDate: data.timeIntervalSinceReferenceDate)
    }
}

extension DataProtocol {
    func value<N: Numeric>() -> N { .init(self) }
    var uint64: UInt64 { value() }
    var timeIntervalSinceReferenceDate: TimeInterval { uint64.littleEndian.bitPattern }
    var date: Date { .init(data: self) }
}

Playground Testing

let date = Date()            // "Nov 15, 2019 at 12:13 PM"
let data = date.data         // 8 bytes
print(Array(data))           // "[25, 232, 158, 22, 124, 191, 193, 65]\n"
let loadedDate = data.date   // "Nov 15, 2019 at 12:13 PM"
print(date == loadedDate)    // "true"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Wow this just made me realize something about Playgrounds. I tried executing it in a loop 100_000 times and if the loop counter % 1000 == 0 I print the time. I can see the playground execution loop counter going up and triggering the print statement every 1 second, but the time increment is going up by 3 milliseconds. Obviously this wouldn't happen outside of a playground. – MH175 Nov 27 '17 at 01:23
  • 1
    Besides the performance what you should consider is that it can't get more precise than that. You are sending the raw data, converting it back to date and preserving all its information. Note that the conversion to an array of bytes it is not needed. – Leo Dabus Nov 27 '17 at 01:28
  • This seems potentially fragile to me. You are relying on the fact that the internal representation is a *value,* and *identical* on source and destination system. It happens to work because the date is stored in a `fileprivate var _time: TimeInterval`. But that is not part of the public API, and might change with future OS or Swift versions. – Martin R Nov 27 '17 at 06:53
  • @MartinR I can use timeIntervalSinceReferenceDate to make sure it doesn't depend on that – Leo Dabus Nov 27 '17 at 06:57
  • 6 methods in 4 extensions for the 2 conversions Date<->Data – a bit over-engineered for my (personal!) taste :) – Martin R Nov 27 '17 at 12:28
  • 1
    (replying to your request for feedback) Note that `dateFromTimeIntervalSinceReferenceDate` on `Data` doesn't need to be optional. A couple of more minor things: you're relying on the endianness of sending and receiving systems being the same; which almost certainly isn't an issue for OP, but is nonetheless worth bearing in mind. To be independent of endianness, you'd want to get the `bitPattern` of the `Double`, and then e.g make sure you're getting the bytes of the `.littleEndian` representation, reconstructing on the other side with `UInt64(littleEndian:)`... – Hamish Nov 27 '17 at 13:52
  • 1
    ...Note you're also relying on the bit representation of `Double`, which *could* technically change up until Swift is ABI stable, but seems pretty unlikely and is almost certainly a non-issue. Finally, I'm not so mad on the use of lots of computed properties here, IMO `dateFromTimeIntervalSinceReferenceDate` on `Data` should be an initialiser on `Date`, and `dateFromTimeIntervalSinceReferenceDate` on `Double` seems a little redundant. – Hamish Nov 27 '17 at 13:52
  • @Hamish Thanks for your feedback ! Answer updated to reflect your comments. Can you show me how I could make it independent of endianness? – Leo Dabus Nov 27 '17 at 14:06
  • 2
    @LeoDabus No worries :) Here's an example: https://gist.github.com/hamishknight/1001a844846776fd08f64efc71cd1aba – Hamish Nov 27 '17 at 14:52
  • @Hamish follow up https://gist.github.com/leodabus/6141a0528cf60f42092ad8ad664ff063 – Leo Dabus Nov 15 '19 at 15:39
1

Here is how I used Leo Dabus's answer.

public struct Timestamp: Equatable, Comparable {

    public let date: Date

    public init() {
        self.date = Date()
    }

    public func toData() -> Data {
        var date = self.date
        return Data(bytes: &date, count: MemoryLayout<Date>.size)
    }

    public init(fromData data: Data) {
        guard data.count == 8 else {
            fatalError("Insufficient bytes. expected 8; got \(data.count). data: \(data)")
        }
        self.date = data.withUnsafeBytes { $0.pointee }
    }

    public static func ==(lhs: Timestamp, rhs: Timestamp) -> Bool {
        return lhs.date == rhs.date
    }

    public static func <(lhs: Timestamp, rhs: Timestamp) -> Bool {
        return lhs.date < rhs.date
    }
}
MH175
  • 2,234
  • 1
  • 19
  • 35
  • In case anyone is interested, I ran a performance test and it takes 0.014s to construct 100_000 of these. Release build. iPad Air 2. – MH175 Nov 27 '17 at 04:57
  • 1
    Why not simply `Int64(date.timeIntervalSince1970 * 1000)`? – Martin R Nov 27 '17 at 06:32
0

Use TimeIntervalSince1970 type:

let exactTimeInMilliseconds = Date().timeIntervalSince1970

Original answer was very inefficient.

Xcoder
  • 1,433
  • 3
  • 17
  • 37
  • Reason for downvoting? – Xcoder Nov 27 '17 at 01:00
  • Not my downvote but you dont need to convert the Date to String. All OP needs is to send 8 bytes (Double) to send the Date in its original format. – Leo Dabus Nov 27 '17 at 01:08
  • Not mine either. CFAbsoluteTimeGetCurrent() looks promising. – MH175 Nov 27 '17 at 01:08
  • Not to mention that the code in this answer doesn't compile nor does it make use of the `date` variable. And it appears to needlessly (though incorrectly) convert a `Date` to a `String` and back to a `Date`. The goal is efficiency, not coming up with the least efficient way to get the time interval. – rmaddy Nov 27 '17 at 01:15
  • By original answer, I mean **my** original answer(the post has been edited). OP can remove the rounded() if they wanted to. – Xcoder Nov 27 '17 at 15:55
  • It's good because OP only needs to use one line of code, and I think it does address all of OP's points listed. – Xcoder Nov 27 '17 at 21:44
  • @Xcoder don't use `TimeIntervalSince1970`. If you need to compare it later there will be inconsistency (it will return false in some cases even if should return true). You should use `timeIntervalSinceReferenceDate`. From NSDate documentation **NSDate objects encapsulate a single point in time, independent of any particular calendrical system or time zone. Date objects are immutable, representing an invariant time interval relative to an absolute reference date (00:00:00 UTC on 1 January 2001).** – Leo Dabus Jan 22 '20 at 20:00