Làm cách nào để tạo dấu thời gian và định dạng ngày theo ISO 8601, RFC 3339, múi giờ UTC?


186

Làm cách nào để tạo dấu thời gian ngày, sử dụng các tiêu chuẩn định dạng cho ISO 8601RFC 3339 ?

Mục tiêu là một chuỗi trông như thế này:

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

Định dạng:

  • năm, tháng, ngày, là "XXXX-XX-XX"
  • chữ "T" làm dấu phân cách
  • giờ, phút, giây, mili giây, như "XX: XX: XX.XXX".
  • chữ "Z" với tư cách là người chỉ định vùng cho độ lệch bằng 0, còn gọi là UTC, GMT, giờ Zulu.

Trường hợp tốt nhất:

  • Mã nguồn Swift đơn giản, ngắn gọn và đơn giản.
  • Không cần sử dụng bất kỳ khung, tiểu dự án, cocoapod, mã C, v.v.

Tôi đã tìm kiếm StackOverflow, Google, Apple, v.v. và chưa tìm thấy câu trả lời Swift cho vấn đề này.

Các lớp mà dường như hứa hẹn nhất là NSDate, NSDateFormatter, NSTimeZone.

Câu hỏi và trả lời liên quan: Làm cách nào để có được ngày ISO 8601 trong iOS?

Đây là điều tốt nhất tôi đã nghĩ ra cho đến nay:

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
Lưu ý rằng iOS10 + SIMPLY BAO GỒM ISO 8601 BUILT-IN .. nó sẽ chỉ tự động hoàn tất cho bạn.
Fattie

2
@Fattie Và - làm thế nào nó có thể xử lý .234Z mili giây đó một phần Zulu / UTC của dấu thời gian? Trả lời: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd

1
@ smat88dd - mẹo tuyệt vời, cảm ơn. Tôi không biết có "lựa chọn về một người định dạng", kỳ lạ và hoang dã!
Fattie

Tôi đang tìm kiếm một giải pháp hoạt động trên linux.
neoneye

@neoneye Chỉ cần sử dụng phiên bản cũ (DateFormatter đơn giản) và thay đổi lịch iso8601 thành gregorian stackoverflow.com/a/28016692/2303865
Leo Dabus

Câu trả lời:


392

Swift 4 • iOS 11.2.1 trở lên

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

Sử dụng:

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 trở lên

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

Giao thức mã hóa

Nếu bạn cần mã hóa và giải mã định dạng này khi làm việc với giao thức Codable, bạn có thể tạo chiến lược mã hóa / giải mã ngày tùy chỉnh của riêng mình:

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

và chiến lược mã hóa

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

Kiểm tra sân chơi

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

mã hóa

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

giải mã

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

nhập mô tả hình ảnh ở đây


3
Sẽ rất hữu ích khi thêm tiện ích mở rộng chuyển đổi ngược lại:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive

1
Chỉ cần lưu ý rằng điều này mất một chút độ chính xác, do đó, điều quan trọng là đảm bảo sự bình đẳng của ngày được so sánh thông qua chuỗi được tạo và không phải là timeInterval. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision

7
Không cần phải nói, nếu bạn không cần mili giây, iOS 10 mới sẽ ISO8601DateFormatterđơn giản hóa quy trình. Tôi đã phát hành một báo cáo lỗi (27242248) cho Apple, yêu cầu họ mở rộng bộ định dạng mới này để cung cấp khả năng chỉ định mili giây (vì bộ định dạng mới này không được sử dụng cho nhiều người trong chúng ta mà không có mili giây).
Rob

1
Trong RFC3339, chúng tôi có thể tìm thấy một ghi chú "LƯU Ý: ISO 8601 xác định ngày và giờ được phân tách bằng" T ". Các ứng dụng sử dụng cú pháp này có thể chọn, vì mục đích dễ đọc, để chỉ định một ngày và toàn thời gian được phân tách bằng (nói) một nhân vật không gian. " Nó có bao gồm cả định dạng ngày mà không có Tví dụ : 2016-09-21 21:05:10+00:00?
manRo

