How to configure DateFormatter to capture microseconds

后端 未结 4 1919
暗喜
暗喜 2020-12-10 16:03

iOS Date() returns date with at least microsecond precision.
I checked this statement by calling Date().timeIntervalSince1970 which results in

相关标签:
4条回答
  • 2020-12-10 16:26

    The resolution of (NS)DateFormatter is limited to milliseconds, compare NSDateFormatter milliseconds bug. A possible solution is to retrieve all date components (up to nanoseconds) as numbers and do a custom string formatting. The date formatter can still be used for the timezone string.

    Example:

    let date = Date(timeIntervalSince1970: 1490891661.074981)
    
    let formatter = DateFormatter()
    formatter.dateFormat = "ZZZZZ"
    let tzString = formatter.string(from: date)
    
    let cal = Calendar.current
    let comps = cal.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond],
                                   from: date)
    let microSeconds = lrint(Double(comps.nanosecond!)/1000) // Divide by 1000 and round
    
    let formatted = String(format: "%04ld-%02ld-%02ldT%02ld:%02ld:%02ld.%06ld",
                           comps.year!, comps.month!, comps.day!,
                           comps.hour!, comps.minute!, comps.second!,
                           microSeconds) + tzString
    
    print(formatted) // 2017-03-30T18:34:21.074981+02:00
    
    0 讨论(0)
  • 2020-12-10 16:32

    It is a bit of a hack, but not that complex and 100% Swift:

    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.'MICROS'xx"
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    
    // Get the number of microseconds with a precision of 6 digits
    let now = Date()
    let dateParts = Calendar.current.dateComponents([.nanosecond], from: now)
    let microSeconds = Int((Double(dateParts.nanosecond!) / 1000).rounded(.toNearestOrEven))
    let microSecPart = String(microSeconds).padding(toLength: 6, withPad: "0", startingAt: 0)
    
    // Format the date and add in the microseconds
    var timestamp = dateFormatter.string(from: now)
    timestamp = timestamp.replacingOccurrences(of: "MICROS", with: microSecPart)
    
    0 讨论(0)
  • 2020-12-10 16:34

    Thanks to @MartinR for solving first half of my problem and to @ForestKunecke for giving me tips how to solve second half of the problem.

    Based on their help I created ready to use solution which converts date from string and vice versa with millisecond precision:

    public final class MicrosecondPrecisionDateFormatter: DateFormatter {
    
        private let microsecondsPrefix = "."
    
        override public init() {
            super.init()
            locale = Locale(identifier: "en_US_POSIX")
            timeZone = TimeZone(secondsFromGMT: 0)
        }
    
        required public init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override public func string(from date: Date) -> String {
            dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
            let components = calendar.dateComponents(Set([Calendar.Component.nanosecond]), from: date)
    
            let nanosecondsInMicrosecond = Double(1000)
            let microseconds = lrint(Double(components.nanosecond!) / nanosecondsInMicrosecond)
    
            // Subtract nanoseconds from date to ensure string(from: Date) doesn't attempt faulty rounding.
            let updatedDate = calendar.date(byAdding: .nanosecond, value: -(components.nanosecond!), to: date)!
            let dateTimeString = super.string(from: updatedDate)
    
            let string = String(format: "%@.%06ldZ",
                                dateTimeString,
                                microseconds)
    
            return string
        }
    
        override public func date(from string: String) -> Date? {
            dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
    
            guard let microsecondsPrefixRange = string.range(of: microsecondsPrefix) else { return nil }
            let microsecondsWithTimeZoneString = String(string.suffix(from: microsecondsPrefixRange.upperBound))
    
            let nonDigitsCharacterSet = CharacterSet.decimalDigits.inverted
            guard let timeZoneRangePrefixRange = microsecondsWithTimeZoneString.rangeOfCharacter(from: nonDigitsCharacterSet) else { return nil }
    
            let microsecondsString = String(microsecondsWithTimeZoneString.prefix(upTo: timeZoneRangePrefixRange.lowerBound))
            guard let microsecondsCount = Double(microsecondsString) else { return nil }
    
            let dateStringExludingMicroseconds = string
                .replacingOccurrences(of: microsecondsString, with: "")
                .replacingOccurrences(of: microsecondsPrefix, with: "")
    
            guard let date = super.date(from: dateStringExludingMicroseconds) else { return nil }
            let microsecondsInSecond = Double(1000000)
            let dateWithMicroseconds = date + microsecondsCount / microsecondsInSecond
    
            return dateWithMicroseconds
        }
    }
    

    Usage:

    let formatter = MicrosecondPrecisionDateFormatter()
    let date = Date(timeIntervalSince1970: 1490891661.074981)
    let formattedString = formatter.string(from: date) // 2017-03-30T16:34:21.074981Z
    
    0 讨论(0)
  • 2020-12-10 16:34

    Solution by @Vlad Papko has some issue:

    For dates like following:

    2019-02-01T00:01:54.3684Z

    it can make string with extra zero:

    2019-02-01T00:01:54.03684Z

    Here is fixed solution, it's ugly, but works without issues:

    override public func string(from date: Date) -> String {
            dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
            let components = calendar.dateComponents(Set([Calendar.Component.nanosecond]), from: date)
    
            let nanosecondsInMicrosecond = Double(1000)
            let microseconds = lrint(Double(components.nanosecond!) / nanosecondsInMicrosecond)
    
            // Subtract nanoseconds from date to ensure string(from: Date) doesn't attempt faulty rounding.
            let updatedDate = calendar.date(byAdding: .nanosecond, value: -(components.nanosecond!), to: date)!
            let dateTimeString = super.string(from: updatedDate)
    
            let stingWithMicroseconds = "\(date.timeIntervalSinceReferenceDate)"
            let dotIndex = stingWithMicroseconds.lastIndex(of: ".")!
            let hasZero = stingWithMicroseconds[stingWithMicroseconds.index(after: dotIndex)] == "0"
            let format = hasZero ? "%@.%06ldZ" : "%@.%6ldZ"
    
            let string = String(format: format,
                                dateTimeString,
                                microseconds)
    
            return string
        }
    
    0 讨论(0)
提交回复
热议问题