如何在 Swift 中解析/创建格式为小数秒 UTC 时区(ISO 8601、RFC 3339)的日期时间戳?

Posted

技术标签:

【中文标题】如何在 Swift 中解析/创建格式为小数秒 UTC 时区(ISO 8601、RFC 3339)的日期时间戳?【英文标题】:How can I parse / create a date time stamp formatted with fractional seconds UTC timezone (ISO 8601, RFC 3339) in Swift? 【发布时间】:2015-01-19 00:58:31 【问题描述】:

如何使用ISO 8601 和RFC 3339 的格式标准生成日期时间戳

目标是一个如下所示的字符串:

"2015-01-01T00:00:00.000Z"

格式:

年、月、日,为“XXXX-XX-XX” 字母“T”作为分隔符 小时、分钟、秒、毫秒,如“XX:XX:XX.XXX”。 字母“Z”作为零时差的区域指示符,也称为 UTC、GMT、祖鲁时间。

最佳情况:

简单、简短、直接的 Swift 源代码。 无需使用任何额外的框架、子项目、cocoapod、C 代码等。

我搜索过 ***、Google、Apple 等,但没有找到 Swift 的答案。

看起来最有前途的类是NSDateNSDateFormatterNSTimeZone

相关问答:How do I get an ISO 8601 date on ios?

这是迄今为止我想出的最好的:

var now = NSDate()
var formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
println(formatter.stringFromDate(now))

【问题讨论】:

请注意,iOS10+ 仅包含 ISO 8601 BUILT-IN .. 它只会为您自动完成。 @Fattie And - 它如何处理时间戳的最后 .234Z 毫秒 Zulu/UTC 部分?答:马特朗斯@***.com/a/42101630/3078330 @smat88dd -- 很棒的提示,谢谢。我不知道有“格式化程序上的选项”,奇怪而狂野! 我正在寻找适用于 linux 的解决方案。 @neoneye 使用旧版本(纯DateFormatter)并将日历iso8601更改为gregorian ***.com/a/28016692/2303865 【参考方案1】:

Swift 4 • iOS 11.2.1 或更高版本

extension ISO8601DateFormatter 
    convenience init(_ formatOptions: Options) 
        self.init()
        self.formatOptions = formatOptions
    


extension Formatter 
    static let iso8601withFractionalSeconds = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])


extension Date 
    var iso8601withFractionalSeconds: String  return Formatter.iso8601withFractionalSeconds.string(from: self) 


extension String 
    var iso8601withFractionalSeconds: Date?  return Formatter.iso8601withFractionalSeconds.date(from: self) 


用法:

Date().description(with: .current)  //  Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
let dateString = Date().iso8601withFractionalSeconds   //  "2019-02-06T00:35:01.746Z"

if let date = dateString.iso8601withFractionalSeconds 
    date.description(with: .current) // "Tuesday, February 5, 2019 at 10:35:01 PM Brasilia Summer Time"
    print(date.iso8601withFractionalSeconds)           //  "2019-02-06T00:35:01.746Z\n"


iOS 9 • Swift 3 或更高版本

extension Formatter 
    static let iso8601withFractionalSeconds: DateFormatter = 
        let formatter = DateFormatter()
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
        return formatter
    ()


可编码协议

如果您在使用 Codable 时需要对这种格式进行编码和解码 协议,您可以创建自己的自定义日期编码/解码策略:

extension JSONDecoder.DateDecodingStrategy 
    static let iso8601withFractionalSeconds = custom 
        let container = try $0.singleValueContainer()
        let string = try container.decode(String.self)
        guard let date = Formatter.iso8601withFractionalSeconds.date(from: string) else 
            throw DecodingError.dataCorruptedError(in: container,
                  debugDescription: "Invalid date: " + string)
        
        return date
    

以及编码策略

extension JSONEncoder.DateEncodingStrategy 
    static let iso8601withFractionalSeconds = custom 
        var container = $1.singleValueContainer()
        try container.encode(Formatter.iso8601withFractionalSeconds.string(from: $0))
    


游乐场测试

let dates = [Date()]   // ["Feb 8, 2019 at 9:48 PM"]

编码

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601withFractionalSeconds
let data = try! encoder.encode(dates)
print(String(data: data, encoding: .utf8)!)

解码

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
let decodedDates = try! decoder.decode([Date].self, from: data)  // ["Feb 8, 2019 at 9:48 PM"]

【讨论】:

