Como criar um carimbo de data e formato de data como fuso horário ISO 8601, RFC 3339, UTC?

186

Como gerar um carimbo de data e hora, usando os padrões de formato para ISO 8601 e RFC 3339 ?

O objetivo é uma sequência que se parece com isso:

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

Formato:

  • ano, mês, dia, como "XXXX-XX-XX"
  • a letra "T" como separador
  • hora, minuto, segundos, milissegundos, como "XX: XX: XX.XXX".
  • a letra "Z" como um designador de zona para deslocamento zero, também conhecido como UTC, GMT, hora do Zulu.

Melhor caso:

  • Código-fonte rápido, simples, curto e direto.
  • Não é necessário usar nenhuma estrutura adicional, subprojeto, cocoapod, código C, etc.

Pesquisei StackOverflow, Google, Apple, etc. e não encontrei uma resposta rápida para isso.

As classes que parecem mais promissores são NSDate, NSDateFormatter, NSTimeZone.

Perguntas e respostas relacionadas: Como obtenho a data ISO 8601 no iOS?

Aqui está o melhor que eu criei até agora:

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
fonte
5
Observe que o iOS10 + SIMPLESMENTE INCLUI A CONSTRUÇÃO ISO 8601 .. ele será preenchido automaticamente apenas para você.
Fattie
2
@Fattie E - como ele pode lidar com essa última parte .234Z milissegundos Zulu / UTC do registro de data e hora? Resposta: Matt Longs @ stackoverflow.com/a/42101630/3078330
smat88dd
1
@ smat88dd - dica fantástica, obrigado. Eu não fazia ideia de que havia "opções em um formatador", esquisitas e loucas!
Fattie
Estou procurando uma solução que funcione no linux.
neoneye
@neoneye Basta usar a versão antiga (simples DateFormatter) e alterar o calendário iso8601 para gregorian stackoverflow.com/a/28016692/2303865
Leo Dabus

Respostas:

392

Swift 4 • iOS 11.2.1 ou posterior

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

Uso:

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 ou posterior

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

Protocolo Codificável

Se você precisar codificar e decodificar esse formato ao trabalhar com o protocolo Codable, poderá criar suas próprias estratégias personalizadas de codificação / decodificação de data:

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

e a estratégia de codificação

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

Teste de Playground

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

codificação

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

decodificação

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

insira a descrição da imagem aqui

Leo Dabus
fonte
3
Seria útil adicionar uma extensão de conversão oposta:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive
1
Apenas observe que isso perde um pouco de precisão; portanto, é importante garantir que a igualdade de datas seja comparada pela string gerada e não pelo timeInterval. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision
7
Escusado será dizer que, se você não precisar de milissegundos, o novo iOS 10 ISO8601DateFormattersimplifica o processo. Emiti um relatório de bug (27242248) à Apple, solicitando que eles expandissem esse novo formatador para oferecer a capacidade de especificar milissegundos também (já que esse novo formatador não é útil para muitos de nós sem os milissegundos).
Rob
1
Na RFC3339 , podemos encontrar uma nota "NOTA: ISO 8601 define data e hora separadas por" T ". Os aplicativos que utilizam esta sintaxe podem escolher, para facilitar a leitura, especificar uma data e um horário completos separados por (digamos) um caractere de espaço ". Abrange também o formato da data sem, Tpor exemplo 2016-09-21 21:05:10+00:00:?
manRo
3
@LeoDabus sim, mas este é o primeiro resultado para "Swift iso8601". Meu comentário foi feito para avisar outros desenvolvedores que se depararem com isso no futuro e não foram direcionados ao OP.
precisa saber é o seguinte
38

Lembre-se de definir o código do idioma para en_US_POSIX código como descrito nas Perguntas e Respostas Técnicas A1480 . No 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))

A questão é que se você estiver em um dispositivo que está usando um calendário não gregoriano, o ano não estará de acordo com RFC3339 / ISO8601 menos que você especifique o localebem como o timeZonee dateFormatstring.

Ou você pode usar ISO8601DateFormatterpara tirar você do mato da configuração localee de timeZonesi mesmo:

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

Para a versão Swift 2, consulte a revisão anterior desta resposta .

Roubar
fonte
por que devemos definir o código do idioma para en_US_POSIX? mesmo se não estamos nos EUA?
mnemonic23
2
Bem, você precisa de um local consistente e a convenção das normas ISO 8601 / RFC 3999 é o formato oferecido por en_US_POSIX. É a língua franca para trocar datas na web. E você não pode interpretar mal as datas se um calendário foi usado no dispositivo ao salvar uma sequência de datas e outro quando a sequência é lida mais tarde. Além disso, você precisa de um formato que nunca será alterado (e é por isso que você usa en_US_POSIXe não en_US). Consulte o Technical Q&A 1480 ou os padrões RFC / ISO para obter mais informações.
Rob
24