3
@LeoDabus có, nhưng đây là kết quả đầu tiên cho "Swift iso8601". Nhận xét của tôi là để cảnh báo các nhà phát triển khác gặp phải điều này trong tương lai và không được hướng đến OP.
thislooksfun

38

Nhớ đặt ngôn ngữ thành en_US_POSIXnhư được mô tả trong Hỏi & Đáp kỹ thuật . Trong 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))

Vấn đề là nếu bạn đang sử dụng thiết bị đang sử dụng lịch không phải tiếng Gregorian, năm đó sẽ không tuân thủ RFC3339 / ISO8601 trừ khi bạn chỉ định chuỗi localecũng như timeZonedateFormatchuỗi.

Hoặc bạn có thể sử dụng ISO8601DateFormatterđể giúp bạn thoát khỏi cỏ dại localetimeZonechính mình:

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

Đối với phiên bản Swift 2, hãy xem bản sửa đổi trước của câu trả lời này .


tại sao chúng ta nên đặt miền địa phương thành en_US_POSIX? ngay cả khi chúng tôi không ở Mỹ?
mnemonic23

2
Chà, bạn cần một số ngôn ngữ nhất quán và quy ước của tiêu chuẩn ISO 8601 / RFC 3999 là định dạng được cung cấp bởi en_US_POSIX. Đó là ngôn ngữ chung để trao đổi ngày trên web. Và bạn không thể hiểu sai ngày nếu một lịch được sử dụng trên thiết bị khi lưu chuỗi ngày và chuỗi khác khi chuỗi được đọc lại sau đó. Ngoài ra, bạn cần một định dạng được đảm bảo để không bao giờ thay đổi (đó là lý do tại sao bạn sử dụng en_US_POSIXvà không en_US). Xem Hỏi đáp kỹ thuật 1480 hoặc các tiêu chuẩn RFC / ISO để biết thêm thông tin.
Cướp

24

Nếu bạn muốn sử dụng ISO8601DateFormatter()với ngày từ nguồn cấp dữ liệu JSON của Rails 4+ (và tất nhiên không cần millis), bạn cần đặt một vài tùy chọn trên bộ định dạng để nó hoạt động ngay nếu không date(from: string)hàm sẽ trả về không. Đây là những gì tôi đang sử dụng:

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

Đây là kết quả của việc sử dụng các câu tùy chọn không có trong ảnh chụp màn hình sân chơi:

nhập mô tả hình ảnh ở đây


Bạn cũng cần bao gồm trong các tùy chọn .withFractionalSecondsnhưng tôi đã thử điều đó và nó cứ bị lỗi libc++abi.dylib: terminating with uncaught exception of type NSException.
Leo Dabus

@MEnnabah Nó hoạt động tốt với tôi trong Swift 4. Bạn có gặp lỗi không?
Matt Long

@LeoDabus, có lỗi giống như của bạn, bạn đã giải quyết nó chưa?
người tự do

Bộ mã hóa tùy chỉnh DateDecodingStrargety stackoverflow.com/a/46458771/2303865
Leo Dabus

@freeman Nếu bạn muốn giữ Ngày bằng tất cả các giây phân số của nó, tôi khuyên bạn nên sử dụng gấp đôi (khoảng thời gian kể từ ngày tham chiếu) khi lưu / nhận ngày của bạn vào máy chủ. Và sử dụng chiến lược giải mã ngày mặc định .deferredToDatekhi sử dụng giao thức Codable
Leo Dabus

6

Swift 5

Nếu bạn đang nhắm mục tiêu iOS 11.0+ / macOS 10.13+, bạn chỉ cần sử dụng ISO8601DateFormattervới withInternetDateTimewithFractionalSecondscác tùy chọn, như vậy:

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

