Bagaimana cara membuat cap waktu tanggal dan format sebagai ISO 8601, RFC 3339, zona waktu UTC?

186

Bagaimana cara menghasilkan cap waktu tanggal, menggunakan standar format untuk ISO 8601 dan RFC 3339 ?

Tujuannya adalah string yang terlihat seperti ini:

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

Format:

  • tahun, bulan, hari, sebagai "XXXX-XX-XX"
  • huruf "T" sebagai pemisah
  • jam, menit, detik, milidetik, sebagai "XX: XX: XX.XXX".
  • huruf "Z" sebagai penunjuk zona untuk zero offset, alias UTC, GMT, waktu Zulu.

Kasus terbaik:

  • Kode sumber cepat yang sederhana, pendek, dan mudah.
  • Tidak perlu menggunakan kerangka kerja tambahan, sub proyek, cocoapod, kode C, dll.

Saya telah mencari StackOverflow, Google, Apple, dll. Dan belum menemukan jawaban Swift untuk ini.

Kelas-kelas yang tampaknya paling menjanjikan adalah NSDate, NSDateFormatter, NSTimeZone.

T&J terkait: Bagaimana cara mendapatkan tanggal ISO 8601 di iOS?

Inilah yang terbaik yang saya hasilkan sejauh ini:

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))
joelparkerhenderson
sumber
6
Perhatikan bahwa iOS10 + SIMPLY TERMASUK ISO 8601 BUILT-IN .. itu hanya akan melengkapi otomatis untuk Anda.
Fattie
2
@Fattie Dan - bagaimana bisa menangani bagian terakhirnya .234Z milidetik Zulu / UTC dari cap waktu? Jawaban: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd
1
@ smat88dd - tip fantastis, terima kasih. Saya tidak tahu ada "opsi pada formatter", aneh dan liar!
Fattie
Saya mencari solusi yang berfungsi di linux.
neoneye
@neoneye Cukup gunakan versi lama (DateFormatter biasa) dan ubah kalender iso8601 menjadi gregorian stackoverflow.com/a/28016692/2303865
Leo Dabus

Jawaban:

392

Swift 4 • iOS 11.2.1 atau lebih baru

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

Pemakaian:

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 atau lebih baru

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

Protokol Codable

Jika Anda perlu menyandikan dan mendekode format ini saat bekerja dengan protokol Codable, Anda dapat membuat strategi penyandian / pengodean tanggal kustom Anda sendiri:

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

dan strategi pengkodean

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

Pengujian Playground

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

encoding

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

decoding

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

masukkan deskripsi gambar di sini

Leo Dabus
sumber
3
Akan bermanfaat untuk menambahkan ekstensi konversi yang berlawanan:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive
1
Hanya catatan bahwa ini kehilangan sedikit presisi sehingga penting untuk memastikan kesetaraan tanggal dibandingkan melalui string yang dihasilkan dan bukan timeInterval. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision
7
Tidak perlu dikatakan lagi, jika Anda tidak membutuhkan milidetik, iOS 10 baru akan ISO8601DateFormattermenyederhanakan prosesnya. Saya telah mengeluarkan laporan bug (27242248) kepada Apple, meminta mereka untuk memperluas formatter baru ini untuk menawarkan kemampuan untuk menentukan milidetik juga (karena formatter baru ini tidak berguna bagi banyak dari kita tanpa milidetik).
Rob
1
Dalam RFC3339 kita dapat menemukan catatan "CATATAN: ISO 8601 mendefinisikan tanggal dan waktu dipisahkan oleh" T ". Aplikasi yang menggunakan sintaks ini dapat memilih, demi keterbacaan, untuk menentukan tanggal penuh dan waktu penuh dipisahkan oleh (katakanlah) karakter luar angkasa. " Apakah itu mencakup juga format tanggal tanpa Tmisalnya: 2016-09-21 21:05:10+00:00?
manRo
3
@ LeoDabus ya, tapi ini adalah hasil pertama untuk "Swift iso8601". Komentar saya dimaksudkan untuk memperingatkan pengembang lain yang menemukan ini di masa depan dan tidak diarahkan ke OP.
thislooksfun
38

Ingatlah untuk mengatur lokal ke en_US_POSIXseperti yang dijelaskan dalam Tanya Jawab Teknis . Dalam 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))

Masalahnya adalah bahwa jika Anda menggunakan perangkat yang menggunakan kalender non-Gregorian, tahun tersebut tidak akan sesuai dengan RFC3339 / ISO8601 kecuali Anda menentukan localeserta dan timeZonedan dateFormatstring.

Atau Anda dapat menggunakan ISO8601DateFormatteruntuk mengeluarkan Anda dari gulma pengaturan localedan timeZonediri Anda sendiri:

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

Untuk rendisi Swift 2, lihat revisi sebelumnya dari jawaban ini .

rampok
sumber
mengapa kita harus mengatur lokal ke en_US_POSIX? bahkan jika kita tidak di AS?
mnemonic23
2
Nah, Anda perlu beberapa lokal yang konsisten dan konvensi standar ISO 8601 / RFC 3999 adalah bahwa format yang ditawarkan oleh en_US_POSIX. Ini adalah lingua franca untuk bertukar tanggal di web. Dan Anda tidak bisa salah mengartikan tanggal jika satu kalender digunakan pada perangkat saat menyimpan string tanggal dan lainnya ketika string dibaca kembali nanti. Juga, Anda memerlukan format yang dijamin tidak akan pernah berubah (itulah sebabnya Anda menggunakan en_US_POSIXdan tidak en_US). Lihat Tanya Jawab Teknis 1480 atau standar RFC / ISO tersebut untuk informasi lebih lanjut.
Rob
24