添加反向转换扩展会很有用:extension String var dateFormattedISO8601: NSDate? return NSDate.Date.formatterISO8601.dateFromString(self) 请注意,这会降低一些精度,因此确保通过生成的字符串而不是 timeInterval 比较日期的相等性很重要。 let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970) 在RFC3339 中我们可以找到一个注释"注意:ISO 8601 定义日期和时间用“T”分隔。为了便于阅读,使用这种语法的应用程序可以选择指定由(例如)空格字符分隔的全日制和全日制。” 它是否也包括没有T 的日期格式,例如:2016-09-21 21:05:10+00:00 这在 LINUX 上不起作用。如果您也以 Linux 为目标,您需要删除 Calendar(identifier: .iso8601) 行,否则它出现段错误并崩溃。 @LeoDabus 是的,但这是“Swift iso8601”的第一个结果。我的评论是为了警告将来遇到此问题的其他开发人员,而不是针对 OP。【参考方案2】:

请记住将语言环境设置为en_US_POSIX,如Technical Q&A1480 中所述。在 Swift 3 中:

let date = Date()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
print(formatter.string(from: date))

问题在于,如果您在使用非公历的设备上,年份将不符合 RFC3339/ISO8601,除非您指定 locale 以及 timeZonedateFormat字符串。

或者您可以使用ISO8601DateFormatter 让您摆脱自己设置localetimeZone 的麻烦:

let date = Date()
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds)  // this is only available effective iOS 11 and macOS 10.13
print(formatter.string(from: date))

对于 Swift 2 版本,请参阅 previous revision of this answer。

【讨论】:

为什么我们应该将语言环境设置为 en_US_POSIX ?即使我们不在美国? 嗯,您需要一些一致的语言环境,并且 ISO 8601/RFC 3999 标准的约定是 en_US_POSIX 提供的格式。这是在网络上交换日期的通用语。如果在保存日期字符串时在设备上使用了一个日历,而在稍后读回字符串时使用了另一个日历,则您不能误解日期。此外,您需要一种保证永远不会改变的格式(这就是您使用en_US_POSIX 而不是en_US 的原因)。有关详细信息,请参阅 Technical Q&A 1480 或那些 RFC/ISO 标准。【参考方案3】:

如果您想使用带有来自 Rails 4+ JSON 提要的日期的ISO8601DateFormatter()(当然不需要毫秒),您需要在格式化程序上设置一些选项以使其正常工作,否则date(from: string) 函数将返回 nil。这是我正在使用的:

extension Date 
    init(dateString:String) 
        self = Date.iso8601Formatter.date(from: dateString)!
    

    static let iso8601Formatter: ISO8601DateFormatter = 
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withFullDate,
                                          .withTime,
                                          .withDashSeparatorInDate,
                                          .withColonSeparatorInTime]
        return formatter
    ()

以下是在 Playground 屏幕截图中使用选项与不使用选项的结果:

【讨论】:

您还需要在选项中包含.withFractionalSeconds,但我已经尝试过了,它一直抛出错误libc++abi.dylib: terminating with uncaught exception of type NSException @MEnnabah 它在 Swift 4 中对我来说很好用。你遇到错误了吗? @LeoDabus,遇到和你一样的错误,你解决了吗? 自定义 JSONDecoder DateDecodingStrategy ***.com/a/46458771/2303865 @freeman 如果您想保留日期及其所有小数秒,我建议在将日期保存/接收到服务器时使用双精度(自参考日期起的时间间隔)。并且在使用 Codable 协议时使用默认的日期解码策略.deferredToDate【参考方案4】:

斯威夫特 5

如果您的目标是 iOS 11.0+ / macOS 10.13+,您只需将 ISO8601DateFormatterwithInternetDateTimewithFractionalSeconds 选项一起使用,如下所示:

let date = Date()

let iso8601DateFormatter = ISO8601DateFormatter()
iso8601DateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let string = iso8601DateFormatter.string(from: date)

// string looks like "2020-03-04T21:39:02.112Z"

【讨论】:

【参考方案5】:

在 iOS10 或更高版本上使用 ISO8601DateFormatter

在 iOS9 或更早版本上使用 DateFormatter

斯威夫特 4

protocol DateFormatterProtocol 
    func string(from date: Date) -> String
    func date(from string: String) -> Date?


extension DateFormatter: DateFormatterProtocol 

@available(iOS 10.0, *)
extension ISO8601DateFormatter: DateFormatterProtocol 

struct DateFormatterShared 
    static let iso8601: DateFormatterProtocol = 
        if #available(iOS 10, *) 
            return ISO8601DateFormatter()
         else 
            // iOS 9
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(secondsFromGMT: 0)
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
            return formatter
        
    ()

【讨论】:

【参考方案6】:

为了进一步赞美 Andrés Torres Marroquín 和 Leo Dabus,我有一个保留小数秒的版本。我在任何地方都找不到它的记录,但 Apple 在输入和输出上都将小数秒截断为微秒(3 位精度)(即使使用 SSSSSSS 指定,与 Unicode tr35-31 相反)。

