|
| 1 | +// Date+Convertible.swift |
| 2 | +// SwiftDevKit |
| 3 | +// |
| 4 | +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors |
| 5 | +// MIT License - https://opensource.org/licenses/MIT |
| 6 | + |
| 7 | +import Foundation |
| 8 | + |
| 9 | +/// Actor that manages thread-safe access to date formatters. |
| 10 | +/// |
| 11 | +/// This actor provides a cache for `DateFormatter` instances, ensuring thread safety |
| 12 | +/// while maintaining performance through reuse. All formatters are configured with |
| 13 | +/// the POSIX locale and UTC timezone for consistent behavior across platforms. |
| 14 | +private actor DateFormatterCache { |
| 15 | + /// Cache of date formatters keyed by format string |
| 16 | + private var formatters: [String: DateFormatter] = [:] |
| 17 | + |
| 18 | + /// Gets or creates a date formatter for the specified format. |
| 19 | + /// |
| 20 | + /// - Parameter format: The date format string |
| 21 | + /// - Returns: A configured DateFormatter instance |
| 22 | + func formatter(for format: String) -> DateFormatter { |
| 23 | + if let formatter = formatters[format] { |
| 24 | + return formatter |
| 25 | + } |
| 26 | + |
| 27 | + let formatter = DateFormatter() |
| 28 | + formatter.dateFormat = format |
| 29 | + formatter.locale = Locale(identifier: "en_US_POSIX") |
| 30 | + formatter.timeZone = TimeZone(secondsFromGMT: 0) |
| 31 | + formatters[format] = formatter |
| 32 | + return formatter |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +/// Extends Date to support string conversion with thread-safe operations. |
| 37 | +/// |
| 38 | +/// This extension provides methods for converting dates to and from strings using various formats. |
| 39 | +/// All operations are thread-safe and can be called concurrently from multiple tasks. |
| 40 | +extension Date: DateConvertible { |
| 41 | + /// Thread-safe date formatter cache |
| 42 | + private static let formatterCache = DateFormatterCache() |
| 43 | + |
| 44 | + public func toString(format: String?) async throws -> String { |
| 45 | + let dateFormat = format ?? DateFormat.iso8601 |
| 46 | + let formatter = await Self.formatterCache.formatter(for: dateFormat) |
| 47 | + return formatter.string(from: self) |
| 48 | + } |
| 49 | + |
| 50 | + public static func fromString(_ string: String, format: String?) async throws -> Date { |
| 51 | + let dateFormat = format ?? DateFormat.iso8601 |
| 52 | + let formatter = await formatterCache.formatter(for: dateFormat) |
| 53 | + |
| 54 | + guard let date = formatter.date(from: string) else { |
| 55 | + throw DateConversionError.invalidFormat(string) |
| 56 | + } |
| 57 | + |
| 58 | + return date |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +// MARK: - Convenience Methods |
| 63 | + |
| 64 | +public extension Date { |
| 65 | + /// Creates a date from an ISO8601 string. |
| 66 | + /// |
| 67 | + /// This is a convenience method that uses the ISO8601 format for parsing. |
| 68 | + /// The operation is thread-safe and can be called concurrently. |
| 69 | + /// |
| 70 | + /// - Parameter iso8601String: The ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") |
| 71 | + /// - Returns: A new Date instance |
| 72 | + /// - Throws: DateConversionError if the string is not valid ISO8601 |
| 73 | + static func fromISO8601(_ iso8601String: String) async throws -> Date { |
| 74 | + try await fromString(iso8601String, format: DateFormat.iso8601) |
| 75 | + } |
| 76 | + |
| 77 | + /// Converts the date to an ISO8601 string. |
| 78 | + /// |
| 79 | + /// This is a convenience method that uses the ISO8601 format for formatting. |
| 80 | + /// The operation is thread-safe and can be called concurrently. |
| 81 | + /// |
| 82 | + /// - Returns: An ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") |
| 83 | + /// - Throws: DateConversionError if the conversion fails |
| 84 | + func toISO8601() async throws -> String { |
| 85 | + try await toString(format: DateFormat.iso8601) |
| 86 | + } |
| 87 | + |
| 88 | + /// Creates a date from an HTTP date string. |
| 89 | + /// |
| 90 | + /// This is a convenience method that uses the HTTP date format for parsing. |
| 91 | + /// The operation is thread-safe and can be called concurrently. |
| 92 | + /// |
| 93 | + /// - Parameter httpDateString: The HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") |
| 94 | + /// - Returns: A new Date instance |
| 95 | + /// - Throws: DateConversionError if the string is not a valid HTTP date |
| 96 | + static func fromHTTPDate(_ httpDateString: String) async throws -> Date { |
| 97 | + try await fromString(httpDateString, format: DateFormat.http) |
| 98 | + } |
| 99 | + |
| 100 | + /// Converts the date to an HTTP date string. |
| 101 | + /// |
| 102 | + /// This is a convenience method that uses the HTTP date format for formatting. |
| 103 | + /// The operation is thread-safe and can be called concurrently. |
| 104 | + /// |
| 105 | + /// - Returns: An HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") |
| 106 | + /// - Throws: DateConversionError if the conversion fails |
| 107 | + func toHTTPDate() async throws -> String { |
| 108 | + try await toString(format: DateFormat.http) |
| 109 | + } |
| 110 | +} |
0 commit comments