ISO 8601, RFC 3339, UTC saat dilimi olarak tarih saat damgası ve biçimi nasıl oluşturulur?


186

ISO 8601 ve RFC 3339 için biçim standartlarını kullanarak bir tarih zaman damgası nasıl oluşturulur ?

Amaç şuna benzer bir dizedir:

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

Biçim:

  • yıl, ay, gün, "XXXX-XX-XX" olarak
  • ayırıcı olarak "T" harfi
  • saat, dakika, saniye, milisaniye, "XX: XX: XX.XXX" olarak.
  • sıfır ofseti için bir bölge belirleyicisi olarak "Z" harfi, diğer bir deyişle UTC, GMT, Zulu saati.

En iyi senaryo:

  • Basit, kısa ve anlaşılır hızlı kaynak kodu.
  • Herhangi bir ek çerçeve, alt proje, kokoapod, C kodu vb. Kullanmaya gerek yoktur.

StackOverflow, Google, Apple vb. Aradım ve buna bir Swift cevabı bulamadım.

En umut verici görünüyor sınıflar vardır NSDate, NSDateFormatter, NSTimeZone.

İlgili Soru-Cevap: iOS'ta ISO 8601 tarihini nasıl alabilirim?

İşte şimdiye kadar bulduğum en iyi şey:

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

6
İOS10 + SIMPLY'nin ISO 8601 ANKASTRE İÇERDİĞİNİ unutmayın ... sadece sizin için otomatik olarak tamamlanacaktır.
Fattie

2
@Fattie Ve - zaman damgasının son 233Z milisaniye Zulu / UTC kısmını nasıl işleyebilir? Cevap: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd

1
@ smat88dd - harika bir ipucu, teşekkürler. Garip ve vahşi "biçimlendirici seçenekleri" vardı hiçbir ipucu yoktu!
Fattie

Linux üzerinde çalışan bir çözüm arıyorum.
neoneye

@neoneye Sadece eski sürümü (düz DateFormatter) kullanın ve iso8601 takvimini gregoryen stackoverflow.com/a/28016692/2303865 olarak değiştirin
Leo Dabus

Yanıtlar:


392

Swift 4 • iOS 11.2.1 veya üstü

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

Kullanımı:

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 veya üstü

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

Uyumlu Protokol

Codable protokolü ile çalışırken bu biçimi kodlamanız ve deşifre etmeniz gerekiyorsa, kendi özel tarih kodlama / kod çözme stratejilerinizi oluşturabilirsiniz:

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

ve kodlama stratejisi

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

Oyun Alanı Testi

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

kodlama

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

şifre çözme

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

resim açıklamasını buraya girin


3
Tam tersi dönüştürme uzantısı eklemek için yararlı olurdu:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive

1
Bunun bir miktar hassasiyeti kaybettiğine dikkat edin, böylece tarih eşitliğinin timeInterval ile değil, üretilen dize ile karşılaştırıldığından emin olun. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision

7
Söylemeye gerek yok, milisaniyeye ihtiyacınız yoksa, yeni iOS 10 ISO8601DateFormatterişlemi basitleştirir. Apple'a, bu yeni biçimlendiriciyi milisaniye belirtme yeteneği sunmak için genişletmelerini isteyen bir hata raporu (27242248) yayınladım (bu yeni biçimlendirici, milisaniye olmadan çoğumuz için kullanılmadığı için).
Rob

1
In RFC3339 bir not bulabilirsiniz "NOT: ISO 8601 tanımlar tarih ve saat ile ayrılmış 'T' Bu sözdizimini kullanarak Uygulamaları, okunabilirlik uğruna, tercih edebilir tam tarih ve tam zamanlı ayrılmış belirtmek için (diyelim). boşluk karakteri. " O olmadan da tarih biçimi olarak kapsamıyor mu Tmesela: 2016-09-21 21:05:10+00:00?
manRo

3
@LeoDabus evet, ama bu "Swift iso8601" için ilk sonuç. Benim yorumum, gelecekte bununla karşılaşan ve OP'ye yönlendirilmemiş diğer geliştiricileri uyarmaktı.
thislooksfun

38

Yerel ayarı Teknik Q & A1480'deen_US_POSIX açıklandığı gibi ayarlamayı unutmayın . Swift 3'te:

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

Sorun, Gregoryen olmayan bir takvim kullanan bir cihazdaysanız localeve timeZoneve dateFormatdizesini belirtmediğiniz sürece yıl RFC3339 / ISO8601 ile uyumlu olmaz .

Ya da sizi ISO8601DateFormatterortamın localeve timeZonekendinizin yabani otlarından çıkarmak için kullanabilirsiniz :

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 yorumlaması için bu cevabın önceki revizyonuna bakın .


yerel ayarı neden en_US_POSIX olarak ayarlamalıyız? ABD'de olmasak bile?
mnemonic23

2
Peki, bazı tutarlı yerel ayarlara ihtiyacınız var ve ISO 8601 / RFC 3999 standartlarının uyumu tarafından sunulan format budur en_US_POSIX. Web'de tarih alışverişi yapmak için lingua franca . Bir tarih dizesini kaydederken bir takvim cihazda kullanılmışsa ve daha sonra yeniden okunurken başka bir takvim kullanılıyorsa tarihleri ​​yanlış yorumlayamazsınız. Ayrıca, asla değişmeyeceği garanti edilen bir formata ihtiyacınız vardır (bu yüzden kullanırsınız en_US_POSIXve kullanmazsınız en_US). Daha fazla bilgi için Teknik Soru ve Cevap 1480'e veya bu RFC / ISO standartlarına bakın.
Rob

