Как создать метку даты и время в формате ISO 8601, RFC 3339, часовой пояс UTC?

186

Как сгенерировать метку даты и времени, используя стандарты формата ISO 8601 и RFC 3339 ?

Цель - строка, которая выглядит следующим образом:

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

Формат:

  • год, месяц, день, как "XXXX-XX-XX"
  • буква "Т" в качестве разделителя
  • часы, минуты, секунды, миллисекунды, как "XX: XX: XX.XXX".
  • буква «Z» в качестве обозначения зоны для смещения нуля, также известного как UTC, GMT, Zulu time.

Лучший случай:

  • Быстрый исходный код, который прост, короток и понятен.
  • Нет необходимости использовать какие-либо дополнительные фреймворки, подпроекты, cocoapod, C-код и т. Д.

Я искал StackOverflow, Google, Apple и т. Д. И не нашел ответа Swift на это.

Классы , которые кажутся наиболее перспективными являются NSDate, NSDateFormatter, NSTimeZone.

Вопросы и ответы по теме: Как получить дату ISO 8601 в iOS?

Вот лучшее, что я придумал до сих пор:

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
источник
6
Обратите внимание, что iOS10 + ПРОСТО ВКЛЮЧАЕТ ИСО 8601 ВСТРОЕННЫЙ ... он просто автоматически завершится для вас.
Толстяк
2
@Fattie И - как он может справиться с этой последней .234Z миллисекундами зулу / UTC части метки времени? Ответ: Мэтт Лонгс @ stackoverflow.com/a/42101630/3078330
smat88dd
1
@ smat88dd - фантастический совет, спасибо. Я понятия не имел, были «варианты форматирования», странные и дикие!
Толстяк
Я ищу решение, которое работает на Linux.
neoneye
@neoneye Просто используйте старую версию (просто DateFormatter) и измените календарь iso8601 на григорианский stackoverflow.com/a/28016692/2303865
Лео Дабус,

Ответы:

393

Swift 4 • iOS 11.2.1 или более поздняя версия

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

Использование:

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, вы можете создать свои собственные стратегии кодирования / декодирования даты:

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"]

введите описание изображения здесь

Лео Дабус
источник
3
Было бы полезно добавить противоположное расширение конверсии:extension String { var dateFormattedISO8601: NSDate? {return NSDate.Date.formatterISO8601.dateFromString(self)} }
Vive
1
Просто обратите внимание, что это теряет немного точности, поэтому важно убедиться, что равенство дат сравнивается через сгенерированную строку, а не timeInterval. let now = NSDate() let stringFromDate = now.iso8601 let dateFromString = stringFromDate.dateFromISO8601! XCTAssertEqual(now.timeIntervalSince1970, dateFromString.timeIntervalSince1970)
pixelrevision
7
Само собой разумеется, если вам не нужны миллисекунды, новая iOS 10 ISO8601DateFormatterупрощает процесс. Я выпустил отчет об ошибке (27242248) для Apple, попросив их расширить этот новый форматер, чтобы также предлагать возможность указывать миллисекунды (поскольку этот новый форматер не используется для многих из нас без миллисекунд).
Роб
1
В RFC3339 мы можем найти примечание «ПРИМЕЧАНИЕ: ISO 8601 определяет дату и время, разделенные« T ». Приложения, использующие этот синтаксис, могут выбрать для удобства чтения указание полной даты и полного времени, разделенных (скажем) космический персонаж. " Охватывает ли это , а также формат даты без Tнапример: 2016-09-21 21:05:10+00:00?
ManRo
3
@ LeoDabus да, но это первый результат для "Swift iso8601". Мой комментарий должен был предупредить других разработчиков, которые сталкивались с этим в будущем, и не был направлен на OP.
thislooksfun
39

Не забудьте установить язык, en_US_POSIXкак описано в Технических вопросах и ответах 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, а также timeZoneи dateFormatстрока.

Или вы можете использовать, ISO8601DateFormatterчтобы вытащить вас из сорняков сеттинга localeи timeZoneсебя:

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 см. Предыдущую редакцию этого ответа .