我应该强调这对于大多数用例来说可能不是必需的。在线日期通常不需要毫秒精度,如果需要,最好使用不同的数据格式。但有时必须以特定方式与预先存在的系统进行互操作。

Xcode 8/9 和 Swift 3.0-3.2

extension Date 
    struct Formatter 
        static let iso8601: DateFormatter = 
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(identifier: "UTC")
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"
            return formatter
        ()
    

    var iso8601: String 
        // create base Date format 
         var formatted = DateFormatter.iso8601.string(from: self)

        // Apple returns millisecond precision. find the range of the decimal portion
         if let fractionStart = formatted.range(of: "."),
             let fractionEnd = formatted.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: formatted.endIndex) 
             let fractionRange = fractionStart.lowerBound..<fractionEnd
            // replace the decimal range with our own 6 digit fraction output
             let microseconds = self.timeIntervalSince1970 - floor(self.timeIntervalSince1970)
             var microsecondsStr = String(format: "%.06f", microseconds)
             microsecondsStr.remove(at: microsecondsStr.startIndex)
             formatted.replaceSubrange(fractionRange, with: microsecondsStr)
        
         return formatted
    


extension String 
    var dateFromISO8601: Date? 
        guard let parsedDate = Date.Formatter.iso8601.date(from: self) else 
            return nil
        

        var preliminaryDate = Date(timeIntervalSinceReferenceDate: floor(parsedDate.timeIntervalSinceReferenceDate))

        if let fractionStart = self.range(of: "."),
            let fractionEnd = self.index(fractionStart.lowerBound, offsetBy: 7, limitedBy: self.endIndex) 
            let fractionRange = fractionStart.lowerBound..<fractionEnd
            let fractionStr = self.substring(with: fractionRange)

            if var fraction = Double(fractionStr) 
                fraction = Double(floor(1000000*fraction)/1000000)
                preliminaryDate.addTimeInterval(fraction)
            
        
        return preliminaryDate
    

【讨论】:

在我看来这是最好的答案,因为它允许人们达到微秒级的精度,而所有其他解决方案都以毫秒为单位截断。 如果您想保留日期及其所有小数秒,则在将日期保存/接收到服务器时应仅使用双精度(自参考日期起的时间间隔)。 @LeoDabus 是的,如果您控制整个系统并且不需要互操作。就像我在答案中所说,这对大多数用户来说不是必需的。但我们并不总是能够控制 Web API 中的数据格式,并且由于 android 和 Python(至少)保留 6 位小数精度,因此有时需要效仿。【参考方案7】:

在我的情况下,我必须将 DynamoDB - lastUpdated 列(Unix 时间戳)转换为正常时间。

lastUpdated 的初始值为:1460650607601 - 通过以下方式转换为 2016-04-14 16:16:47 +0000:

   if let lastUpdated : String = userObject.lastUpdated 

                let epocTime = NSTimeInterval(lastUpdated)! / 1000 // convert it from milliseconds dividing it by 1000

                let unixTimestamp = NSDate(timeIntervalSince1970: epocTime) //convert unix timestamp to Date
                let dateFormatter = NSDateFormatter()
                dateFormatter.timeZone = NSTimeZone()
                dateFormatter.locale = NSLocale.currentLocale() // NSLocale(localeIdentifier: "en_US_POSIX")
                dateFormatter.dateFormat =  "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
                dateFormatter.dateFromString(String(unixTimestamp))

                let updatedTimeStamp = unixTimestamp
                print(updatedTimeStamp)

            

【讨论】:

【参考方案8】:

将来可能需要更改格式,如果在应用程序中到处调用 date.dateFromISO8601 可能会让人头疼。使用类和协议来包装实现,在一个地方更改日期时间格式调用会更简单。如果可能,请使用 RFC3339,它是一个更完整的表示。 DateFormatProtocol 和 DateFormat 非常适合依赖注入。

class AppDelegate: UIResponder, UIApplicationDelegate 

    internal static let rfc3339DateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
    internal static let localeEnUsPosix = "en_US_POSIX"


import Foundation

protocol DateFormatProtocol 

    func format(date: NSDate) -> String
    func parse(date: String) -> NSDate?




import Foundation

class DateFormat:  DateFormatProtocol 

    func format(date: NSDate) -> String 
        return date.rfc3339
    

    func parse(date: String) -> NSDate? 
        return date.rfc3339
    




extension NSDate 

    struct Formatter 
        static let rfc3339: NSDateFormatter = 
            let formatter = NSDateFormatter()
            formatter.calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierISO8601)
            formatter.locale = NSLocale(localeIdentifier: AppDelegate.localeEnUsPosix)
            formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
            formatter.dateFormat = rfc3339DateFormat
            return formatter
        ()
    

    var rfc3339: String  return Formatter.rfc3339.stringFromDate(self) 