24

ISO8601DateFormatter()Bir Rails 4+ JSON feed'inden bir tarihle kullanmak istiyorsanız (ve elbette milislere ihtiyacınız yoksa), doğru çalışması için formatlayıcıda birkaç seçenek ayarlamanız gerekir, aksi takdirde date(from: string)işlev nil döndürür. İşte ne kullanıyorum:

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

Oyun ekran görüntüsünde olmayan seçenek ayetlerini kullanmanın sonucu:

resim açıklamasını buraya girin


Ayrıca seçeneklere dahil etmek gerekir .withFractionalSecondsama zaten denedim ve bir hata atmaya devam ediyor libc++abi.dylib: terminating with uncaught exception of type NSException.
Leo Dabus

@MEnnabah Swift 4'te benim için iyi çalışıyor. Bir hata mı alıyorsunuz?
Matt Long

@LeoDabus, seninkiyle aynı hatayı aldı, çözdün mü?
freeman

özel JSONDecoder DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo Dabus

@ freeman Eğer tarihi tüm kesirli saniyeleriyle korumak istiyorsanız, tarihinizi sunucuya kaydederken / alırken bir çift (referans tarihten bu yana zaman aralığı) kullanmanızı öneririm. .deferredToDateCodable protokolünü kullanırken varsayılan tarih kod çözme stratejisini kullanın
Leo Dabus

6

Hızlı 5

İOS 11.0+ / MacOS 10.13+ hedefliyorsanız, sadece kullanmak ISO8601DateFormatterile withInternetDateTimeve withFractionalSecondsopsiyon şöyle:

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

ISO8601DateFormatterİOS10 veya daha yeni sürümlerde kullanır .

DateFormatterİOS9 veya daha eski sürümlerde kullanır .

Hızlı 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
        }
    }()
}

5

Andrés Torres Marroquín ve Leo Dabus'a iltifat etmek için, kesirli saniyeleri koruyan bir versiyonum var. Belgeyi hiçbir yerde belgelendiremiyorum, ancak Apple, hem giriş hem de çıkıştaki mikrosaniye (3 basamaklı hassasiyet) kesirli saniyeleri kesiyor (SSSSSSS kullanılarak belirtilmiş olsa da, Unicode tr35-31'in aksine ).

Bunun çoğu kullanım durumu için muhtemelen gerekli olmadığını vurgulamalıyım . Çevrimiçi tarihler genellikle milisaniye hassasiyete ihtiyaç duymazlar ve böyle olduklarında, farklı bir veri biçimi kullanmak genellikle daha iyidir. Ancak bazen önceden var olan bir sistemle belirli bir şekilde birlikte çalışmak gerekir.

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

Benim görüşüme göre bu, diğer tüm çözümlerin milisaniyede kısaldığı mikrosaniye hassasiyet seviyesine ulaşmasına izin verdiği için en iyi cevap.
Michael A. McCloskey

Tarihi tüm kesirli saniyeleriyle korumak istiyorsanız, tarihinizi sunucuya kaydederken / alırken yalnızca bir çift (referans tarihten bu yana zaman aralığı) kullanmanız gerekir.
Leo Dabus

@LeoDabus evet, tüm sistemi kontrol ediyorsanız ve birlikte çalışmanız gerekmiyorsa. Cevabımda söylediğim gibi, bu çoğu kullanıcı için gerekli değildir. Ancak hepimiz web API'lerindeki veri biçimlendirmesi üzerinde her zaman kontrole sahip değiliz ve Android ve Python (en azından) 6 basamaklı kesirli hassasiyeti koruduğu için, bazen uygun olmak gerekir.
Eli Burke

3

Benim durumumda DynamoDB - lastUpdated sütununu (Unix Zaman Damgası) Normal Saate dönüştürmek zorundayım.

LastUpdated ilk değeri: 1460650607601 - 2016-04-14 16:16:47 +0000'e dönüştürüldü:

   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

Gelecekte, formatın değiştirilmesi gerekebilir, bu da date.dateFromISO8601 uygulamasının her yerine çağrı yapan küçük bir baş ağrısı olabilir. Uygulamayı sarmak için bir sınıf ve protokol kullanın, tarih saat formatı çağrısını tek bir yerde değiştirmek daha kolay olacaktır. Mümkünse RFC3339 kullanın, daha eksiksiz bir gösterimdir. DateFormatProtocol ve DateFormat bağımlılık enjeksiyonu için mükemmeldir.

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

Yeni var ISO8601DateFormatterSadece bir satırlı bir dize oluşturmanıza izin sınıf var. Geriye dönük uyumluluk için eski bir C kütüphanesi kullandım. Umarım bu birisi için faydalıdır.

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

Leo Dabus'un sürümünü tamamlamak için Swift ve Objective-C yazılan projelere destek ekledim, isteğe bağlı milisaniyeler için destek ekledim, muhtemelen en iyisi değil, ama şunu anlayacaksınız:

Xcode 8 ve 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

Bazı manuel Dize maskeleri veya TimeFormatters olmadan

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

Bu iyi bir cevaptır, ancak kullanmak .iso8601milisaniye içermez.
Stefan Arentz

0

Bir nesne paradigmasındaki kabul edilebilir cevaba dayanarak

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())")
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.