Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions Sources/FoundationEssentials/Calendar/Calendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1616,19 +1616,12 @@ extension Calendar : Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

if let current = try container.decodeIfPresent(Current.self, forKey: .current) {
switch current {
case .autoupdatingCurrent:
self = Calendar.autoupdatingCurrent
return
case .current:
self = Calendar.current
return
case .fixed:
// Fall through to identifier-based
break
}
if let current = try container.decodeIfPresent(Current.self, forKey: .current), current == .autoupdatingCurrent {
self = Calendar.autoupdatingCurrent
return
}

// Just like TimeZone and Locale, Whether the calendar was fixed or current we decode as a fixed calendar if it wasn't encoded as the sentinel autoupdating current

let identifierString = try container.decode(String.self, forKey: .identifier)
// Same as NSCalendar.Identifier
Expand All @@ -1655,7 +1648,8 @@ extension Calendar : Codable {
try container.encode(self.firstWeekday, forKey: .firstWeekday)
try container.encode(self.minimumDaysInFirstWeek, forKey: .minimumDaysInFirstWeek)

// current and autoupdatingCurrent are sentinel values. Calendar could theoretically not treat 'current' as a sentinel, but it is required for Locale (one of the properties of Calendar), so transitively we have to do the same here
// autoupdatingCurrent is a sentinel value
// Prior to FoundationPreview 6.3 releases, Calendar treated current-equivalent calendars as sentinel values while decoding as well. As of FoundationPreview 6.2 releases, Calendar no longer decodes the current sentinel value, but it is still encoded to preserve behavior when decoding with older runtimes
if self == Calendar.autoupdatingCurrent {
try container.encode(Current.autoupdatingCurrent, forKey: .current)
} else if self == Calendar.current {
Expand Down
29 changes: 26 additions & 3 deletions Sources/FoundationEssentials/Locale/Locale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ public struct Locale : Hashable, Equatable, Sendable {
// On Darwin, this overrides are applied on top of CFPreferences.
return LocaleCache.cache.localeAsIfCurrent(name: name, overrides: overrides, disableBundleMatching: disableBundleMatching)
}

internal static func localeWithPreferences(identifier: String, preferences: LocalePreferences) -> Locale {
return LocaleCache.cache.localeWithPreferences(identifier: identifier, prefs: preferences)
}

internal static func localeAsIfCurrentWithBundleLocalizations(_ availableLocalizations: [String], allowsMixedLocalizations: Bool) -> Locale? {
return LocaleCache.cache.localeAsIfCurrentWithBundleLocalizations(availableLocalizations, allowsMixedLocalizations: allowsMixedLocalizations)
Expand Down Expand Up @@ -814,6 +818,7 @@ extension Locale : Codable {
private enum CodingKeys : Int, CodingKey {
case identifier
case current
case preferences
}

// CFLocale enforces a rule that fixed/current/autoupdatingCurrent can never be equal even if their values seem like they are the same
Expand All @@ -825,23 +830,35 @@ extension Locale : Codable {

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let prefs = try container.decodeIfPresent(LocalePreferences.self, forKey: .preferences)

if let current = try container.decodeIfPresent(Current.self, forKey: .current) {
switch current {
case .autoupdatingCurrent:
self = Locale.autoupdatingCurrent
return
case .current:
self = Locale.current
return
if prefs == nil {
// Prior to FoundationPreview 6.3 releases, Locale did not encode preferences and expected decoding .current to decode with the new current user's preferences via the new process' .currrent locale
// Preserve behavior for encoded current locales without encoded preferences by decoding as the current locale here to preserve the intent of including user preferences even though preferences are not included in the archive
// If preferences were encoded (the current locale encoded from a post-FoundationPreview 6.3 release), fallthrough to the new behavior below
self = Locale.current
return
}
case .fixed:
// Fall through to identifier-based
break
}
}

let identifier = try container.decode(String.self, forKey: .identifier)
self.init(identifier: identifier)
if let prefs {
// If preferences were encoded, create a locale with the preferences and identifier (not including preferences from the current user)
self = Locale.localeWithPreferences(identifier: identifier, preferences: prefs)
} else {
// If no preferences were encoded, create a fixed locale with just the identifier
self.init(identifier: identifier)
}
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -852,9 +869,15 @@ extension Locale : Codable {
if self == Locale.autoupdatingCurrent {
try container.encode(Current.autoupdatingCurrent, forKey: .current)
} else if self == Locale.current {
// Always encode .current for the current locale to preserve existing decoding behavior of .current when decoding on older runtimes prior to FoundationPreview 6.3 releases
try container.encode(Current.current, forKey: .current)
} else {
try container.encode(Current.fixed, forKey: .current)
}

if let prefs {
// Encode preferences (if present) so that when decoding on newer runtimes (FoundationPreview 6.3 releases and later) we create a locale with the preferences as they are at encode time
try container.encode(prefs, forKey: .preferences)
}
}
}
201 changes: 197 additions & 4 deletions Sources/FoundationEssentials/Locale/Locale_Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal import _ForSwiftFoundation

/// Holds user preferences about `Locale`, retrieved from user defaults. It is only used when creating the `current` Locale. Fixed-identifier locales never have preferences.
package struct LocalePreferences: Hashable, Sendable {
package enum MeasurementUnit {
package enum MeasurementUnit: Int, Codable {
case centimeters
case inches

Expand All @@ -38,7 +38,7 @@ package struct LocalePreferences: Hashable, Sendable {
}
}

package enum TemperatureUnit {
package enum TemperatureUnit: Int, Codable {
case fahrenheit
case celsius

Expand Down Expand Up @@ -66,7 +66,7 @@ package struct LocalePreferences: Hashable, Sendable {
package var firstWeekday: [Calendar.Identifier : Int]?
package var minDaysInFirstWeek: [Calendar.Identifier : Int]?
#if FOUNDATION_FRAMEWORK
struct ICUSymbolsAndStrings : Hashable, @unchecked Sendable {
package struct ICUSymbolsAndStrings : Hashable, @unchecked Sendable {
// The following `CFDictionary` ivars are used directly by `CFDateFormatter`. Keep them as `CFDictionary` to avoid bridging them into and out of Swift. We don't need to access them from Swift at all.

package var icuDateTimeSymbols: CFDictionary?
Expand All @@ -91,6 +91,8 @@ package struct LocalePreferences: Hashable, Sendable {
package var temperatureUnit: TemperatureUnit?
package var force24Hour: Bool?
package var force12Hour: Bool?

// Note: When adding new preferences, be sure to include them in the serialized format via the Codable conformance below

package init() { }

Expand All @@ -108,7 +110,8 @@ package struct LocalePreferences: Hashable, Sendable {
force24Hour: Bool? = nil,
force12Hour: Bool? = nil,
numberSymbols: [UInt32 : String]? = nil,
dateFormats: [Date.FormatStyle.DateStyle: String]? = nil) {
dateFormats: [Date.FormatStyle.DateStyle: String]? = nil,
icuSymbolsAndStrings: ICUSymbolsAndStrings = ICUSymbolsAndStrings()) {

self.metricUnits = metricUnits
self.languages = languages
Expand All @@ -123,6 +126,7 @@ package struct LocalePreferences: Hashable, Sendable {
self.force12Hour = force12Hour
self.numberSymbols = numberSymbols
self.dateFormats = dateFormats
self.icuSymbolsAndStrings = icuSymbolsAndStrings
}
#else
package init(metricUnits: Bool? = nil,
Expand Down Expand Up @@ -331,3 +335,192 @@ package struct LocalePreferences: Hashable, Sendable {
}
}
}

extension LocalePreferences: Codable {
private enum CodingKeys: String, CodingKey {
case metricUnits = "metric"
case languages = "langs"
case locale
case collationOrder = "coll"
case firstWeekday = "weekFirst"
case minDaysInFirstWeek = "weekMin"
case icuSymbolsAndStrings = "icu"
case dateFormats = "dates"
case numberSymbols = "nums"
case country
case measurementUnits = "meas"
case temperatureUnit = "temp"
case force24Hour = "24h"
case force12Hour = "12h"
}

package func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(metricUnits, forKey: .metricUnits)
try container.encodeIfPresent(languages, forKey: .languages)
try container.encodeIfPresent(locale, forKey: .locale)
try container.encodeIfPresent(collationOrder, forKey: .collationOrder)
let firstWeekdayMapped = self.firstWeekday.map {
var result = [String : Int]()
for (identifier, day) in $0 {
result[identifier.cfCalendarIdentifier] = day
}
return result
}
try container.encodeIfPresent(firstWeekdayMapped, forKey: .firstWeekday)
let minDaysInFirstWeekMapped = self.minDaysInFirstWeek.map {
var result = [String : Int]()
for (identifier, days) in $0 {
result[identifier.cldrIdentifier] = days
}
return result
}
try container.encodeIfPresent(minDaysInFirstWeekMapped, forKey: .minDaysInFirstWeek)
#if FOUNDATION_FRAMEWORK
if icuSymbolsAndStrings.containsValuesToSerialize {
try container.encodeIfPresent(icuSymbolsAndStrings, forKey: .icuSymbolsAndStrings)
}
#if !NO_FORMATTERS
try container.encodeIfPresent(dateFormats.map {
var result = [UInt : String]()
for (format, value) in $0 {
result[format.rawValue] = value
}
return result
}, forKey: .dateFormats)
#endif
#endif
try container.encodeIfPresent(numberSymbols, forKey: .numberSymbols)
try container.encodeIfPresent(country, forKey: .country)
try container.encodeIfPresent(measurementUnits, forKey: .measurementUnits)
try container.encodeIfPresent(temperatureUnit, forKey: .temperatureUnit)
try container.encodeIfPresent(force24Hour, forKey: .force24Hour)
try container.encodeIfPresent(force12Hour, forKey: .force12Hour)
}

package init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let metricUnits = try container.decodeIfPresent(Bool.self, forKey: .metricUnits)
let languages = try container.decodeIfPresent([String].self, forKey: .languages)
let locale = try container.decodeIfPresent(String.self, forKey: .locale)
let collationOrder = try container.decodeIfPresent(String.self, forKey: .collationOrder)
let firstWeekday = try container.decodeIfPresent([String : Int].self, forKey: .firstWeekday).map {
var result = [Calendar.Identifier : Int]()
for (stringIdentifier, day) in $0 {
guard let identifier = Calendar.Identifier(identifierString: stringIdentifier) else {
throw DecodingError.dataCorruptedError(forKey: .firstWeekday, in: container, debugDescription: "Unknown calendar identifier '\(stringIdentifier)'")
}
result[identifier] = day
}
return result
}
let minDaysInFirstWeek = try container.decodeIfPresent([String : Int].self, forKey: .minDaysInFirstWeek).map {
var result = [Calendar.Identifier : Int]()
for (stringIdentifier, days) in $0 {
guard let identifier = Calendar.Identifier(identifierString: stringIdentifier) else {
throw DecodingError.dataCorruptedError(forKey: .minDaysInFirstWeek, in: container, debugDescription: "Unknown calendar identifier '\(stringIdentifier)'")
}
result[identifier] = days
}
return result
}
let country = try container.decodeIfPresent(String.self, forKey: .country)
let measurementUnits = try container.decodeIfPresent(MeasurementUnit.self, forKey: .measurementUnits)
let temperatureUnit = try container.decodeIfPresent(TemperatureUnit.self, forKey: .temperatureUnit)
let force24Hour = try container.decodeIfPresent(Bool.self, forKey: .force24Hour)
let force12Hour = try container.decodeIfPresent(Bool.self, forKey: .force12Hour)
let numberSymbols = try container.decodeIfPresent([UInt32 : String].self, forKey: .numberSymbols)

#if FOUNDATION_FRAMEWORK && !NO_FORMATTERS
let dateFormats = try container.decodeIfPresent([UInt: String].self, forKey: .dateFormats).map {
var result = [Date.FormatStyle.DateStyle : String]()
for (rawValue, format) in $0 {
result[Date.FormatStyle.DateStyle(rawValue: rawValue)] = format
}
return result
}
let icuDateFormats = dateFormats.map {
var cfResult = [String : String]()
for (style, format) in $0 {
cfResult["\(style.rawValue)"] = format
}
return cfResult as CFDictionary
}
let icuNumberSymbols = numberSymbols.map {
var result = [String : String]()
for (rawValue, symbol) in $0 {
result["\(rawValue)"] = symbol
}
return result as CFDictionary
}
var icuSymbolsAndStrings = try container.decodeIfPresent(ICUSymbolsAndStrings.self, forKey: .icuSymbolsAndStrings) ?? ICUSymbolsAndStrings()
icuSymbolsAndStrings.icuDateFormatStrings = icuDateFormats
icuSymbolsAndStrings.icuNumberSymbols = icuNumberSymbols

self.init(
metricUnits: metricUnits,
languages: languages,
locale: locale,
collationOrder: collationOrder,
firstWeekday: firstWeekday,
minDaysInFirstWeek: minDaysInFirstWeek,
country: country,
measurementUnits: measurementUnits,
temperatureUnit: temperatureUnit,
force24Hour: force24Hour,
force12Hour: force12Hour,
numberSymbols: numberSymbols,
dateFormats: dateFormats,
icuSymbolsAndStrings: icuSymbolsAndStrings
)
#else
self.init(
metricUnits: metricUnits,
languages: languages,
locale: locale,
collationOrder: collationOrder,
firstWeekday: firstWeekday,
minDaysInFirstWeek: minDaysInFirstWeek,
country: country,
measurementUnits: measurementUnits,
temperatureUnit: temperatureUnit,
force24Hour: force24Hour,
force12Hour: force12Hour,
numberSymbols: numberSymbols
)
#endif
}
}

#if FOUNDATION_FRAMEWORK
extension LocalePreferences.ICUSymbolsAndStrings: Codable {
var containsValuesToSerialize: Bool {
icuDateTimeSymbols != nil || icuTimeFormatStrings != nil || icuNumberFormatStrings != nil
}

private enum CodingKeys: String, CodingKey {
case icuDateTimeSymbols = "dtSym"
case icuTimeFormatStrings = "times"
case icuNumberFormatStrings = "nums"
}

package func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(icuDateTimeSymbols.map { $0 as? [String : [String]] }, forKey: .icuDateTimeSymbols)
try container.encodeIfPresent(icuTimeFormatStrings.map { $0 as? [String : String] }, forKey: .icuTimeFormatStrings)
try container.encodeIfPresent(icuNumberFormatStrings.map { $0 as? [String : String] }, forKey: .icuNumberFormatStrings)
}

package init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.icuDateTimeSymbols = try container.decodeIfPresent([String : [String]].self, forKey: .icuDateTimeSymbols).map { $0 as CFDictionary }
self.icuTimeFormatStrings = try container.decodeIfPresent([String : String].self, forKey: .icuTimeFormatStrings).map { $0 as CFDictionary }
self.icuNumberFormatStrings = try container.decodeIfPresent([String : String].self, forKey: .icuNumberFormatStrings).map { $0 as CFDictionary }

// Will be filled in by LocalePreferences serialized value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this comment. I was indeed confused above why these two are handled separately from the others.

self.icuDateFormatStrings = nil
self.icuNumberSymbols = nil
}
}
#endif
3 changes: 3 additions & 0 deletions Sources/FoundationEssentials/TimeZone/TimeZone.swift
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ extension TimeZone : Codable {
var container = encoder.container(keyedBy: CodingKeys.self)
// Even if we are autoupdatingCurrent, encode the identifier for backward compatibility
try container.encode(self.identifier, forKey: .identifier)

// Autoupdating current timezones are treated as sentinel values, but the current TimeZone is encoded as a fixed TimeZone
// This is the same behavior as Locale/Calendar except it did not previously encode as a sentinel value before FoundationPreview 6.3, so no extra key is encoded for the current time zone
if _tz.isAutoupdating {
try container.encode(true, forKey: .autoupdating)
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ struct TimeZoneCache : Sendable, ~Copyable {
#endif
return oldTimeZone
}

mutating func resetCurrent(to newValue: TimeZone) {
currentTimeZone = newValue
#if FOUNDATION_FRAMEWORK
bridgedCurrentTimeZone = nil
#endif
}

/// Reads from environment variables `TZFILE`, `TZ` and finally the symlink pointed at by the C macro `TZDEFAULT` to figure out what the current (aka "system") time zone is.
mutating func findCurrentTimeZone() -> TimeZone {
Expand Down Expand Up @@ -398,6 +405,10 @@ struct TimeZoneCache : Sendable, ~Copyable {
func reset() -> TimeZone? {
return lock.withLock { $0.reset() }
}

func resetCurrent(to newValue: TimeZone) {
return lock.withLock { $0.resetCurrent(to: newValue) }
}

var current: TimeZone {
lock.withLock { $0.current() }
Expand Down
Loading