Nsdateformatter Milliseconds Bug

NSDateFormatter milliseconds bug

It seems that NSDateFormatter works only with millisecond resolution, for the
following reasons:

  • By setting a breakpoint in CFDateFormatterCreateDateFromString, one can
    see that this function is called from dateFromString::

    (lldb) bt
    * thread #1: tid = 0x26d03f, 0x018f47d0 CoreFoundation`CFDateFormatterCreateDateFromString, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x018f47d0 CoreFoundation`CFDateFormatterCreateDateFromString
    frame #1: 0x0116e0ea Foundation`getObjectValue + 248
    frame #2: 0x0116dfc7 Foundation`-[NSDateFormatter getObjectValue:forString:errorDescription:] + 206
    frame #3: 0x0116879f Foundation`-[NSDateFormatter dateFromString:] + 71
    * frame #4: 0x00002d56 foo`main(argc=1, argv=0xbfffee54) + 182 at main.mm:25
  • CFDateFormatterCreateDateFromString() is from
    CFDateFormatter.c
    which is open source. One can see that all calendrical calculations are made using the
    ICU Calendar Classes.

  • It is stated in calendar.h that Calendar uses UDate which has a millisecond resolution:

    /**
    * <code>Calendar</code> is an abstract base class for converting between
    * a <code>UDate</code> object and a set of integer fields such as
    * <code>YEAR</code>, <code>MONTH</code>, <code>DAY</code>, <code>HOUR</code>,
    * and so on. (A <code>UDate</code> object represents a specific instant in
    * time with millisecond precision. See UDate
    * for information about the <code>UDate</code> class.)
    * ...

How to configure DateFormatter to capture microseconds

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 microsecond 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

Format string for NSDateFormatter to produce milliseconds

Try to use 'SS' specifier (with number of S's equal to number of digits you want to get - 3 in your case), e.g.

[formatter setDateFormat:@"yyyy/MM/dd hh:mm:ss:SSS"];

Swift DateFormatter Optional Milliseconds

Two suggestions:

  • Convert the string with the date format including the milliseconds. If it returns nil convert it with the other format.

  • Strip the milliseconds from the string with Regular Expression:

    var dateString = "2018-01-21T20:11:20.057Z"
    dateString = dateString.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
    // -> 2018-01-21T20:11:20Z

Edit:

To use it with Codable you have to write a custom initializer, specifying dateDecodingStrategy does not work

struct Foo: Decodable {
let birthDate : Date
let name : String

private enum CodingKeys : String, CodingKey { case born, name }

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var rawDate = try container.decode(String.self, forKey: .born)
rawDate = rawDate.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
birthDate = ISO8601DateFormatter().date(from: rawDate)!
name = try container.decode(String.self, forKey: .name)
}
}

let jsonString = """
[{"name": "Bob", "born": "2018-01-21T20:11:20.057Z"}, {"name": "Matt", "born": "2018-01-21T20:11:20Z"}]
"""

do {
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode([Foo].self, from: data)
print(result)
} catch {
print("error: ", error)
}

Swift full date with milliseconds

Updated for Swift 3

let d = Date()
let df = DateFormatter()
df.dateFormat = "y-MM-dd H:mm:ss.SSSS"

df.string(from: d) // -> "2016-11-17 17:51:15.1720"

When you have a Date d, you can get the formatted string using a NSDateFormatter. You can also use a formatter to turn a string date based on your format into a Date

See this chart for more on what dateFormat can do http://waracle.net/iphone-nsdateformatter-date-formatting-table/

How to parse ISO 8601 using NSDateFormatter with optional milliseconds part

As far as I know there is no way to make optional parameters.

The usual solution is to use two formatters, one for each format.
To decide which formatter to use, you can either

  1. Count the number of characters in the date string (as suggested in Parsing a RFC 822 date with NSDateFormatter)

  2. Just try both formatters and get the first non-nil result.

Since your date formats are similar, you can go with only one formatter and if the date string is too short, append .000 before using the formatter.

Date to milliseconds and back to date in Swift

I don't understand why you're doing anything with strings...

extension Date {
var millisecondsSince1970:Int64 {
Int64((self.timeIntervalSince1970 * 1000.0).rounded())
}

init(milliseconds:Int64) {
self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
}
}


Date().millisecondsSince1970 // 1476889390939
Date(milliseconds: 0) // "Dec 31, 1969, 4:00 PM" (PDT variant of 1970 UTC)

NSDateFormatter and default locale cause bug

The problem solved by setting the formatter's locale to en_US_POSIX. It is necessary while parsing fixed-format date strings because the format of the date strings is constant unlike device' locale.

The code should be like

+(NSDate*)dateTimeFromJSONDateString:(NSString*)dateString{
static NSDateFormatter *_dateFormatter;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_dateFormatter = [[NSDateFormatter alloc] init];
_dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZ"];
});
if (string)
return [_dateFormatter dateFromString:string];
else
return nil;
}

Getting current time of day down to milliseconds error

You can use the code below:

let formatter = NSDateFormatter()
//formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0) // you can set GMT time
formatter.timeZone = NSTimeZone.localTimeZone() // or as local time
formatter.dateFormat = "HH:mm:ss.SSS"
println(formatter.stringFromDate(NSDate()))

Output: "05:57:26.123"



Related Topics



Leave a reply



Submit