From 2e959fecfe63b079e1db4f3b9bd612e042a8a9cb Mon Sep 17 00:00:00 2001 From: Dave DeLong Date: Sun, 11 Feb 2024 10:38:11 -0700 Subject: [PATCH] Simplify coding if it's using standard values If the region's values are equivalent to the standard values, then we only really to encode the identifiers and not the full object --- .../Time/4-Fixed Values/Fixed+Codable.swift | 95 +++++++++++++++++-- .../Time/Internals/DateFormatterCache.swift | 2 + .../Time/Internals/Region+Equivalence.swift | 53 +++++++++++ Sources/Time/Internals/SimpleCache.swift | 57 +++++++++++ Sources/Time/Internals/Snapshot.swift | 11 ++- 5 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 Sources/Time/Internals/Region+Equivalence.swift create mode 100644 Sources/Time/Internals/SimpleCache.swift diff --git a/Sources/Time/4-Fixed Values/Fixed+Codable.swift b/Sources/Time/4-Fixed Values/Fixed+Codable.swift index 74299bf..61ca563 100644 --- a/Sources/Time/4-Fixed Values/Fixed+Codable.swift +++ b/Sources/Time/4-Fixed Values/Fixed+Codable.swift @@ -13,15 +13,17 @@ extension Region: Codable { case locale case calendar } - - #warning("TODO: if the c/l/tz are unchanged from default, only encode the identifiers") public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let calendar = try container.decode(Calendar.self, forKey: .calendar) - let timeZone = try container.decode(TimeZone.self, forKey: .timeZone) - let locale = try container.decode(Locale.self, forKey: .locale) - self.init(calendar: calendar, timeZone: timeZone, locale: locale) + + let calendarContainer = try container.decode(CodableCalendar.self, forKey: .calendar) + let timeZoneContainer = try container.decode(CodableTimeZone.self, forKey: .timeZone) + let localeContainer = try container.decode(CodableLocale.self, forKey: .locale) + + self.init(calendar: calendarContainer.calendar, + timeZone: timeZoneContainer.timeZone, + locale: localeContainer.locale) } public func encode(to encoder: Encoder) throws { @@ -62,3 +64,84 @@ extension Fixed: Codable { try container.encode(instant, forKey: .value) } } + +private struct CodableLocale: Codable { + + let locale: Locale + + init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + let identifier = try container.decode(String.self) + self.locale = Locale.standard(identifier) + } catch { + self.locale = try Locale(from: decoder) + } + } + + func encode(to encoder: Encoder) throws { + let standard = Locale.standard(locale.identifier) + + if standard.isEquivalent(to: locale) { + var single = encoder.singleValueContainer() + try single.encode(locale.identifier) + } else { + try locale.encode(to: encoder) + } + } + +} + +private struct CodableTimeZone: Codable { + + let timeZone: TimeZone + + init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + let identifier = try container.decode(String.self) + self.timeZone = TimeZone.standard(identifier) + } catch { + self.timeZone = try TimeZone(from: decoder) + } + } + + func encode(to encoder: Encoder) throws { + let standard = TimeZone.standard(timeZone.identifier) + + if standard.isEquivalent(to: timeZone) { + var single = encoder.singleValueContainer() + try single.encode(timeZone.identifier) + } else { + try timeZone.encode(to: encoder) + } + } + +} + +private struct CodableCalendar: Codable { + + let calendar: Calendar + + init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + let identifier = try container.decode(Calendar.Identifier.self) + self.calendar = Calendar(identifier: identifier) + } catch { + self.calendar = try Calendar(from: decoder) + } + } + + func encode(to encoder: Encoder) throws { + let standard = Calendar.standard(calendar.identifier) + + if standard.isEquivalent(to: calendar) { + var single = encoder.singleValueContainer() + try single.encode(calendar.identifier) + } else { + try calendar.encode(to: encoder) + } + } + +} diff --git a/Sources/Time/Internals/DateFormatterCache.swift b/Sources/Time/Internals/DateFormatterCache.swift index 5cfccbf..768123d 100644 --- a/Sources/Time/Internals/DateFormatterCache.swift +++ b/Sources/Time/Internals/DateFormatterCache.swift @@ -30,6 +30,7 @@ extension DateFormatter { internal static func formatter(for templates: Array, region: Region) -> DateFormatter { let template = templates.compactMap { $0?.template }.joined() + if template.isEmpty { fatalError("Somehow have an empty template? this should not happen") } return self.formatter(for: .init(configuration: .template(template), region: region)) } } @@ -38,6 +39,7 @@ private class DateFormatterCache { static let shared = DateFormatterCache() + #warning("TODO: better locking primitive?") private let queue = DispatchQueue(label: "DateFormatterCache") private var formatters = Dictionary() diff --git a/Sources/Time/Internals/Region+Equivalence.swift b/Sources/Time/Internals/Region+Equivalence.swift new file mode 100644 index 0000000..78c3d7e --- /dev/null +++ b/Sources/Time/Internals/Region+Equivalence.swift @@ -0,0 +1,53 @@ +// +// File.swift +// +// +// Created by Dave DeLong on 2/11/24. +// + +import Foundation + +extension Calendar { + + func isEquivalent(to other: Calendar) -> Bool { + guard identifier == other.identifier else { return false } + guard timeZone.isEquivalent(to: other.timeZone) else { return false } + guard firstWeekday == other.firstWeekday else { return false } + guard minimumDaysInFirstWeek == other.minimumDaysInFirstWeek else { return false } + + return true + } + +} + +extension TimeZone { + + func isEquivalent(to other: TimeZone) -> Bool { + guard identifier == other.identifier else { return false } + + return true + } + +} + +extension Locale { + + func isEquivalent(to other: Locale) -> Bool { + guard calendar.identifier == other.calendar.identifier else { return false } + guard collation == other.collation else { return false } + guard currency == other.currency else { return false } + guard firstDayOfWeek == other.firstDayOfWeek else { return false } + guard hourCycle == other.hourCycle else { return false } + + guard measurementSystem == other.measurementSystem else { return false } + guard numberingSystem == other.numberingSystem else { return false } + guard region == other.region else { return false } + guard subdivision == other.subdivision else { return false } + guard variant == other.variant else { return false } + + guard language == other.language else { return false } + + return true + } + +} diff --git a/Sources/Time/Internals/SimpleCache.swift b/Sources/Time/Internals/SimpleCache.swift new file mode 100644 index 0000000..3fc13d9 --- /dev/null +++ b/Sources/Time/Internals/SimpleCache.swift @@ -0,0 +1,57 @@ +// +// File.swift +// +// +// Created by Dave DeLong on 2/11/24. +// + +import Foundation + +extension Locale { + private static let cache = SimpleCache() + + static func standard(_ id: String) -> Locale { + return cache.get(id, create: { Locale(identifier: id) }) + } +} + +extension Calendar { + private static let cache = SimpleCache() + + static func standard(_ id: Calendar.Identifier) -> Calendar { + return cache.get(id, create: { Calendar(identifier: id) }) + } +} + +extension TimeZone { + private static let cache = SimpleCache() + + static func standard(_ id: String) -> TimeZone { + return cache.get(id, create: { TimeZone(identifier: id)! }) + } +} + +private class SimpleCache { + + private var storage = Dictionary() + + #warning("TODO: better synchronization primitive?") + private let lock = NSLock() + + init() { } + + func get(_ id: Key, create: () -> T) -> T { + lock.lock() + + let returnValue: T + if let existing = storage[id] { + returnValue = existing + } else { + returnValue = create() + storage[id] = returnValue + } + + lock.unlock() + return returnValue + } +} diff --git a/Sources/Time/Internals/Snapshot.swift b/Sources/Time/Internals/Snapshot.swift index fcce0f2..83bb69e 100644 --- a/Sources/Time/Internals/Snapshot.swift +++ b/Sources/Time/Internals/Snapshot.swift @@ -11,6 +11,10 @@ extension Locale { private static let currentSnapshot: Snapshot = Snapshot(notification: NSLocale.currentLocaleDidChangeNotification, createSnapshot: { let auto = Locale.autoupdatingCurrent + + let standard = Locale.standard(auto.identifier) + if auto.isEquivalent(to: standard) { return standard } + var components = Locale.Components() components.calendar = auto.calendar.identifier @@ -41,7 +45,7 @@ extension Locale { extension TimeZone { private static let currentSnapshot: Snapshot = Snapshot(notification: .NSSystemTimeZoneDidChange, createSnapshot: { - return TimeZone(identifier: TimeZone.autoupdatingCurrent.identifier)! + return TimeZone.standard(TimeZone.autoupdatingCurrent.identifier) }) func snapshot() -> Self { @@ -55,6 +59,10 @@ extension Calendar { private static let currentSnapshot: Snapshot = Snapshot(notification: NSLocale.currentLocaleDidChangeNotification, createSnapshot: { let auto = Calendar.autoupdatingCurrent + + let standard = Calendar.standard(auto.identifier) + if auto.isEquivalent(to: standard) { return standard } + var snapshot = Calendar(identifier: auto.identifier) // don't bother snapshotting the timezone and locale, @@ -82,6 +90,7 @@ private class Snapshot { private var _snapshot: T? private var observationToken: NSObjectProtocol? + #warning("TODO: better synchronization primitive?") private let lock = NSLock() var snapshot: T {