Se você quiser usar o ISO8601DateFormatter()com uma data de um feed JSON do Rails 4+ (e não precisar de milis é claro), precisará definir algumas opções no formatador para que ele funcione corretamente, caso contrário, a date(from: string)função retornará nula. Aqui está o que estou usando:

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

Aqui está o resultado do uso das opções versos que não estão em uma captura de tela do playground:

insira a descrição da imagem aqui

Matt Long
fonte
Você precisaria incluir nas opções também o .withFractionalSecondsmas eu já tentei isso e ele continua gerando um erro libc++abi.dylib: terminating with uncaught exception of type NSException.
quer
@MEnnabah Funciona bem para mim no Swift 4. Você está recebendo um erro?
Matt Long
@LeoDabus, obteve o mesmo erro que o seu, você resolveu?
freeman
JSONDecoder personalizado DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Leo Dabus
@ freeman Se você deseja preservar a Data com todos os seus segundos fracionários, sugiro usar um duplo (intervalo de tempo desde a data de referência) ao salvar / receber sua data no servidor. E usar a estratégia de decodificação data padrão .deferredToDateao usar o protocolo codificável
Leo Dabus
6

Swift 5

Se você está segmentando o iOS 11.0+ / macOS 10.13+, basta usar ISO8601DateFormattercom as opções withInternetDateTimee withFractionalSeconds, assim:

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
fonte
5

Para elogiar ainda mais Andrés Torres Marroquín e Leo Dabus, tenho uma versão que preserva segundos fracionários. Não consigo encontrá-lo documentado em nenhum lugar, mas a Apple trunca segundos fracionários até o microssegundo (3 dígitos de precisão) na entrada e na saída (mesmo que especificado usando SSSSSSS, ao contrário do Unicode tr35-31 ).

Devo enfatizar que isso provavelmente não é necessário para a maioria dos casos de uso . As datas on-line normalmente não precisam de precisão de milissegundos e, quando o fazem, geralmente é melhor usar um formato de dados diferente. Mas, às vezes, é preciso interoperar com um sistema preexistente de uma maneira específica.

Xcode 8/9 e 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
fonte
Esta é a melhor resposta na minha opinião, pois permite chegar a um nível de precisão de microssegundos em que todas as outras soluções truncam em milissegundos.
Michael A. McCloskey
Se você deseja preservar a Data com todos os seus segundos fracionários, use apenas um dobro (intervalo de tempo desde a data de referência) ao salvar / receber sua data no servidor.
Leo Dabus
@LeoDabus sim, se você controla todo o sistema e não precisa interoperar. Como eu disse na resposta, isso não é necessário para a maioria dos usuários. Mas nem sempre temos controle sobre a formatação dos dados nas APIs da web e, como Android e Python (pelo menos) preservam 6 dígitos de precisão fracionária, às vezes é necessário seguir o exemplo.
Eli Burke
4

Usa ISO8601DateFormatterno iOS10 ou mais recente.

Usa DateFormatterno iOS9 ou mais antigo.

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
        }
    }()
}
neoneye
fonte
3

No meu caso, tenho que converter a coluna DynamoDB - lastUpdated (carimbo de data e hora do Unix) para o horário normal.

O valor inicial de lastUpdated era: 1460650607601 - convertido para 14-04-2016 16:16:47 +0000 via:

   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
fonte
3

No futuro, pode ser necessário alterar o formato, o que pode ser uma dor de cabeça pequena com date.dateFromISO8601 em todos os lugares em um aplicativo. Use uma classe e um protocolo para encerrar a implementação. A alteração da chamada do formato de data e hora em um local será mais simples. Use RFC3339, se possível, é uma representação mais completa. DateFormatProtocol e DateFormat são ótimos para injeção de dependência.

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
fonte
3

Há um novo ISO8601DateFormatter classe que permite criar uma string com apenas uma linha. Para compatibilidade com versões anteriores, usei uma biblioteca C antiga. Espero que isso seja útil para alguém.

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
fonte
1

Para complementar a versão de Leo Dabus, adicionei suporte para projetos escritos Swift e Objective-C, também adicionei suporte para milissegundos opcionais, provavelmente não é o melhor, mas você entenderia:

Xcode 8 e 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
fonte
0

Sem algumas máscaras de String manuais ou 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
fonte
Esta é uma boa resposta, mas o uso .iso8601não incluirá milissegundos.
Stefan Arentz
0

Com base na resposta aceitável em um paradigma de objeto

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