Jika Anda ingin menggunakan ISO8601DateFormatter()dengan tanggal dari Rails 4+ JSON feed (dan tentu saja tidak perlu milis), Anda perlu mengatur beberapa opsi pada formatter agar berfungsi dengan benar jika date(from: string)fungsi tidak akan mengembalikan nol. Inilah yang saya gunakan:

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

Inilah hasil dari penggunaan ayat-ayat pilihan bukan di tangkapan layar taman bermain:

masukkan deskripsi gambar di sini

Matt Long
sumber
Anda harus memasukkan dalam opsi juga .withFractionalSecondstetapi saya sudah mencobanya dan itu terus melontarkan kesalahan libc++abi.dylib: terminating with uncaught exception of type NSException.
Leo Dabus
@ UMnnabah Ini berfungsi dengan baik untuk saya di Swift 4. Apakah Anda mendapatkan kesalahan?
Matt Long
@ LeoDabus, ada kesalahan yang sama seperti milik Anda, apakah Anda menyelesaikannya?
freeman
custom JSONDecoder DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo Dabus
@ freeman Jika Anda ingin mempertahankan Tanggal dengan semua detik fraksinya, saya sarankan untuk menggunakan ganda (interval waktu sejak tanggal referensi) ketika menyimpan / menerima tanggal Anda ke server. Dan gunakan strategi decoding tanggal default .deferredToDatesaat menggunakan protokol Codable
Leo Dabus
6

Cepat 5

Jika Anda menargetkan iOS 11.0+ / macOS 10.13+, Anda cukup menggunakan ISO8601DateFormatterdengan withInternetDateTimedan withFractionalSecondsopsi, seperti:

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"
jrc
sumber
5

Penggunaan ISO8601DateFormatterpada iOS10 atau yang lebih baru.

Penggunaan DateFormatterpada iOS9 atau lebih.

Cepat 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
        }
    }()
}
neoneye
sumber
5

Untuk lebih lanjut memuji Andrés Torres Marroquín dan Leo Dabus, saya memiliki versi yang mempertahankan pecahan detik. Saya tidak dapat menemukannya didokumentasikan di mana pun, tetapi Apple memotong fraksional detik ke mikrodetik (presisi 3 digit) pada input dan output (meskipun ditentukan menggunakan SSSSSSS, bertentangan dengan Unicode tr35-31 ).

Saya harus menekankan bahwa ini mungkin tidak perlu untuk kebanyakan kasus penggunaan . Tanggal online biasanya tidak memerlukan ketepatan milidetik, dan ketika mereka melakukannya, seringkali lebih baik menggunakan format data yang berbeda. Tetapi kadang-kadang seseorang harus beroperasi dengan sistem yang sudah ada sebelumnya dengan cara tertentu.

Xcode 8/9 dan 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
    }
}
Eli Burke
sumber
Ini adalah jawaban terbaik menurut pendapat saya karena memungkinkan seseorang untuk mencapai tingkat presisi mikrodetik di mana semua solusi lain terpotong pada milidetik.
Michael A. McCloskey
Jika Anda ingin mempertahankan Tanggal dengan semua detik fraksinya, Anda harus menggunakan hanya dua kali (interval waktu sejak tanggal referensi) saat menyimpan / menerima tanggal Anda ke server.
Leo Dabus
@ LeoDabus ya, jika Anda mengontrol seluruh sistem dan tidak perlu beroperasi. Seperti yang saya katakan di jawaban, ini tidak perlu bagi sebagian besar pengguna. Tetapi kita tidak semua selalu memiliki kendali atas pemformatan data dalam API web, dan karena Android dan Python (setidaknya) mempertahankan 6 digit presisi fraksional, terkadang perlu untuk mengikuti.
Eli Burke
3

Dalam kasus saya, saya harus mengubah kolom DynamoDB - lastUpdated (Unix Timestamp) ke Waktu Normal.

Nilai awal lastUpdated adalah: 1460650607601 - dikonversi ke 2016-04-14 16:16:47 +0000 melalui:

   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)

            }
ioopl
sumber
3

Di masa depan format mungkin perlu diubah yang bisa menjadi sakit kepala kecil yang memiliki date.dateFromISO8601 panggilan di mana-mana dalam aplikasi. Gunakan kelas dan protokol untuk membungkus implementasi, mengubah panggilan format waktu tanggal di satu tempat akan lebih sederhana. Gunakan RFC3339 jika memungkinkan, itu representasi yang lebih lengkap. DateFormatProtocol dan DateFormat sangat bagus untuk injeksi ketergantungan.

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

}
Gary Davies
sumber
3

Ada ISO8601DateFormatterkelas baru yang memungkinkan Anda membuat string hanya dengan satu baris. Untuk kompatibilitas mundur saya menggunakan C-library lama. Saya harap ini bermanfaat bagi seseorang.

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)
        }
    }
}
Thomas Szabo
sumber
1

Untuk melengkapi versi Leo Dabus, saya menambahkan dukungan untuk proyek yang ditulis Swift dan Objective-C, juga menambahkan dukungan untuk milidetik opsional, mungkin bukan yang terbaik tetapi Anda akan mendapatkan intinya:

Xcode 8 dan 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
    }
}
Andrés Torres Marroquín
sumber
0

Tanpa beberapa topeng String manual atau 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
Dmitrii Z
sumber
Ini adalah jawaban yang bagus, tetapi menggunakan .iso8601tidak akan mencakup milidetik.
Stefan Arentz
0

Berdasarkan jawaban yang dapat diterima dalam paradigma objek

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

tempat panggilan

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