обкрадывать
источник
почему мы должны установить локаль en_US_POSIX? даже если мы не в США?
mnemonic23
2
Ну, вам нужен какой-то согласованный язык, а стандарт ISO 8601 / RFC 3999 - это формат, предлагаемый en_US_POSIX. Это лингва франка для обмена датами в Интернете. И вы не можете неправильно интерпретировать даты, если один календарь использовался на устройстве при сохранении строки даты, а другой - при последующем считывании строки. Кроме того, вам нужен формат, который гарантированно никогда не изменится (именно поэтому вы используете en_US_POSIXи не en_US). См. Технические вопросы и ответы 1480 или эти стандарты RFC / ISO для получения дополнительной информации.
Роб
24

Если вы хотите использовать ISO8601DateFormatter()с датой из фида Rails 4+ JSON (и, конечно, не нуждаетесь в миллисекундах), вам нужно установить несколько параметров для форматера, чтобы он работал правильно, иначе 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
    }()
}

Вот результат использования стихов опций, которых нет на скриншоте детской площадки:

введите описание изображения здесь

Мэтт Лонг
источник
Вы должны были бы включить в опции также, .withFractionalSecondsно я уже пробовал это, и он продолжает выдавать ошибку libc++abi.dylib: terminating with uncaught exception of type NSException.
Лев Дабус
@Mnnabah У меня в Swift 4 работает нормально. Вы получаете ошибку?
Мэтт Лонг
@LeoDabus, получил ту же ошибку, что и ты, ты решил ее?
Freeman
пользовательский JSONDecoder DateDecodingStrategy stackoverflow.com/a/46458771/2303865
Лео Дабус
@freeman Если вы хотите сохранить дату со всеми ее долями секунды, я предлагаю использовать двойное значение (интервал времени с контрольной даты) при сохранении / получении вашей даты на сервере. И используйте стратегию декодирования даты по умолчанию .deferredToDateпри использовании протокола Codable
Лео Дабус
6

Swift 5

Если вы ориентируетесь IOS 11.0+ / MacOS 10.13+, вы просто использовать ISO8601DateFormatterс withInternetDateTimeи withFractionalSecondsопциями, например , так:

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
источник
5

Используется ISO8601DateFormatterна iOS10 или новее.

Используется DateFormatterна iOS9 или старше.

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
источник
5

Чтобы еще больше поблагодарить Андреса Торреса Маррокина и Лео Дабуса, у меня есть версия, которая сохраняет доли секунды. Я не могу найти это нигде, но 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
    }
}
Эли Берк
источник
На мой взгляд, это лучший ответ в том смысле, что он позволяет достичь точности микросекунды, когда все другие решения усекаются за миллисекунды.
Майкл А. Макклоски
Если вы хотите сохранить дату со всеми ее долями в секундах, вы должны использовать только двойное значение (интервал времени с контрольной даты) при сохранении / получении вашей даты на сервере.
Лев Дабус
@ LeoDabus да, если вы контролируете всю систему и не нуждаетесь во взаимодействии. Как я уже сказал в ответе, это не обязательно для большинства пользователей. Но мы не всегда можем контролировать форматирование данных в веб-API, и поскольку Android и Python (по крайней мере) сохраняют 6 цифр дробной точности, иногда необходимо следовать их примеру.
Эли Берк
3

В моем случае мне нужно преобразовать столбец DynamoDB - lastUpdated (Unix Timestamp) в обычное время.

Начальное значение 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)

            }
ioopl
источник
3

В будущем, возможно, потребуется изменить формат, что может быть небольшой головной болью с вызовом date.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
        }
    }

}
Гари Дэвис
источник
3

Существует новый ISO8601DateFormatterкласс, который позволяет вам создать строку с одной строкой. Для обратной совместимости я использовал старую C-библиотеку. Надеюсь это кому-нибудь пригодится.

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, я добавил поддержку проектов, написанных на 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
    }
}
Андрес Торрес Маррокин
источник
0

Без некоторых ручных строковых масок или 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
Дмитрий З
источник
Это хороший ответ, но использование .iso8601не будет включать миллисекунды.
Стефан Арентц
0

На основании приемлемого ответа в объектной парадигме

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
источник