extension String 
    var rfc3339: NSDate? 
        return NSDate.Formatter.rfc3339.dateFromString(self)
    




class DependencyService: DependencyServiceProtocol 

    private var dateFormat: DateFormatProtocol?

    func setDateFormat(dateFormat: DateFormatProtocol) 
        self.dateFormat = dateFormat
    

    func getDateFormat() -> DateFormatProtocol 
        if let dateFormatObject = dateFormat 

            return dateFormatObject
         else 
            let dateFormatObject = DateFormat()
            dateFormat = dateFormatObject

            return dateFormatObject
        
    


【讨论】:

【参考方案9】:

有一个新的ISO8601DateFormatter 类可以让您创建一个只有一行的字符串。为了向后兼容,我使用了一个旧的 C 库。我希望这对某人有用。

Swift 3.0

extension Date 
    var iso8601: String 
        if #available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 
            return ISO8601DateFormatter.string(from: self, timeZone: TimeZone.current, formatOptions: .withInternetDateTime)
         else 
            var buffer = [CChar](repeating: 0, count: 25)
            var time = time_t(self.timeIntervalSince1970)
            strftime_l(&buffer, buffer.count, "%FT%T%z", localtime(&time), nil)
            return String(cString: buffer)
        
    

【讨论】:

【参考方案10】:

为了补充 Leo Dabus 的版本,我添加了对编写 Swift 和 Objective-C 的项目的支持,还添加了对可选毫秒的支持,这可能不是最好的,但你会明白的:

Xcode 8 和 Swift 3

extension Date 
    struct Formatter 
        static let iso8601: DateFormatter = 
            let formatter = DateFormatter()
            formatter.calendar = Calendar(identifier: .iso8601)
            formatter.locale = Locale(identifier: "en_US_POSIX")
            formatter.timeZone = TimeZone(secondsFromGMT: 0)
            formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
            return formatter
        ()
    

    var iso8601: String 
        return Formatter.iso8601.string(from: self)
    



extension String 
    var dateFromISO8601: Date? 
        var data = self
        if self.range(of: ".") == nil 
            // Case where the string doesn't contain the optional milliseconds
            data = data.replacingOccurrences(of: "Z", with: ".000000Z")
        
        return Date.Formatter.iso8601.date(from: data)
    



extension NSString 
    var dateFromISO8601: Date? 
        return (self as String).dateFromISO8601
    

【讨论】:

【参考方案11】:

没有一些手动的字符串掩码或 TimeFormatters

import Foundation

struct DateISO: Codable 
    var date: Date


extension Date
    var isoString: String 
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        guard let data = try? encoder.encode(DateISO(date: self)),
        let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as?  [String: String]
            else  return "" 
        return json?.first?.value ?? ""
    


let dateString = Date().isoString

【讨论】:

这是一个很好的答案,但使用.iso8601 将不包括毫秒。【参考方案12】:

基于对象范式中可接受的答案

class ISO8601Format

    let format: ISO8601DateFormatter

    init() 
        let format = ISO8601DateFormatter()
        format.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        format.timeZone = TimeZone(secondsFromGMT: 0)!
        self.format = format
    

    func date(from string: String) -> Date 
        guard let date = format.date(from: string) else  fatalError() 
        return date
    

    func string(from date: Date) -> String  return format.string(from: date) 



class ISO8601Time

    let date: Date
    let format = ISO8601Format() //FIXME: Duplication

    required init(date: Date)  self.date = date 

    convenience init(string: String) 
        let format = ISO8601Format() //FIXME: Duplication
        let date = format.date(from: string)
        self.init(date: date)
    

    func concise() -> String  return format.string(from: date) 

    func description() -> String  return date.description(with: .current) 

呼叫站点

let now = Date()
let time1 = ISO8601Time(date: now)
print("time1.concise(): \(time1.concise())")
print("time1: \(time1.description())")


let time2 = ISO8601Time(string: "2020-03-24T23:16:17.661Z")
print("time2.concise(): \(time2.concise())")
print("time2: \(time2.description())")

【讨论】:

以上是关于如何在 Swift 中解析/创建格式为小数秒 UTC 时区(ISO 8601、RFC 3339)的日期时间戳?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 中使用 Codable 转换带有可选小数秒的日期字符串?

如何创建一个 Django 自定义字段来存储 MYSQL DATETIME(6) 并在 Django/MySQL 中启用小数秒(毫秒或微秒)?

NSPredicate 'IN' 运算符在 swift 中的格式问题。无法解析格式字符串

如何将 DateTime 转换为本地化的小数秒字符串?

如何让 JSON 中的日期格式在 ruby 与 Swift 间保持一致

如何让 JSON 中的日期格式在 ruby 与 Swift 间保持一致