diff --git a/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift b/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift index 5aadcc5d1..16a6b82d4 100644 --- a/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift +++ b/Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift @@ -13,15 +13,20 @@ import Benchmark import func Benchmark.blackHole -#if FOUNDATION_FRAMEWORK // This test uses CFString +#if os(macOS) && USE_PACKAGE +import FoundationEssentials +import FoundationInternationalization +#else import Foundation +#endif let benchmarks = { Benchmark.defaultConfiguration.maxIterations = 1_000 Benchmark.defaultConfiguration.maxDuration = .seconds(3) Benchmark.defaultConfiguration.scalingFactor = .kilo - Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .mallocCountTotal, .throughput] + Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .throughput, .peakMemoryResident, .peakMemoryResidentDelta] +#if FOUNDATION_FRAMEWORK let string1 = "aaA" as CFString let string2 = "AAĆ " as CFString let range1 = CFRange(location: 0, length: CFStringGetLength(string1)) @@ -34,5 +39,22 @@ let benchmarks = { CFStringCompareWithOptionsAndLocale(string1, string2, range1, .init(rawValue: 0), nsLocale) } } -} #endif + + let identifiers = Locale.availableIdentifiers + let allComponents = identifiers.map { Locale.Components(identifier: $0) } + Benchmark("LocaleInitFromComponents") { benchmark in + for components in allComponents { + let locale = Locale(components: components) + let components2 = Locale.Components(locale: locale) + let locale2 = Locale(components: components2) // cache hit + } + } + + Benchmark("LocaleComponentsInitIdentifer") { benchmark in + for identifier in identifiers { + let components = Locale.Components(identifier: identifier) + } + } +} + diff --git a/Sources/FoundationEssentials/Locale/Locale+Components.swift b/Sources/FoundationEssentials/Locale/Locale+Components.swift index a093b21ff..918100a8c 100644 --- a/Sources/FoundationEssentials/Locale/Locale+Components.swift +++ b/Sources/FoundationEssentials/Locale/Locale+Components.swift @@ -85,6 +85,42 @@ extension Locale { public init(languageCode: Locale.LanguageCode? = nil, script: Locale.Script? = nil, languageRegion: Locale.Region? = nil) { self.languageComponents = Language.Components(languageCode: languageCode, script: script, region: languageRegion) } + + // Returns an ICU-style identifier like "de_DE@calendar=gregorian" + // Must include every component stored by a `Locale.Components`, and be kept in sync with `init(identifier:)`. + package var icuIdentifier: String { + + var keywords = [(ICULegacyKey, String)]() + if let id = calendar?.cldrIdentifier { keywords.append((Calendar.Identifier.legacyKeywordKey, id)) } + if let id = collation?._normalizedIdentifier { keywords.append((Locale.Collation.legacyKeywordKey, id)) } + if let id = currency?._normalizedIdentifier { keywords.append((Locale.Currency.legacyKeywordKey, id)) } + if let id = numberingSystem?._normalizedIdentifier { keywords.append((Locale.NumberingSystem.legacyKeywordKey, id)) } + if let id = firstDayOfWeek?.rawValue { keywords.append((Locale.Weekday.legacyKeywordKey, id)) } + if let id = hourCycle?.rawValue { keywords.append((Locale.HourCycle.legacyKeywordKey, id)) } + if let id = measurementSystem?._normalizedIdentifier { keywords.append((Locale.MeasurementSystem.legacyKeywordKey, id)) } + // No need for redundant region keyword + if let region = region, region != languageComponents.region { + // rg keyword value is actually a subdivision code + keywords.append((Locale.Region.legacyKeywordKey, Locale.Subdivision.subdivision(for: region)._normalizedIdentifier)) + } + if let id = subdivision?._normalizedIdentifier { keywords.append((Locale.Subdivision.legacyKeywordKey, id)) } + if let id = timeZone?.identifier { keywords.append((TimeZone.legacyKeywordKey, id)) } + if let id = variant?._normalizedIdentifier { keywords.append((Locale.Variant.legacyKeywordKey, id)) } + + var locID = languageComponents.identifier + let keywordCounts = keywords.count + if keywordCounts > 0 { + locID.append("@") + } + + for (i, (key, val)) in keywords.enumerated() { + locID.append("\(key.key)=\(val)") + if i != keywordCounts - 1 { + locID.append(";") + } + } + return locID + } } } diff --git a/Sources/FoundationEssentials/Locale/Locale_Cache.swift b/Sources/FoundationEssentials/Locale/Locale_Cache.swift index 004ac1ae8..96b8c36e2 100644 --- a/Sources/FoundationEssentials/Locale/Locale_Cache.swift +++ b/Sources/FoundationEssentials/Locale/Locale_Cache.swift @@ -45,7 +45,7 @@ struct LocaleCache : Sendable, ~Copyable { } private var cachedFixedLocales: [String : any _LocaleProtocol] = [:] - private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol] = [:] + private var cachedFixedComponentsLocales: [String /*ICU identifier*/: any _LocaleProtocol] = [:] #if FOUNDATION_FRAMEWORK private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:] @@ -99,17 +99,14 @@ struct LocaleCache : Sendable, ~Copyable { #endif // FOUNDATION_FRAMEWORK - func fixedComponents(_ comps: Locale.Components) -> (any _LocaleProtocol)? { - cachedFixedComponentsLocales[comps] - } - mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol { - if let l = fixedComponents(comps) { + let identifier = comps.icuIdentifier + if let l = cachedFixedComponentsLocales[identifier] { return l } else { let new = _localeICUClass().init(components: comps) - cachedFixedComponentsLocales[comps] = new + cachedFixedComponentsLocales[identifier] = new return new } } diff --git a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift index b4cc25488..6aaf8f5e9 100644 --- a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift @@ -22,34 +22,7 @@ internal import _FoundationICU @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) extension Locale.Components { - // Returns an ICU-style identifier like "de_DE@calendar=gregorian" - internal var icuIdentifier: String { - var keywords: [ICULegacyKey: String] = [:] - if let id = calendar?.cldrIdentifier { keywords[Calendar.Identifier.legacyKeywordKey] = id } - if let id = collation?._normalizedIdentifier { keywords[Locale.Collation.legacyKeywordKey] = id } - if let id = currency?._normalizedIdentifier { keywords[Locale.Currency.legacyKeywordKey] = id } - if let id = numberingSystem?._normalizedIdentifier { keywords[Locale.NumberingSystem.legacyKeywordKey] = id } - if let id = firstDayOfWeek?.rawValue { keywords[Locale.Weekday.legacyKeywordKey] = id } - if let id = hourCycle?.rawValue { keywords[Locale.HourCycle.legacyKeywordKey] = id } - if let id = measurementSystem?._normalizedIdentifier { keywords[Locale.MeasurementSystem.legacyKeywordKey] = id } - // No need for redundant region keyword - if let region = region, region != languageComponents.region { - // rg keyword value is actually a subdivision code - keywords[Locale.Region.legacyKeywordKey] = Locale.Subdivision.subdivision(for: region)._normalizedIdentifier - } - if let id = subdivision?._normalizedIdentifier { keywords[Locale.Subdivision.legacyKeywordKey] = id } - if let id = timeZone?.identifier { keywords[TimeZone.legacyKeywordKey] = id } - if let id = variant?._normalizedIdentifier { keywords[Locale.Variant.legacyKeywordKey] = id } - - var locID = languageComponents.identifier - for (key, val) in keywords { - // This uses legacy key-value pairs, like "collation=phonebook" instead of "-cu-phonebk", so be sure that the above values are `legacyKeywordKey` - // See Locale.Components.legacyKey(forKey:) for more info on performance costs - locID = Locale.identifierWithKeywordValue(locID, key: key, value: val) - } - return locID - } - + /// - Parameter identifier: Unicode language identifier such as "en-u-nu-thai-ca-buddhist-kk-true" public init(identifier: String) { let languageComponents = Locale.Language.Components(identifier: identifier)