我如何在Swift中解析/创建一个以分数秒UTC时区(ISO 8601, RFC 3339)格式化的日期时间戳?

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

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

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

格式:

  • 年、月、日,如“xxxx - xx - xx”;
  • 字母"作为分隔符
  • 时,分,秒,毫秒,用“XX:XX:XX. xxx”表示。
  • 字母“Z"作为零偏移的区域指示符,也就是UTC, GMT, Zulu时间。

最好的情况:

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

我已经搜索了StackOverflow,谷歌,Apple等,还没有找到一个Swift的答案。

最有希望的类是NSDateNSDateFormatterNSTimeZone

相关Q&A: 如何在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))
167621 次浏览

记住将区域设置为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))

问题是,如果你在使用非公历的设备上,年份将不符合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的呈现,请参见这个答案的以前版本

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协议

如果您在使用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"]

enter image description here

在我的情况下,我必须将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)


}

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

在未来的格式可能需要改变,这可能是一个小头痛有日期。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
}
}


}

如果你想将ISO8601DateFormatter()与来自Rails 4+ JSON提要的日期一起使用(当然不需要millis),你需要在格式化器上设置一些选项以使其正常工作,否则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
}()
}

以下是使用选项和不使用游乐场截图的结果:

enter image description here

为了进一步赞扬Andrés托雷斯Marroquín和利奥·达布斯,我有一个保留小数秒的版本。我在任何地方都找不到它的文档,但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
}
}

有一个新的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)
}
}
}

在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
}
}()
}

没有一些手动字符串掩码或时间格式化器

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

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

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

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

callsite

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

现在是2022年,但我正在寻找这个问题的答案(即如何将日期转换为ISO8601,其中包括秒的分数)。现在的答案只有一句话:

var somedate: Date = Date.now
var isodate = somedate.ISO8601Format(Date.ISO8601FormatStyle(includingFractionalSeconds: true))

所以这将打印类似2022-08-16T17:45:08.548Z的东西