如何创建日期时间戳和格式为ISO 8601,RFC 3339,UTC时区?


186

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

目标是一个看起来像这样的字符串:

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

格式:

  • 年,月,日,如“ XXXX-XX-XX”
  • 字母“ T”作为分隔符
  • 小时,分钟,秒,毫秒,如“ XX:XX:XX.XXX”。
  • 字母“ Z”作为零偏移(又称为UTC,GMT,Zulu时间)的区域标记。

最佳情况:

  • 快速,简短,直接的Swift源代码。
  • 无需使用任何其他框架,子项目,cocoapod,C代码等。

我已经搜索了StackOverflow,Google,Apple等,但没有找到Swift的答案。

似乎最有前途的类NSDateNSDateFormatterNSTimeZone

相关问答:如何在iOS中获取ISO 8601日期?

到目前为止,这是我想出的最好的方法:

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

5
请注意,iOS10 +简单包含ISO 8601内置 ..它将自动为您完成。
Fattie

2
@Fattie并且-它如何处理时间戳的最后一个.234Z毫秒Zulu / UTC部分?答案:马特·
朗斯(Mat

1
@ smat88dd-很棒的提示,谢谢 我不知道那里有“格式化程序的选项”,很奇怪又很疯狂!
Fattie

我正在寻找一种适用于Linux的解决方案。
neoneye

@neoneye只需使用旧版本(普通的DateFormatter)并将日历iso8601更改为gregorian stackoverflow.com/a/28016692/2303865
Leo Dabus,

Answers:


392

Swift 4•iOS 11.2.1或更高版本

extension ISO8601DateFormatter {
    convenience init(_ formatOptions: Options, timeZone: TimeZone = TimeZone(secondsFromGMT: 0)!) {
        self.init()
        self.formatOptions = formatOptions
        self.timeZone = timeZone
    }
}

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"]

在此处输入图片说明


3
添加相反的转化扩展很有用:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive,2016年

1
请注意,这会降低精度,因此确保通过生成的字符串而不是timeInterval比较日期的相等性很重要。 let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision '16

7
不用说,如果您不需要毫秒,则新的iOS 10可以ISO8601DateFormatter简化此过程。我已向Apple发布了一个错误报告(27242248),要求他们扩展此新的格式化程序,以提供指定毫秒的功能(因为如果没有毫秒,此新格式化程序对我们中的大多数人来说就不会使用)。
罗布

1
RFC3339中,我们可以找到 注释:“注意:ISO 8601定义了以“ T”分隔的日期和时间。出于可读性考虑,使用此语法的应用程序可以选择指定以(例如)分隔的完整日期和完整时间空格字符。” 它涵盖没有很好的日期格式T如:2016-09-21 21:05:10+00:00
manRo 2016年

3
@LeoDabus是的,但这是“ Swift iso8601”的第一个结果。我的评论旨在警告将来遇到此问题的其他开发人员,而不是针对OP。
thislooksfun

38

切记en_US_POSIX按照技术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))

问题是,如果您使用的设备使用非格里高利历,则除非您指定and locale以及timeZoneand dateFormat字符串,否则年份将不符合RFC3339 / ISO8601 。

或者,您可以使用它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复制,请参阅此答案的先前版本


为什么我们应该将语言环境设置为en_US_POSIX?即使我们不在美国?
mnemonic17年

2
嗯,您需要一些一致的语言环境,并且ISO 8601 / RFC 3999标准的约定是的格式en_US_POSIX。这是用于在网络上交换日期的通用语言。而且,如果保存日期字符串时在设备上使用了一个日历,而稍后又读回该字符串时,则在设备上使用了一个日历,就不会误解日期。另外,您需要一种保证永不更改的格式(这就是为什么要使用en_US_POSIXnot en_US)的原因。有关更多信息,请参见技术问答1480或那些RFC / ISO标准。
罗布

24

如果要使用ISO8601DateFormatter()带有Rails 4+ JSON提要中的日期的日期(当然不需要毫秒),则需要在格式化程序上设置一些选项,以使其正常工作,否则该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
    }()
}

这是在游乐场屏幕截图中未使用选项诗句的结果:

在此处输入图片说明


您可能还需要在选项中包含,.withFractionalSeconds但是我已经尝试过了,并且一直在抛出错误libc++abi.dylib: terminating with uncaught exception of type NSException
Leo Dabus

@MEnnabah在Swift 4中对我来说效果很好。您遇到错误了吗?
马特·朗

@LeoDabus,遇到与您相同的错误,您解决了吗?
Freeman

自定义JSONDecoder DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo Dabus

@freeman如果您想保留所有小数秒的日期,我建议在将日期保存/接收到服务器时使用双精度(自参考日期以来的时间间隔)。并.deferredToDate在使用可编码协议时使用默认的日期解码策略
Leo Dabus

6

斯威夫特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

为了进一步赞扬AndrésTorresMarroquí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
    }
}

在我看来,这是最好的答案,因为它可以使精度达到微秒级,而所有其他解决方案都将在几毫秒内截断。
Michael A. McCloskey

如果要保留日期的所有小数秒,则在将日期保存/接收到服务器时,应仅使用双精度(自参考日期以来的时间间隔)。
Leo Dabus

@LeoDabus是的,如果您控制整个系统并且不需要进行互操作。就像我在答案中说的那样,对于大多数用户而言,这不是必需的。但是我们并不总是可以控制Web API中的数据格式,而且由于Android和Python(至少)保留6位小数精度,所以有时有必要遵循。
伊莱·伯克

4

用途ISO8601DateFormatter上iOS10或更高版本。

用途DateFormatter上iOS9或以上。

斯威夫特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
        }
    }()
}

3

就我而言,我必须将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)

            }

3

将来可能需要更改格式,这可能是一个小麻烦,需要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
        }
    }

}

3

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

斯威夫特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)
        }
    }
}

1

为了补充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
    }
}

0

没有一些手动的字符串掩码或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将不包括毫秒。
Stefan Arentz

0

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

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())")
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.