Để thêm lời khen ngợi Andrés Torres Marroquín và Leo Dabus, tôi có một phiên bản duy trì giây phân đoạn. Tôi không thể tìm thấy tài liệu này ở bất cứ đâu, nhưng Apple rút ngắn một phần giây thành micro giây (3 chữ số chính xác) trên cả đầu vào và đầu ra (ngay cả khi được chỉ định sử dụng SSSSSSS, trái với Unicode tr35-31 ).

Tôi nên nhấn mạnh rằng điều này có lẽ không cần thiết cho hầu hết các trường hợp sử dụng . Ngày trực tuyến thường không cần độ chính xác đến mili giây và khi thực hiện, thường sử dụng định dạng dữ liệu khác sẽ tốt hơn. Nhưng đôi khi người ta phải tương tác với một hệ thống có sẵn theo một cách cụ thể.

Xcode 8/9 và 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
    }
}

Đây là câu trả lời tốt nhất theo ý kiến ​​của tôi ở chỗ nó cho phép người ta đạt được mức độ chính xác đến một phần triệu giây trong đó tất cả các giải pháp khác cắt ngắn ở mili giây.
Michael A. McCloskey

Nếu bạn muốn duy trì Ngày với tất cả các giây phân số của nó, bạn chỉ nên sử dụng gấp đôi (khoảng thời gian kể từ ngày tham chiếu) khi lưu / nhận ngày của bạn vào máy chủ.
Leo Dabus

@LeoDabus có, nếu bạn kiểm soát toàn bộ hệ thống và không cần phải tương tác. Giống như tôi đã nói trong câu trả lời, điều này không cần thiết cho hầu hết người dùng. Nhưng tất cả chúng ta không luôn có quyền kiểm soát định dạng dữ liệu trong API web và vì Android và Python (ít nhất) bảo toàn 6 chữ số chính xác, đôi khi cần phải tuân theo.
Eli Burke

4

Sử dụng ISO8601DateFormattertrên iOS10 hoặc mới hơn.

Sử dụng DateFormattertrên iOS9 trở lên.

Swift 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

Trong trường hợp của tôi, tôi phải chuyển đổi cột DynamoDB - LastUpdated (Dấu thời gian Unix) thành Thời gian bình thường.

Giá trị ban đầu của LastUpdated là: 1460650607601 - được chuyển đổi thành 2016-04-14 16:16:47 +0000 qua:

   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

Trong tương lai, định dạng có thể cần phải được thay đổi, có thể là một cơn đau đầu nhỏ khi có các cuộc gọi date.dateFromISO8601 ở mọi nơi trong một ứng dụng. Sử dụng một lớp và giao thức để bao bọc việc thực hiện, thay đổi cuộc gọi định dạng thời gian ngày ở một nơi sẽ đơn giản hơn. Sử dụng RFC3339 nếu có thể, nó là một đại diện hoàn thiện hơn. DateFormatProtocol và DateFormat là tuyệt vời để tiêm phụ thuộc.

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

Có một ISO8601DateFormatterlớp mới cho phép bạn tạo một chuỗi chỉ với một dòng. Để tương thích ngược tôi đã sử dụng một thư viện C cũ. Tôi hy vọng điều này hữu ích cho một ai đó.

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

1

Để bổ sung cho phiên bản Leo Dabus, tôi đã thêm hỗ trợ cho các dự án được viết bằng Swift và Objective-C, cũng đã thêm hỗ trợ cho các mili giây tùy chọn, có thể không phải là tốt nhất nhưng bạn sẽ hiểu rõ:

Xcode 8 và 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

Nếu không có một số mặt nạ Chuỗi thủ công hoặc 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

Đây là một câu trả lời tốt, nhưng sử dụng .iso8601sẽ không bao gồm mili giây.
Stefan Arentz

0

Dựa trên câu trả lời chấp nhận được trong mô hình đối tượng

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

kêu gọi

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())")
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.