diff --git a/Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift b/Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift index dbdb0ea57..553dac52a 100644 --- a/Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift +++ b/Benchmarks/Benchmarks/Formatting/BenchmarkFormatting.swift @@ -12,6 +12,7 @@ import Benchmark import func Benchmark.blackHole +import Dispatch #if os(macOS) && USE_PACKAGE import FoundationEssentials @@ -61,5 +62,35 @@ let benchmarks = { } } + Benchmark("parallel-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in + for _ in benchmark.scaledIterations { + DispatchQueue.concurrentPerform(iterations: 1000) { _ in + let result = 10.123.formatted() + blackHole(result) + } + } + } + + Benchmark("parallel-and-serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in + for _ in benchmark.scaledIterations { + DispatchQueue.concurrentPerform(iterations: 10) { _ in + // Reuse the values on this thread a bunch + for _ in 0..<100 { + let result = 10.123.formatted() + blackHole(result) + } + } + } + } + + Benchmark("serialized-number-formatting", configuration: .init(scalingFactor: .kilo)) { benchmark in + for _ in benchmark.scaledIterations { + for _ in 0..<1000 { + let result = 10.123.formatted() + blackHole(result) + } + } + } + #endif // swift(>=6) } diff --git a/Sources/FoundationEssentials/Calendar/Calendar.swift b/Sources/FoundationEssentials/Calendar/Calendar.swift index ba62e8199..35bd03ed5 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar.swift @@ -323,7 +323,7 @@ public struct Calendar : Hashable, Equatable, Sendable { /// /// - note: The autoupdating Calendar will only compare equal to another autoupdating Calendar. public static var autoupdatingCurrent : Calendar { - Calendar(inner: CalendarCache.cache.autoupdatingCurrent) + Calendar(inner: CalendarCache.autoupdatingCurrent) } // MARK: - diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Cache.swift b/Sources/FoundationEssentials/Calendar/Calendar_Cache.swift index 97de542c6..f72bb5ef6 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Cache.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Cache.swift @@ -34,103 +34,73 @@ func _calendarClass(identifier: Calendar.Identifier, useGregorian: Bool) -> _Cal } /// Singleton which listens for notifications about preference changes for Calendar and holds cached singletons for the current locale, calendar, and time zone. -struct CalendarCache : Sendable { +struct CalendarCache : Sendable, ~Copyable { // MARK: - State - struct State : Sendable { - // If nil, the calendar has been invalidated and will be created next time State.current() is called - private var currentCalendar: (any _CalendarProtocol)? - private var autoupdatingCurrentCalendar: _CalendarAutoupdating? - private var fixedCalendars: [Calendar.Identifier: any _CalendarProtocol] = [:] - - private var noteCount = -1 - private var wasResetManually = false - - mutating func check() { -#if FOUNDATION_FRAMEWORK - // On Darwin we listen for certain distributed notifications to reset the current Calendar. - let newNoteCount = _CFLocaleGetNoteCount() + _CFTimeZoneGetNoteCount() + Int(_CFCalendarGetMidnightNoteCount()) -#else - let newNoteCount = 1 -#endif - if newNoteCount != noteCount || wasResetManually { - // rdar://102017659 - // Don't create `currentCalendar` here to avoid deadlocking when retrieving a fixed - // calendar. Creating the current calendar gets the current locale, decodes a plist - // from CFPreferences, and may call +[NSDate initialize] on a separate thread. This - // leads to a deadlock if we are also initializing a class on the current thread - currentCalendar = nil - fixedCalendars = [:] - - noteCount = newNoteCount - wasResetManually = false - } - } - - mutating func current() -> any _CalendarProtocol { - check() - if let currentCalendar { - return currentCalendar - } else { - let id = Locale.current._calendarIdentifier - // If we cannot create the right kind of class, we fail immediately here - let calendarClass = _calendarClass(identifier: id, useGregorian: true)! - let calendar = calendarClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil) - currentCalendar = calendar - return calendar - } + static let cache = CalendarCache() + + // The values stored in these two locks do not depend upon each other, so it is safe to access them with separate locks. This helps avoids contention on a single lock. + + private let _current = LockedState<(any _CalendarProtocol)?>(initialState: nil) + private let _fixed = LockedState<[Calendar.Identifier: any _CalendarProtocol]>(initialState: [:]) + + fileprivate init() { + } + + var current: any _CalendarProtocol { + if let result = _current.withLock({ $0 }) { + return result } + + let id = Locale.current._calendarIdentifier + // If we cannot create the right kind of class, we fail immediately here + let calendarClass = _calendarClass(identifier: id, useGregorian: true)! + let calendar = calendarClass.init(identifier: id, timeZone: nil, locale: Locale.current, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil) - mutating func autoupdatingCurrent() -> any _CalendarProtocol { - if let autoupdatingCurrentCalendar { - return autoupdatingCurrentCalendar + return _current.withLock { + if let current = $0 { + // Someone beat us to setting it - use the existing one + return current } else { - let calendar = _CalendarAutoupdating() - autoupdatingCurrentCalendar = calendar + $0 = calendar return calendar } } - - mutating func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol { - check() - if let cached = fixedCalendars[id] { - return cached - } else { - // If we cannot create the right kind of class, we fail immediately here - let calendarClass = _calendarClass(identifier: id, useGregorian: true)! - let new = calendarClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil) - fixedCalendars[id] = new - return new - } - } - - mutating func reset() { - wasResetManually = true - } - } - - let lock: LockedState - - static let cache = CalendarCache() - - fileprivate init() { - lock = LockedState(initialState: State()) } - + func reset() { - lock.withLock { $0.reset() } - } - - var current: any _CalendarProtocol { - lock.withLock { $0.current() } + // rdar://102017659 + // Don't create `currentCalendar` here to avoid deadlocking when retrieving a fixed + // calendar. Creating the current calendar gets the current locale, decodes a plist + // from CFPreferences, and may call +[NSDate initialize] on a separate thread. This + // leads to a deadlock if we are also initializing a class on the current thread + _current.withLock { $0 = nil } + _fixed.withLock { $0 = [:] } } - var autoupdatingCurrent: any _CalendarProtocol { - lock.withLock { $0.autoupdatingCurrent() } - } + // MARK: Singletons + + static let autoupdatingCurrent = _CalendarAutoupdating() + + // MARK: - func fixed(_ id: Calendar.Identifier) -> any _CalendarProtocol { - lock.withLock { $0.fixed(id) } + if let existing = _fixed.withLock({ $0[id] }) { + return existing + } + + // If we cannot create the right kind of class, we fail immediately here + let calendarClass = _calendarClass(identifier: id, useGregorian: true)! + let new = calendarClass.init(identifier: id, timeZone: nil, locale: nil, firstWeekday: nil, minimumDaysInFirstWeek: nil, gregorianStartDate: nil) + + return _fixed.withLock { + if let existing = $0[id] { + return existing + } else { + $0[id] = new + return new + } + } } func fixed(identifier: Calendar.Identifier, locale: Locale?, timeZone: TimeZone?, firstWeekday: Int?, minimumDaysInFirstWeek: Int?, gregorianStartDate: Date?) -> any _CalendarProtocol { diff --git a/Sources/FoundationEssentials/Formatting/FormatterCache.swift b/Sources/FoundationEssentials/Formatting/FormatterCache.swift index cc4d18c3e..01653e9ae 100644 --- a/Sources/FoundationEssentials/Formatting/FormatterCache.swift +++ b/Sources/FoundationEssentials/Formatting/FormatterCache.swift @@ -11,7 +11,6 @@ //===----------------------------------------------------------------------===// package struct FormatterCache: Sendable { - let countLimit = 100 private let _lock: LockedState<[Format: FormattingType]> diff --git a/Sources/FoundationEssentials/Locale/Locale.swift b/Sources/FoundationEssentials/Locale/Locale.swift index 2a81137a2..1ef7f1978 100644 --- a/Sources/FoundationEssentials/Locale/Locale.swift +++ b/Sources/FoundationEssentials/Locale/Locale.swift @@ -65,7 +65,7 @@ public struct Locale : Hashable, Equatable, Sendable { /// /// - note: The autoupdating Locale will only compare equal to another autoupdating Locale. public static var autoupdatingCurrent : Locale { - Locale(inner: LocaleCache.cache.autoupdatingCurrent) + Locale(inner: LocaleCache.autoupdatingCurrent) } /// Returns the user's current locale. @@ -75,12 +75,12 @@ public struct Locale : Hashable, Equatable, Sendable { /// System locale. internal static var system : Locale { - Locale(inner: LocaleCache.cache.system) + Locale(inner: LocaleCache.system) } /// Unlocalized locale (`en_001`). internal static var unlocalized : Locale { - Locale(inner: LocaleCache.cache.unlocalized) + Locale(inner: LocaleCache.unlocalized) } #if FOUNDATION_FRAMEWORK && canImport(_FoundationICU) diff --git a/Sources/FoundationEssentials/Locale/Locale_Autoupdating.swift b/Sources/FoundationEssentials/Locale/Locale_Autoupdating.swift index 9cff2b260..337711663 100644 --- a/Sources/FoundationEssentials/Locale/Locale_Autoupdating.swift +++ b/Sources/FoundationEssentials/Locale/Locale_Autoupdating.swift @@ -263,7 +263,7 @@ internal final class _LocaleAutoupdating : _LocaleProtocol, @unchecked Sendable } func bridgeToNSLocale() -> NSLocale { - LocaleCache.cache.autoupdatingCurrentNSLocale() + LocaleCache.autoupdatingCurrentNSLocale } #endif diff --git a/Sources/FoundationEssentials/Locale/Locale_Cache.swift b/Sources/FoundationEssentials/Locale/Locale_Cache.swift index 892795d76..e3cda86f1 100644 --- a/Sources/FoundationEssentials/Locale/Locale_Cache.swift +++ b/Sources/FoundationEssentials/Locale/Locale_Cache.swift @@ -32,19 +32,22 @@ dynamic package func _localeICUClass() -> _LocaleProtocol.Type { #endif /// Singleton which listens for notifications about preference changes for Locale and holds cached singletons. -struct LocaleCache : Sendable { +struct LocaleCache : Sendable, ~Copyable { // MARK: - State struct State { - private var cachedCurrentLocale: (any _LocaleProtocol)! - private var cachedSystemLocale: (any _LocaleProtocol)! + + init() { +#if FOUNDATION_FRAMEWORK + // For Foundation.framework, we listen for system notifications about the system Locale changing from the Darwin notification center. + _CFNotificationCenterInitializeDependentNotificationIfNecessary(CFNotificationName.cfLocaleCurrentLocaleDidChange!.rawValue) +#endif + } + private var cachedFixedLocales: [String : any _LocaleProtocol] = [:] private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol] = [:] #if FOUNDATION_FRAMEWORK - private var cachedCurrentNSLocale: _NSSwiftLocale! - private var cachedAutoupdatingNSLocale: _NSSwiftLocale! - private var cachedSystemNSLocale: _NSSwiftLocale! private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:] struct IdentifierAndPrefs : Hashable { @@ -54,66 +57,7 @@ struct LocaleCache : Sendable { private var cachedFixedLocaleToNSLocales: [IdentifierAndPrefs : _NSSwiftLocale] = [:] #endif - - private var cachedAutoupdatingLocale: _LocaleAutoupdating! - - private var noteCount = -1 - private var wasResetManually = false - - /// Clears the cached `Locale` values, if they need to be recalculated. - mutating func resetCurrentIfNeeded() { -#if FOUNDATION_FRAMEWORK - let newNoteCount = _CFLocaleGetNoteCount() + _CFTimeZoneGetNoteCount() + Int(_CFCalendarGetMidnightNoteCount()) -#else - let newNoteCount = 1 -#endif - - if newNoteCount != noteCount || wasResetManually { - cachedCurrentLocale = nil - noteCount = newNoteCount - wasResetManually = false - -#if FOUNDATION_FRAMEWORK - cachedCurrentNSLocale = nil - // For Foundation.framework, we listen for system notifications about the system Locale changing from the Darwin notification center. - _CFNotificationCenterInitializeDependentNotificationIfNecessary(CFNotificationName.cfLocaleCurrentLocaleDidChange!.rawValue) -#endif - } - } - - /// Get or create the current locale. - /// `disableBundleMatching` should normally be disabled (`false`). The only reason to turn it on (`true`) is if we are attempting to create a testing scenario that does not use the main bundle's languages. - mutating func current(preferences: LocalePreferences?, cache: Bool, disableBundleMatching: Bool) -> (any _LocaleProtocol)? { - resetCurrentIfNeeded() - - if let cachedCurrentLocale { - return cachedCurrentLocale - } - - // At this point we know we need to create, or re-create, the Locale instance. - // If we do not have a set of preferences to use, we have to return nil. - guard let preferences else { - return nil - } - - let locale = _localeICUClass().init(name: nil, prefs: preferences, disableBundleMatching: disableBundleMatching) - if cache { - // It's possible this was an 'incomplete locale', in which case we will want to calculate it again later. - self.cachedCurrentLocale = locale - } - - return locale - } - - mutating func autoupdatingCurrent() -> _LocaleAutoupdating { - if let cached = cachedAutoupdatingLocale { - return cached - } else { - cachedAutoupdatingLocale = _LocaleAutoupdating() - return cachedAutoupdatingLocale - } - } - + mutating func fixed(_ id: String) -> any _LocaleProtocol { // Note: Even if the currentLocale's identifier is the same, currentLocale may have preference overrides which are not reflected in the identifier itself. if let locale = cachedFixedLocales[id] { @@ -153,65 +97,14 @@ struct LocaleCache : Sendable { } #endif - mutating func currentNSLocale(preferences: LocalePreferences?, cache: Bool) -> _NSSwiftLocale? { - resetCurrentIfNeeded() - - if let currentNSLocale = cachedCurrentNSLocale { - return currentNSLocale - } else if let current = cachedCurrentLocale { - // We have a cached Swift Locale but not an NSLocale, yet - let nsLocale = _NSSwiftLocale(Locale(inner: current)) - cachedCurrentNSLocale = nsLocale - return nsLocale - } - - // At this point we know we need to create, or re-create, the Locale instance. - - // If we do not have a set of preferences to use, we have to return nil. - guard let preferences else { - return nil - } - -#if canImport(_FoundationICU) - // We have neither a Swift Locale nor an NSLocale. Recalculate and set both. - let locale = _LocaleICU(name: nil, prefs: preferences, disableBundleMatching: false) -#else - let locale = _LocaleUnlocalized(name: nil, prefs: preferences, disableBundleMatching: false) -#endif - let nsLocale = _NSSwiftLocale(Locale(inner: locale)) - - if cache { - // It's possible this was an 'incomplete locale', in which case we will want to calculate it again later. - self.cachedCurrentLocale = locale - cachedCurrentNSLocale = nsLocale - } - - return nsLocale - } - - mutating func autoupdatingNSLocale() -> _NSSwiftLocale { - if let result = cachedAutoupdatingNSLocale { - return result - } - - // Don't call Locale.autoupdatingCurrent directly to avoid a recursive lock - cachedAutoupdatingNSLocale = _NSSwiftLocale(Locale(inner: autoupdatingCurrent())) - return cachedAutoupdatingNSLocale - } - - mutating func systemNSLocale() -> _NSSwiftLocale { - if let result = cachedSystemNSLocale { - return result - } - - let inner = Locale(inner: system()) - cachedSystemNSLocale = _NSSwiftLocale(inner) - return cachedSystemNSLocale - } #endif // FOUNDATION_FRAMEWORK - mutating func fixedComponents(_ comps: Locale.Components) -> any _LocaleProtocol { - if let l = cachedFixedComponentsLocales[comps] { + func fixedComponents(_ comps: Locale.Components) -> (any _LocaleProtocol)? { + cachedFixedComponentsLocales[comps] + } + + mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol { + if let l = fixedComponents(comps) { return l } else { let new = _localeICUClass().init(components: comps) @@ -220,81 +113,95 @@ struct LocaleCache : Sendable { return new } } - - mutating func system() -> any _LocaleProtocol { - if let locale = cachedSystemLocale { - return locale - } - - let locale = _localeICUClass().init(identifier: "", prefs: nil) - cachedSystemLocale = locale - return locale - } - - mutating func reset() { - wasResetManually = true - } } let lock: LockedState - + static let cache = LocaleCache() - + private let _currentCache = LockedState<(any _LocaleProtocol)?>(initialState: nil) + +#if FOUNDATION_FRAMEWORK + private var _currentNSCache = LockedState<_NSSwiftLocale?>(initialState: nil) +#endif + fileprivate init() { lock = LockedState(initialState: State()) } - func reset() { - lock.withLock { $0.reset() } - } - + /// For testing of `autoupdatingCurrent` only. If you want to test `current`, create a custom `Locale` with the appropriate settings using `localeAsIfCurrent(name:overrides:disableBundleMatching:)` and use that instead. /// This mutates global state of the current locale, so it is not safe to use in concurrent testing. func resetCurrent(to preferences: LocalePreferences) { - lock.withLock { - $0.reset() - // Disable bundle matching so we can emulate a non-English main bundle during test - let _ = $0.current(preferences: preferences, cache: true, disableBundleMatching: true) + // Disable bundle matching so we can emulate a non-English main bundle during test + let newLocale = _localeICUClass().init(name: nil, prefs: preferences, disableBundleMatching: true) + _currentCache.withLock { + $0 = newLocale } +#if FOUNDATION_FRAMEWORK + _currentNSCache.withLock { $0 = nil } +#endif + } + + func reset() { + _currentCache.withLock { $0 = nil } +#if FOUNDATION_FRAMEWORK + _currentNSCache.withLock { $0 = nil } +#endif } var current: any _LocaleProtocol { - var result = lock.withLock { - $0.current(preferences: nil, cache: false, disableBundleMatching: false) + if let result = _currentCache.withLock({ $0 }) { + return result } - if let result { return result } - // We need to fetch prefs and try again - let (prefs, doCache) = preferences() - - result = lock.withLock { - $0.current(preferences: prefs, cache: doCache, disableBundleMatching: false) - } + let (preferences, doCache) = preferences() + let locale = _localeICUClass().init(name: nil, prefs: preferences, disableBundleMatching: false) - guard let result else { - fatalError("Nil result getting current Locale with preferences") + // It's possible this was an 'incomplete locale', in which case we will want to calculate it again later. + if doCache { + return _currentCache.withLock { + if let current = $0 { + // Someone beat us to setting it - use existing one + return current + } else { + $0 = locale + return locale + } + } } - return result + return locale } - /// This value is immutable, so we can share one instance for the whole process. - private static let _unlocalizedCache = _LocaleUnlocalized(identifier: "en_001") - var unlocalized: _LocaleUnlocalized { - Self._unlocalizedCache - } + // MARK: Singletons - var autoupdatingCurrent: _LocaleAutoupdating { - lock.withLock { $0.autoupdatingCurrent() } - } + // This value is immutable, so we can share one instance for the whole process. + static let unlocalized = _LocaleUnlocalized(identifier: "en_001") - var system: any _LocaleProtocol { - lock.withLock { $0.system() } - } + // This value is immutable, so we can share one instance for the whole process. + static let autoupdatingCurrent = _LocaleAutoupdating() + static let system : any _LocaleProtocol = { + _localeICUClass().init(identifier: "", prefs: nil) + }() + +#if FOUNDATION_FRAMEWORK + static let autoupdatingCurrentNSLocale : _NSSwiftLocale = { + _NSSwiftLocale(Locale(inner: autoupdatingCurrent)) + }() + + static let systemNSLocale : _NSSwiftLocale = { + _NSSwiftLocale(Locale(inner: system)) + }() +#endif + + // MARK: - + func fixed(_ id: String) -> any _LocaleProtocol { - lock.withLock { $0.fixed(id) } + lock.withLock { + $0.fixed(id) + } } #if FOUNDATION_FRAMEWORK @@ -308,38 +215,30 @@ struct LocaleCache : Sendable { } #endif - func autoupdatingCurrentNSLocale() -> _NSSwiftLocale { - lock.withLock { $0.autoupdatingNSLocale() } - } - func currentNSLocale() -> _NSSwiftLocale { - var result = lock.withLock { - $0.currentNSLocale(preferences: nil, cache: false) - } - - if let result { return result } - - // We need to fetch prefs and try again. Don't do this inside a lock (106190030). On Darwin it is possible to get a KVO callout from fetching the preferences, which could ask for the current Locale, which could cause a reentrant lock. - let (prefs, doCache) = preferences() - - result = lock.withLock { - $0.currentNSLocale(preferences: prefs, cache: doCache) + if let result = _currentNSCache.withLock({ $0 }) { + return result } - guard let result else { - fatalError("Nil result getting current NSLocale with preferences") + // Create the current _NSSwiftLocale, based on the current Swift Locale. + let nsLocale = _NSSwiftLocale(Locale(inner: current)) + + // TODO: The current locale has an idea of not caching, which we have never honored here in the NSLocale cache + return _currentNSCache.withLock { + if let current = $0 { + // Someone beat us to setting it, use that one + return current + } else { + $0 = nsLocale + return nsLocale + } } - - return result } - func systemNSLocale() -> _NSSwiftLocale { - lock.withLock { $0.systemNSLocale() } - } #endif // FOUNDATION_FRAMEWORK func fixedComponents(_ comps: Locale.Components) -> any _LocaleProtocol { - lock.withLock { $0.fixedComponents(comps) } + lock.withLock { $0.fixedComponentsWithCache(comps) } } #if FOUNDATION_FRAMEWORK && !NO_CFPREFERENCES diff --git a/Sources/FoundationEssentials/Locale/Locale_Notifications.swift b/Sources/FoundationEssentials/Locale/Locale_Notifications.swift new file mode 100644 index 000000000..35133d3e2 --- /dev/null +++ b/Sources/FoundationEssentials/Locale/Locale_Notifications.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Synchronization) && FOUNDATION_FRAMEWORK +internal import Synchronization +#endif + +/// Keeps a global generation count for updated Locale information, including locale, time zone, and calendar preferences. +/// If any of those preferences change, then `count` will update to a new value. Compare that to a cached value to see if your cached `Locale.current`, `TimeZone.current`, or `Calendar.current` to see if it is out of date. +/// If any cached values need to be recalculated process-wide, call `reset`. +struct LocaleNotifications : Sendable, ~Copyable { + static let cache = LocaleNotifications() + +#if canImport(Synchronization) && FOUNDATION_FRAMEWORK + let _count = Atomic(1) +#else + let _count = LockedState(initialState: 1) +#endif + + func count() -> Int { +#if canImport(Synchronization) && FOUNDATION_FRAMEWORK + _count.load(ordering: .relaxed) +#else + _count.withLock { $0 } +#endif + } + + /// Make a new generation current, but no associated Locale. + func reset() { + LocaleCache.cache.reset() + CalendarCache.cache.reset() + _ = TimeZoneCache.cache.reset() +#if canImport(Synchronization) && FOUNDATION_FRAMEWORK + _count.add(1, ordering: .relaxed) +#else + _count.withLock { $0 += 1 } +#endif + } +} + +#if FOUNDATION_FRAMEWORK +@_cdecl("_localeNotificationCount") +func _localeNotificationCount() -> Int { + LocaleNotifications.cache.count() +} +#endif + diff --git a/Sources/FoundationEssentials/LockedState.swift b/Sources/FoundationEssentials/LockedState.swift index 8f3ad7fef..ac7539f1b 100644 --- a/Sources/FoundationEssentials/LockedState.swift +++ b/Sources/FoundationEssentials/LockedState.swift @@ -155,3 +155,4 @@ extension LockedState where State == Void { } extension LockedState: @unchecked Sendable where State: Sendable {} + diff --git a/Sources/FoundationEssentials/String/String+Internals.swift b/Sources/FoundationEssentials/String/String+Internals.swift index 07383af4a..bf787911b 100644 --- a/Sources/FoundationEssentials/String/String+Internals.swift +++ b/Sources/FoundationEssentials/String/String+Internals.swift @@ -52,7 +52,11 @@ extension String { extension String { package func _trimmingWhitespace() -> String { - String(unicodeScalars._trimmingCharacters { + if self.isEmpty { + return "" + } + + return String(unicodeScalars._trimmingCharacters { $0.properties.isWhitespace }) } diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone.swift b/Sources/FoundationEssentials/TimeZone/TimeZone.swift index d924312e5..053b1030c 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone.swift @@ -116,7 +116,7 @@ public struct TimeZone : Hashable, Equatable, Sendable { #endif /// The time zone currently used by the system. - public static var current : TimeZone { + public static var current: TimeZone { TimeZone(inner: TimeZoneCache.cache.current._tz) } @@ -125,8 +125,8 @@ public struct TimeZone : Hashable, Equatable, Sendable { /// If this time zone is mutated, then it no longer tracks the system time zone. /// /// The autoupdating time zone only compares equal to itself. - public static var autoupdatingCurrent : TimeZone { - TimeZone(inner: TimeZoneCache.cache.autoupdatingCurrent()) + public static var autoupdatingCurrent: TimeZone { + TimeZone(inner: TimeZoneCache.cache.autoupdatingCurrent) } /// The default time zone, settable via ObjC but not available in Swift API (because it's global mutable state). @@ -405,7 +405,7 @@ extension TimeZone { internal static func resetSystemTimeZone() -> TimeZone? { let oldTimeZone = TimeZoneCache.cache.reset() // Also reset the calendar cache, since the current calendar uses the current time zone - CalendarCache.cache.reset() + LocaleNotifications.cache.reset() return oldTimeZone } diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift index 72bbe1703..bb940cbda 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift @@ -51,14 +51,19 @@ dynamic package func _timeZoneGMTClass() -> _TimeZoneProtocol.Type { #endif /// Singleton which listens for notifications about preference changes for TimeZone and holds cached values for current, fixed time zones, etc. -struct TimeZoneCache : Sendable { +struct TimeZoneCache : Sendable, ~Copyable { // MARK: - State struct State { + + init() { +#if FOUNDATION_FRAMEWORK + // On Darwin we listen for certain distributed notifications to reset the current TimeZone. + _CFNotificationCenterInitializeDependentNotificationIfNecessary(CFNotificationName.cfTimeZoneSystemTimeZoneDidChange!.rawValue) +#endif + } // a.k.a. `systemTimeZone` - private var currentTimeZone: TimeZone! - - private var autoupdatingCurrentTimeZone: _TimeZoneAutoupdating! + private var currentTimeZone: TimeZone? // If this is not set, the behavior is to fall back to the current time zone private var defaultTimeZone: TimeZone? @@ -69,44 +74,24 @@ struct TimeZoneCache : Sendable { // This cache holds offset-specified time zones, but only a subset of the universe of possible values. See the implementation below for the policy. private var offsetTimeZones: [Int: any _TimeZoneProtocol] = [:] - private var noteCount = -1 private var identifiers: [String]? private var abbreviations: [String : String]? #if FOUNDATION_FRAMEWORK // These are caches of the NSTimeZone subclasses for use from Objective-C (without allocating each time) - private var bridgedCurrentTimeZone: _NSSwiftTimeZone! - private var bridgedAutoupdatingCurrentTimeZone: _NSSwiftTimeZone! + private var bridgedCurrentTimeZone: _NSSwiftTimeZone? private var bridgedDefaultTimeZone: _NSSwiftTimeZone? private var bridgedFixedTimeZones: [String : _NSSwiftTimeZone] = [:] private var bridgedOffsetTimeZones: [Int : _NSSwiftTimeZone] = [:] #endif // FOUNDATION_FRAMEWORK - - mutating func check() { -#if FOUNDATION_FRAMEWORK - // On Darwin we listen for certain distributed notifications to reset the current TimeZone. - let newNoteCount = _CFLocaleGetNoteCount() + _CFTimeZoneGetNoteCount() + Int(_CFCalendarGetMidnightNoteCount()) -#else - let newNoteCount = 1 -#endif // FOUNDATION_FRAMEWORK - - if newNoteCount != noteCount { - currentTimeZone = findCurrentTimeZone() - noteCount = newNoteCount -#if FOUNDATION_FRAMEWORK - bridgedCurrentTimeZone = _NSSwiftTimeZone(timeZone: currentTimeZone) - _CFNotificationCenterInitializeDependentNotificationIfNecessary(CFNotificationName.cfTimeZoneSystemTimeZoneDidChange!.rawValue) -#endif // FOUNDATION_FRAMEWORK - } - } mutating func reset() -> TimeZone? { let oldTimeZone = currentTimeZone - // Ensure we do not reuse the existing time zone - noteCount = -1 - check() - + currentTimeZone = nil +#if FOUNDATION_FRAMEWORK + bridgedCurrentTimeZone = nil +#endif return oldTimeZone } @@ -194,23 +179,24 @@ struct TimeZoneCache : Sendable { } mutating func current() -> TimeZone { - check() - return currentTimeZone + if let currentTimeZone { + return currentTimeZone + } else { + let newCurrent = findCurrentTimeZone() + currentTimeZone = newCurrent + return newCurrent + } } mutating func `default`() -> TimeZone { - check() if let manuallySetDefault = defaultTimeZone { return manuallySetDefault } else { - return currentTimeZone + return current() } } - mutating func setDefaultTimeZone(_ tz: TimeZone?) -> TimeZone? { - // Ensure we are listening for notifications from here on out - check() - let old = defaultTimeZone + mutating func setDefaultTimeZone(_ tz: TimeZone?) { defaultTimeZone = tz #if FOUNDATION_FRAMEWORK if let tz { @@ -219,7 +205,6 @@ struct TimeZoneCache : Sendable { bridgedDefaultTimeZone = nil } #endif // FOUNDATION_FRAMEWORK - return old } mutating func fixed(_ identifier: String) -> (any _TimeZoneProtocol)? { @@ -255,15 +240,6 @@ struct TimeZoneCache : Sendable { } } - mutating func autoupdatingCurrent() -> _TimeZoneAutoupdating { - if let cached = autoupdatingCurrentTimeZone { - return cached - } else { - autoupdatingCurrentTimeZone = _TimeZoneAutoupdating() - return autoupdatingCurrentTimeZone - } - } - mutating func timeZoneAbbreviations() -> [String : String] { if abbreviations == nil { abbreviations = defaultAbbreviations @@ -332,28 +308,20 @@ struct TimeZoneCache : Sendable { // MARK: - State Bridging #if FOUNDATION_FRAMEWORK mutating func bridgedCurrent() -> _NSSwiftTimeZone { - check() - return bridgedCurrentTimeZone - } - - mutating func bridgedAutoupdatingCurrent() -> _NSSwiftTimeZone { - if let autoupdating = bridgedAutoupdatingCurrentTimeZone { - return autoupdating + if let bridgedCurrentTimeZone { + return bridgedCurrentTimeZone } else { - // Do not call TimeZone.autoupdatingCurrent, as it will recursively lock. - let tz = TimeZone(inner: autoupdatingCurrent()) - let result = _NSSwiftTimeZone(timeZone: tz) - bridgedAutoupdatingCurrentTimeZone = result - return result + let newBridged = _NSSwiftTimeZone(timeZone: current()) + bridgedCurrentTimeZone = newBridged + return newBridged } } mutating func bridgedDefault() -> _NSSwiftTimeZone { - check() if let manuallySetDefault = bridgedDefaultTimeZone { return manuallySetDefault } else { - return bridgedCurrentTimeZone + return bridgedCurrent() } } @@ -433,19 +401,10 @@ struct TimeZoneCache : Sendable { } func setDefault(_ tz: TimeZone?) { - let oldDefaultTimeZone = lock.withLock { - return $0.setDefaultTimeZone(tz) - } + lock.withLock { $0.setDefaultTimeZone(tz) } - CalendarCache.cache.reset() -#if FOUNDATION_FRAMEWORK - if let oldDefaultTimeZone { - let noteName = CFNotificationName(rawValue: "kCFTimeZoneSystemTimeZoneDidChangeNotification-2" as CFString) - let oldAsNS = oldDefaultTimeZone as NSTimeZone - let unmanaged = Unmanaged.passRetained(oldAsNS).autorelease() - CFNotificationCenterPostNotification(CFNotificationCenterGetLocalCenter(), noteName, unmanaged.toOpaque(), nil, true) - } -#endif // FOUNDATION_FRAMEWORK + // Reset any 'current' locales, calendars, time zones + LocaleNotifications.cache.reset() } func fixed(_ identifier: String) -> _TimeZoneProtocol? { @@ -456,8 +415,9 @@ struct TimeZoneCache : Sendable { lock.withLock { $0.offsetFixed(seconds) } } - func autoupdatingCurrent() -> _TimeZoneAutoupdating { - lock.withLock { $0.autoupdatingCurrent() } + private static let _autoupdatingCurrentCache = _TimeZoneAutoupdating() + var autoupdatingCurrent: _TimeZoneAutoupdating { + return Self._autoupdatingCurrentCache } func timeZoneAbbreviations() -> [String : String] { @@ -474,8 +434,9 @@ struct TimeZoneCache : Sendable { lock.withLock { $0.bridgedCurrent() } } + private static let _bridgedAutoupdatingCurrent = _NSSwiftTimeZone(timeZone: TimeZone(inner: TimeZoneCache.cache.autoupdatingCurrent)) var bridgedAutoupdatingCurrent: _NSSwiftTimeZone { - lock.withLock { $0.bridgedAutoupdatingCurrent() } + Self._bridgedAutoupdatingCurrent } var bridgedDefault: _NSSwiftTimeZone { diff --git a/Sources/FoundationInternationalization/Calendar/Calendar_ObjC.swift b/Sources/FoundationInternationalization/Calendar/Calendar_ObjC.swift index 4b3ca1e36..e5867deab 100644 --- a/Sources/FoundationInternationalization/Calendar/Calendar_ObjC.swift +++ b/Sources/FoundationInternationalization/Calendar/Calendar_ObjC.swift @@ -75,11 +75,6 @@ extension NSCalendar { } return _NSSwiftCalendar(calendar: Calendar(identifier: id)) } - - @objc - class func _resetCurrent() { - CalendarCache.cache.reset() - } } // MARK: - diff --git a/Sources/FoundationInternationalization/Formatting/Number/ICUNumberFormatter.swift b/Sources/FoundationInternationalization/Formatting/Number/ICUNumberFormatter.swift index a0c40c0a3..ed288d189 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/ICUNumberFormatter.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/ICUNumberFormatter.swift @@ -33,7 +33,7 @@ internal class ICUNumberFormatterBase : @unchecked Sendable { /// Stored for testing purposes only internal let skeleton: String - init?(skeleton: String, localeIdentifier: String, preferences: LocalePreferences?) { + init?(skeleton: String, localeIdentifier: String) { self.skeleton = skeleton let ustr = Array(skeleton.utf16) var status = U_ZERO_ERROR @@ -222,29 +222,28 @@ internal class ICUNumberFormatterBase : @unchecked Sendable { final class ICUNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable { fileprivate struct Signature : Hashable { - let collection: NumberFormatStyleConfiguration.Collection + let skeleton: String let localeIdentifier: String - let localePreferences: LocalePreferences? } fileprivate static let cache = FormatterCache() private static func _create(with signature: Signature) -> ICUNumberFormatter? { Self.cache.formatter(for: signature) { - .init(skeleton: signature.collection.skeleton, localeIdentifier: signature.localeIdentifier, preferences: signature.localePreferences) + .init(skeleton: signature.skeleton, localeIdentifier: signature.localeIdentifier) } } static func create(for style: IntegerFormatStyle) -> ICUNumberFormatter? { - _create(with: .init(collection: style.collection, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences)) } static func create(for style: Decimal.FormatStyle) -> ICUNumberFormatter? { - _create(with: .init(collection: style.collection, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences)) } static func create(for style: FloatingPointFormatStyle) -> ICUNumberFormatter? { - _create(with: .init(collection: style.collection, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences)) } func attributedFormat(_ v: Value) -> AttributedString { @@ -259,16 +258,15 @@ final class ICUNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable { final class ICUCurrencyNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable { fileprivate struct Signature : Hashable { - let collection: CurrencyFormatStyleConfiguration.Collection + let skeleton: String let currencyCode: String let localeIdentifier: String - let localePreferences: LocalePreferences? } private static func skeleton(for signature: Signature) -> String { var s = "currency/\(signature.currencyCode)" - let stem = signature.collection.skeleton + let stem = signature.skeleton if stem.count > 0 { s += " " + stem } @@ -280,20 +278,20 @@ final class ICUCurrencyNumberFormatter : ICUNumberFormatterBase, @unchecked Send static private func _create(with signature: Signature) -> ICUCurrencyNumberFormatter? { return Self.cache.formatter(for: signature) { - .init(skeleton: Self.skeleton(for: signature), localeIdentifier: signature.localeIdentifier, preferences: signature.localePreferences) + .init(skeleton: Self.skeleton(for: signature), localeIdentifier: signature.localeIdentifier) } } static func create(for style: IntegerFormatStyle.Currency) -> ICUCurrencyNumberFormatter? { - _create(with: .init(collection: style.collection, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences)) } static func create(for style: Decimal.FormatStyle.Currency) -> ICUCurrencyNumberFormatter? { - _create(with: .init(collection: style.collection, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences)) } static func create(for style: FloatingPointFormatStyle.Currency) -> ICUCurrencyNumberFormatter? { - _create(with: .init(collection: style.collection, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences)) } func attributedFormat(_ v: Value) -> AttributedString { @@ -308,14 +306,13 @@ final class ICUCurrencyNumberFormatter : ICUNumberFormatterBase, @unchecked Send final class ICUPercentNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable { fileprivate struct Signature : Hashable { - let collection: NumberFormatStyleConfiguration.Collection + let skeleton: String let localeIdentifier: String - let localePreferences: LocalePreferences? } private static func skeleton(for signature: Signature) -> String { var s = "percent" - let stem = signature.collection.skeleton + let stem = signature.skeleton if stem.count > 0 { s += " " + stem } @@ -326,20 +323,20 @@ final class ICUPercentNumberFormatter : ICUNumberFormatterBase, @unchecked Senda private static func _create(with signature: Signature) -> ICUPercentNumberFormatter? { return Self.cache.formatter(for: signature) { - .init(skeleton: Self.skeleton(for: signature), localeIdentifier: signature.localeIdentifier, preferences: signature.localePreferences) + .init(skeleton: Self.skeleton(for: signature), localeIdentifier: signature.localeIdentifier) } } static func create(for style: IntegerFormatStyle.Percent) -> ICUPercentNumberFormatter? { - _create(with: .init(collection: style.collection, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences)) } static func create(for style: Decimal.FormatStyle.Percent) -> ICUPercentNumberFormatter? { - _create(with: .init(collection: style.collection, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences)) } static func create(for style: FloatingPointFormatStyle.Percent) -> ICUPercentNumberFormatter? { - _create(with: .init(collection: style.collection, localeIdentifier: style.locale.identifierCapturingPreferences, localePreferences: style.locale.prefs)) + _create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences)) } func attributedFormat(_ v: Value) -> AttributedString { @@ -356,15 +353,14 @@ final class ICUByteCountNumberFormatter : ICUNumberFormatterBase, @unchecked Sen fileprivate struct Signature : Hashable { let skeleton: String let localeIdentifier: String - let localePreferences: LocalePreferences? } fileprivate static let cache = FormatterCache() static func create(for skeleton: String, locale: Locale) -> ICUByteCountNumberFormatter? { - let signature = Signature(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences, localePreferences: locale.prefs) + let signature = Signature(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences) return Self.cache.formatter(for: signature) { - .init(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences, preferences: locale.prefs) + .init(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences) } } @@ -415,15 +411,14 @@ final class ICUMeasurementNumberFormatter : ICUNumberFormatterBase, @unchecked S fileprivate struct Signature : Hashable { let skeleton: String let localeIdentifier: String - let localePreferences: LocalePreferences? } fileprivate static let cache = FormatterCache() static func create(for skeleton: String, locale: Locale) -> ICUMeasurementNumberFormatter? { - let signature = Signature(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences, localePreferences: locale.prefs) + let signature = Signature(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences) return Self.cache.formatter(for: signature) { - .init(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences, preferences: locale.prefs) + .init(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences) } } diff --git a/Sources/FoundationInternationalization/Locale/Locale_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale_ICU.swift index d26fb71aa..b35b101a9 100644 --- a/Sources/FoundationInternationalization/Locale/Locale_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale_ICU.swift @@ -41,7 +41,6 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { // Single-optional values are caches where the result may not be nil. If the value is nil, the result has not yet been calculated. struct State: Hashable, Sendable { var languageComponents: Locale.Language.Components? - var calendarId: Calendar.Identifier? var collation: Locale.Collation? var currency: Locale.Currency?? var numberingSystem: Locale.NumberingSystem? @@ -56,7 +55,6 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { var subdivision: Locale.Subdivision?? var timeZone: TimeZone?? var variant: Locale.Variant?? - var identifierCapturingPreferences: String? // If the key is present, the value has been calculated (and the result may or may not be nil). var identifierDisplayNames: [String : String?] = [:] @@ -131,6 +129,8 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { // MARK: - ivar let identifier: String + let identifierCapturingPreferences: String + let calendarIdentifier: Calendar.Identifier let doesNotRequireSpecialCaseHandling: Bool @@ -159,6 +159,8 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { self.identifier = Locale._canonicalLocaleIdentifier(from: identifier) doesNotRequireSpecialCaseHandling = Locale.identifierDoesNotRequireSpecialCaseHandling(self.identifier) self.prefs = prefs + calendarIdentifier = Self._calendarIdentifier(forIdentifier: self.identifier) + identifierCapturingPreferences = Self._identifierCapturingPreferences(forIdentifier: self.identifier, calendarIdentifier: calendarIdentifier, preferences: prefs) lock = LockedState(initialState: State()) } @@ -166,12 +168,12 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { self.identifier = components.icuIdentifier doesNotRequireSpecialCaseHandling = Locale.identifierDoesNotRequireSpecialCaseHandling(self.identifier) prefs = nil + calendarIdentifier = Self._calendarIdentifier(forIdentifier: self.identifier) + identifierCapturingPreferences = Self._identifierCapturingPreferences(forIdentifier: self.identifier, calendarIdentifier: calendarIdentifier, preferences: prefs) // Copy over the component values into our internal state - if they are set var state = State() state.languageComponents = components.languageComponents - if let v = components.calendar { state.calendarId = v } - if let v = components.calendar { state.calendarId = v } if let v = components.collation { state.collation = v } if let v = components.currency { state.currency = v } if let v = components.numberingSystem { state.numberingSystem = v } @@ -284,6 +286,8 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { self.identifier = Locale._canonicalLocaleIdentifier(from: fixedIdent) doesNotRequireSpecialCaseHandling = Locale.identifierDoesNotRequireSpecialCaseHandling(self.identifier) self.prefs = prefs + calendarIdentifier = Self._calendarIdentifier(forIdentifier: self.identifier) + identifierCapturingPreferences = Self._identifierCapturingPreferences(forIdentifier: self.identifier, calendarIdentifier: calendarIdentifier, preferences: prefs) lock = LockedState(initialState: State()) } @@ -432,42 +436,33 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { // // Intentionally ignore `prefs.country`: Locale identifier should already contain // that information. Do not override it. - var identifierCapturingPreferences: String { - lock.withLock { state in - if let result = state.identifierCapturingPreferences { - return result - } - - guard let prefs else { - state.identifierCapturingPreferences = identifier - return identifier - } - - var components = Locale.Components(identifier: identifier) - - if let id = prefs.collationOrder { - components.collation = .init(id) - } - - if let firstWeekdayPrefs = prefs.firstWeekday { - let calendarID = _lockedCalendarIdentifier(&state) - if let weekdayNumber = firstWeekdayPrefs[calendarID], let weekday = Locale.Weekday(Int32(weekdayNumber)) { - components.firstDayOfWeek = weekday - } - } - - if let measurementSystem = prefs.measurementSystem { - components.measurementSystem = measurementSystem - } - - if let hourCycle = prefs.hourCycle { - components.hourCycle = hourCycle + static func _identifierCapturingPreferences(forIdentifier identifier: String, calendarIdentifier: Calendar.Identifier, preferences prefs: LocalePreferences?) -> String { + guard let prefs else { + return identifier + } + + var components = Locale.Components(identifier: identifier) + + if let id = prefs.collationOrder { + components.collation = .init(id) + } + + if let firstWeekdayPrefs = prefs.firstWeekday { + let calendarID = calendarIdentifier + if let weekdayNumber = firstWeekdayPrefs[calendarID], let weekday = Locale.Weekday(Int32(weekdayNumber)) { + components.firstDayOfWeek = weekday } - - let completeID = components.icuIdentifier - state.identifierCapturingPreferences = completeID - return completeID } + + if let measurementSystem = prefs.measurementSystem { + components.measurementSystem = measurementSystem + } + + if let hourCycle = prefs.hourCycle { + components.hourCycle = hourCycle + } + + return components.icuIdentifier } // MARK: - Language Code @@ -720,46 +715,31 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { // MARK: - LocaleCalendarIdentifier - private func _lockedCalendarIdentifier(_ state: inout State) -> Calendar.Identifier { - if let calendarId = state.calendarId { - return calendarId - } else { - var calendarIDString = Locale.keywordValue(identifier: identifier, key: "calendar") - if calendarIDString == nil { - // Try again - var status = U_ZERO_ERROR - let e = ucal_getKeywordValuesForLocale("calendar", identifier, UBool.true, &status) - defer { uenum_close(e) } - guard let e, status.isSuccess else { - state.calendarId = .gregorian - return .gregorian - } - // Just get the first value - var resultLength = Int32(0) - let result = uenum_next(e, &resultLength, &status) - guard status.isSuccess, let result else { - state.calendarId = .gregorian - return .gregorian - } - calendarIDString = String(cString: result) + private static func _calendarIdentifier(forIdentifier identifier: String) -> Calendar.Identifier { + var calendarIDString = Locale.keywordValue(identifier: identifier, key: "calendar") + if calendarIDString == nil { + // Try again + var status = U_ZERO_ERROR + let e = ucal_getKeywordValuesForLocale("calendar", identifier, UBool.true, &status) + defer { uenum_close(e) } + guard let e, status.isSuccess else { + return .gregorian } - - guard let calendarIDString else { - // Fallback value - state.calendarId = .gregorian + // Just get the first value + var resultLength = Int32(0) + let result = uenum_next(e, &resultLength, &status) + guard status.isSuccess, let result else { return .gregorian } - - let id = Calendar.Identifier(identifierString: calendarIDString) ?? .gregorian - state.calendarId = id - return id + calendarIDString = String(cString: result) } - } - - var calendarIdentifier: Calendar.Identifier { - lock.withLock { state in - _lockedCalendarIdentifier(&state) + + guard let calendarIDString else { + // Fallback value + return .gregorian } + + return Calendar.Identifier(identifierString: calendarIDString) ?? .gregorian } func calendarIdentifierDisplayName(for value: Calendar.Identifier) -> String? { @@ -780,20 +760,17 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { // MARK: - LocaleCalendar var calendar: Calendar { - lock.withLock { state in - let id = _lockedCalendarIdentifier(&state) - var calendar = Calendar(identifier: id) - - if let prefs { - let firstWeekday = prefs.firstWeekday?[id] - let minDaysInFirstWeek = prefs.minDaysInFirstWeek?[id] - if let firstWeekday { calendar.firstWeekday = firstWeekday } - if let minDaysInFirstWeek { calendar.minimumDaysInFirstWeek = minDaysInFirstWeek } - } - - // In order to avoid a retain cycle (Calendar has a Locale, Locale has a Calendar), we do not keep a reference to the Calendar in Locale but create one each time. Most of the time the value of `Calendar(identifier:)` will return a cached value in any case. - return calendar + var calendar = Calendar(identifier: calendarIdentifier) + + if let prefs { + let firstWeekday = prefs.firstWeekday?[calendarIdentifier] + let minDaysInFirstWeek = prefs.minDaysInFirstWeek?[calendarIdentifier] + if let firstWeekday { calendar.firstWeekday = firstWeekday } + if let minDaysInFirstWeek { calendar.minimumDaysInFirstWeek = minDaysInFirstWeek } } + + // In order to avoid a retain cycle (Calendar has a Locale, Locale has a Calendar), we do not keep a reference to the Calendar in Locale but create one each time. Most of the time the value of `Calendar(identifier:)` will return a cached value in any case. + return calendar } var timeZone: TimeZone? { @@ -1185,7 +1162,7 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { return hourCycle } - let calendarId = _lockedCalendarIdentifier(&state) + let calendarId = calendarIdentifier let rootHourCycle = Locale.HourCycle.zeroToTwentyThree if let regionOverride = _lockedRegion(&state)?.identifier { // Use the "rg" override in the identifier if there's one @@ -1239,8 +1216,7 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { // Check prefs if let firstWeekdayPref = prefs?.firstWeekday { - // `_lockedCalendarIdentifier` isn't cheap. Only call it when we already know there is `prefs` to read from - let calendarId = _lockedCalendarIdentifier(&state) + let calendarId = calendarIdentifier if let first = forceFirstWeekday(calendarId) { state.firstDayOfWeek = first return first @@ -1398,7 +1374,7 @@ internal final class _LocaleICU: _LocaleProtocol, Sendable { // Check prefs if prefs != nil { // `_lockedCalendarIdentifier` isn't cheap. Only call it when we already know there is `prefs` to read from - let calendarId = _lockedCalendarIdentifier(&state) + let calendarId = calendarIdentifier if let minDays = forceMinDaysInFirstWeek(calendarId) { state.minimalDaysInFirstWeek = minDays return minDays diff --git a/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift b/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift index 35abc28d6..b61659d73 100644 --- a/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift +++ b/Sources/FoundationInternationalization/Locale/Locale_ObjC.swift @@ -27,7 +27,7 @@ internal import Foundation_Private.NSLocale extension NSLocale { @objc static var _autoupdatingCurrent: NSLocale { - LocaleCache.cache.autoupdatingCurrentNSLocale() + LocaleCache.autoupdatingCurrentNSLocale } @objc @@ -37,7 +37,7 @@ extension NSLocale { @objc static var _system: NSLocale { - LocaleCache.cache.systemNSLocale() + LocaleCache.systemNSLocale } @objc @@ -65,7 +65,7 @@ extension NSLocale { @objc private class func _resetCurrent() { - LocaleCache.cache.reset() + LocaleNotifications.cache.reset() } @objc