diff --git a/Sources/FoundationEssentials/Calendar/Calendar.swift b/Sources/FoundationEssentials/Calendar/Calendar.swift index a35488120..74b44748d 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar.swift @@ -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 @@ -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 { diff --git a/Sources/FoundationEssentials/Locale/Locale.swift b/Sources/FoundationEssentials/Locale/Locale.swift index 3c0620674..836df169f 100644 --- a/Sources/FoundationEssentials/Locale/Locale.swift +++ b/Sources/FoundationEssentials/Locale/Locale.swift @@ -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) @@ -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 @@ -825,6 +830,7 @@ 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 { @@ -832,8 +838,13 @@ extension Locale : Codable { 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 @@ -841,7 +852,13 @@ extension Locale : Codable { } 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 { @@ -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) + } } } diff --git a/Sources/FoundationEssentials/Locale/Locale_Preferences.swift b/Sources/FoundationEssentials/Locale/Locale_Preferences.swift index 10ee5114d..c8f7a5443 100644 --- a/Sources/FoundationEssentials/Locale/Locale_Preferences.swift +++ b/Sources/FoundationEssentials/Locale/Locale_Preferences.swift @@ -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 @@ -38,7 +38,7 @@ package struct LocalePreferences: Hashable, Sendable { } } - package enum TemperatureUnit { + package enum TemperatureUnit: Int, Codable { case fahrenheit case celsius @@ -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? @@ -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() { } @@ -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 @@ -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, @@ -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 + self.icuDateFormatStrings = nil + self.icuNumberSymbols = nil + } +} +#endif diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone.swift b/Sources/FoundationEssentials/TimeZone/TimeZone.swift index c28f7abcd..491bf5b80 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone.swift @@ -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) } diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift index 1bb33343c..7bb5288b2 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone_Cache.swift @@ -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 { @@ -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() } diff --git a/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift b/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift index 6f784c8b0..cd38e1d37 100644 --- a/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift +++ b/Tests/FoundationEssentialsTests/GregorianCalendarRecurrenceRuleTests.swift @@ -26,6 +26,42 @@ private struct GregorianCalendarRecurrenceRuleTests { gregorian.timeZone = .gmt return gregorian }() + + @Test func roundtripEncoding() throws { + // These are not necessarily valid recurrence rule, they are constructed + // in a way to test all encoding paths + var calendar = Calendar(identifier: .gregorian) + calendar.locale = .init(identifier: "en_001") + calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) + + var recurrenceRule1 = Calendar.RecurrenceRule(calendar: calendar, frequency: .daily) + recurrenceRule1.interval = 2 + recurrenceRule1.months = [1, 2, Calendar.RecurrenceRule.Month(4, isLeap: true)] + recurrenceRule1.weeks = [2, 3] + recurrenceRule1.weekdays = [.every(.monday), .nth(1, .wednesday)] + recurrenceRule1.end = .afterOccurrences(5) + + var recurrenceRule2 = Calendar.RecurrenceRule(calendar: calendar, frequency: .daily) + recurrenceRule2.months = [2, 10] + recurrenceRule2.weeks = [1, -1] + recurrenceRule2.setPositions = [1] + recurrenceRule2.hours = [14] + recurrenceRule2.minutes = [30] + recurrenceRule2.seconds = [0] + recurrenceRule2.daysOfTheYear = [1] + recurrenceRule2.daysOfTheMonth = [4] + recurrenceRule2.weekdays = [.every(.monday), .nth(1, .wednesday)] + recurrenceRule2.end = .afterDate(.distantFuture) + + let recurrenceRule1JSON = try JSONEncoder().encode(recurrenceRule1) + let recurrenceRule2JSON = try JSONEncoder().encode(recurrenceRule2) + let decoded1 = try JSONDecoder().decode(Calendar.RecurrenceRule.self, from: recurrenceRule1JSON) + let decoded2 = try JSONDecoder().decode(Calendar.RecurrenceRule.self, from: recurrenceRule2JSON) + + #expect(recurrenceRule1 == decoded1) + #expect(recurrenceRule2 == decoded2) + #expect(recurrenceRule1 != recurrenceRule2) + } @Test func simpleDailyRecurrence() { let start = Date(timeIntervalSince1970: 1285027200.0) // 2010-09-21T00:00:00-0000 diff --git a/Tests/FoundationInternationalizationTests/CalendarRecurrenceRuleTests.swift b/Tests/FoundationInternationalizationTests/CalendarRecurrenceRuleTests.swift index d3af14b70..4422e570b 100644 --- a/Tests/FoundationInternationalizationTests/CalendarRecurrenceRuleTests.swift +++ b/Tests/FoundationInternationalizationTests/CalendarRecurrenceRuleTests.swift @@ -28,47 +28,6 @@ private struct CalendarRecurrenceRuleTests { return gregorian }() - @Test func roundtripEncoding() async throws { - // This test does not directly use the current Calendar, however encoding any Calendar will check if it is equivalent to the current Calendar - // If equivalent, it will serialize a sentinel value and deserialize as the current Calendar regardless of the actual serialized identifier - // This test will fail if calendar == Calendar.current at encode time and the current calendar changes to a different value before decode time (making the encoded calendar and the decoded calendar not equivalent - try await usingCurrentInternationalizationPreferences { - // These are not necessarily valid recurrence rule, they are constructed - // in a way to test all encoding paths - var calendar = Calendar(identifier: .gregorian) - calendar.locale = .init(identifier: "en_001") - calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0)) - - var recurrenceRule1 = Calendar.RecurrenceRule(calendar: calendar, frequency: .daily) - recurrenceRule1.interval = 2 - recurrenceRule1.months = [1, 2, Calendar.RecurrenceRule.Month(4, isLeap: true)] - recurrenceRule1.weeks = [2, 3] - recurrenceRule1.weekdays = [.every(.monday), .nth(1, .wednesday)] - recurrenceRule1.end = .afterOccurrences(5) - - var recurrenceRule2 = Calendar.RecurrenceRule(calendar: calendar, frequency: .daily) - recurrenceRule2.months = [2, 10] - recurrenceRule2.weeks = [1, -1] - recurrenceRule2.setPositions = [1] - recurrenceRule2.hours = [14] - recurrenceRule2.minutes = [30] - recurrenceRule2.seconds = [0] - recurrenceRule2.daysOfTheYear = [1] - recurrenceRule2.daysOfTheMonth = [4] - recurrenceRule2.weekdays = [.every(.monday), .nth(1, .wednesday)] - recurrenceRule2.end = .afterDate(.distantFuture) - - let recurrenceRule1JSON = try JSONEncoder().encode(recurrenceRule1) - let recurrenceRule2JSON = try JSONEncoder().encode(recurrenceRule2) - let decoded1 = try JSONDecoder().decode(Calendar.RecurrenceRule.self, from: recurrenceRule1JSON) - let decoded2 = try JSONDecoder().decode(Calendar.RecurrenceRule.self, from: recurrenceRule2JSON) - - #expect(recurrenceRule1 == decoded1) - #expect(recurrenceRule2 == decoded2) - #expect(recurrenceRule1 != recurrenceRule2) - } - } - @Test func yearlyRecurrenceInLunarCalendar() { // Find the first day of the lunar new year let start = Date(timeIntervalSince1970: 1726876800.0) // 2024-09-21T00:00:00-0000 diff --git a/Tests/FoundationInternationalizationTests/CalendarTests.swift b/Tests/FoundationInternationalizationTests/CalendarTests.swift index 3a2d67e74..8f5b3db3a 100644 --- a/Tests/FoundationInternationalizationTests/CalendarTests.swift +++ b/Tests/FoundationInternationalizationTests/CalendarTests.swift @@ -230,6 +230,34 @@ private struct CalendarTests { let decodedModified = try decodeHelper(modified) #expect(decodedModified != autoupdatingCurrent) #expect(modified == decodedModified) + + do { + // Calendar does not decode the current as a sentinel value + var prefs = LocalePreferences() + prefs.languages = ["en-US"] + prefs.locale = "en_US" + prefs.minDaysInFirstWeek = [.gregorian : 5] + LocaleCache.cache.resetCurrent(to: prefs) + CalendarCache.cache.reset() + + let encodedCurrent = try JSONEncoder().encode(Calendar.current) + let encodedAutoupdatingCurrent = try JSONEncoder().encode(Calendar.autoupdatingCurrent) + + prefs = LocalePreferences() + prefs.languages = ["es-ES"] + prefs.locale = "es_ES" + prefs.minDaysInFirstWeek = [.gregorian : 3] + LocaleCache.cache.resetCurrent(to: prefs) + CalendarCache.cache.reset() + + let decodedCurrent = try JSONDecoder().decode(Calendar.self, from: encodedCurrent) + let decodedAutoupdatingCurrent = try JSONDecoder().decode(Calendar.self, from: encodedAutoupdatingCurrent) + + #expect(decodedCurrent.minimumDaysInFirstWeek == 5) + #expect(decodedCurrent.locale?.identifier == "en_US") + #expect(decodedAutoupdatingCurrent.minimumDaysInFirstWeek == 3) + #expect(decodedAutoupdatingCurrent.locale?.identifier == "es_ES") + } } } diff --git a/Tests/FoundationInternationalizationTests/LocaleTests.swift b/Tests/FoundationInternationalizationTests/LocaleTests.swift index 2fa9fe866..00eb7fda0 100644 --- a/Tests/FoundationInternationalizationTests/LocaleTests.swift +++ b/Tests/FoundationInternationalizationTests/LocaleTests.swift @@ -116,6 +116,34 @@ private struct LocaleTests { #expect(current != autoupdatingCurrent) #expect(decodedCurrent != autoupdatingCurrent) #expect(current != decodedAutoupdatingCurrent) + + do { + // Locale does not decode the current as a sentinel value + var prefs = LocalePreferences() + prefs.languages = ["en-US"] + prefs.locale = "en_US" + prefs.minDaysInFirstWeek = [.gregorian : 5] + LocaleCache.cache.resetCurrent(to: prefs) + CalendarCache.cache.reset() + + let encodedCurrent = try JSONEncoder().encode(Locale.current) + let encodedAutoupdatingCurrent = try JSONEncoder().encode(Locale.autoupdatingCurrent) + + prefs = LocalePreferences() + prefs.languages = ["es-ES"] + prefs.locale = "es_ES" + prefs.minDaysInFirstWeek = [.gregorian : 3] + LocaleCache.cache.resetCurrent(to: prefs) + CalendarCache.cache.reset() + + let decodedCurrent = try JSONDecoder().decode(Locale.self, from: encodedCurrent) + let decodedAutoupdatingCurrent = try JSONDecoder().decode(Locale.self, from: encodedAutoupdatingCurrent) + + #expect(decodedCurrent.identifier == "en_US") + #expect(decodedCurrent.prefs?.minDaysInFirstWeek?[.gregorian] == 5) + #expect(decodedAutoupdatingCurrent.identifier == "es_ES") + #expect(decodedAutoupdatingCurrent.prefs?.minDaysInFirstWeek?[.gregorian] == 3) + } } } diff --git a/Tests/FoundationInternationalizationTests/TimeZoneTests.swift b/Tests/FoundationInternationalizationTests/TimeZoneTests.swift index 724b15799..45442fd8b 100644 --- a/Tests/FoundationInternationalizationTests/TimeZoneTests.swift +++ b/Tests/FoundationInternationalizationTests/TimeZoneTests.swift @@ -159,6 +159,22 @@ private struct TimeZoneTests { #expect(current != autoupdatingCurrent) #expect(decodedCurrent != autoupdatingCurrent) #expect(current != decodedAutoupdatingCurrent) + + do { + // TimeZone does not decode the current as a sentinel value + TimeZoneCache.cache.resetCurrent(to: .gmt) + + let encodedCurrent = try JSONEncoder().encode(TimeZone.current) + let encodedAutoupdatingCurrent = try JSONEncoder().encode(TimeZone.autoupdatingCurrent) + + TimeZoneCache.cache.resetCurrent(to: TimeZone(identifier: "America/Los_Angeles")!) + + let decodedCurrent = try JSONDecoder().decode(TimeZone.self, from: encodedCurrent) + let decodedAutoupdatingCurrent = try JSONDecoder().decode(TimeZone.self, from: encodedAutoupdatingCurrent) + + #expect(decodedCurrent.identifier == "GMT") + #expect(decodedAutoupdatingCurrent.identifier == "America/Los_Angeles") + } } } }