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
28 changes: 25 additions & 3 deletions Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
}
}
}

36 changes: 36 additions & 0 deletions Sources/FoundationEssentials/Locale/Locale+Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down
11 changes: 4 additions & 7 deletions Sources/FoundationEssentials/Locale/Locale_Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [:]
Expand Down Expand Up @@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down