Skip to content

Implement Locale.Region category filtering methods #1253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ extension Locale.Region {

internal static let _isoRegionCodes: [String] = {
var status = U_ZERO_ERROR
let types = [URGN_WORLD, URGN_CONTINENT, URGN_SUBCONTINENT, URGN_TERRITORY]
let types = [URGN_WORLD, URGN_CONTINENT, URGN_SUBCONTINENT, URGN_TERRITORY, URGN_GROUPING]
var codes: [String] = []
for t in types {
status = U_ZERO_ERROR
Expand All @@ -302,6 +302,182 @@ extension Locale.Region {
}
return codes
}()

/// Categories of a region. See https://www.unicode.org/reports/tr35/tr35-35/tr35-info.html#Territory_Data
@available(FoundationPreview 6.2, *)
public struct Category: Codable, Sendable, Hashable, CustomDebugStringConvertible {
public var debugDescription: String {
switch inner {
case .world:
return "world"
case .continent:
return "continent"
case .subcontinent:
return "subcontinent"
case .territory:
return "territory"
case .grouping:
return "grouping"
}
}

enum Inner {
case world
case continent
case subcontinent
case territory
case grouping
}

var inner: Inner
fileprivate init(_ inner: Inner) {
self.inner = inner
}

var uregionType: URegionType {
switch inner {
case .world:
return URGN_WORLD
case .continent:
return URGN_CONTINENT
case .subcontinent:
return URGN_SUBCONTINENT
case .territory:
return URGN_TERRITORY
case .grouping:
return URGN_GROUPING
}
}

fileprivate init?(uregionType: URegionType) {
switch uregionType {
case URGN_CONTINENT:
self = .init(.continent)
case URGN_WORLD:
self = .init(.world)
case URGN_SUBCONTINENT:
self = .init(.subcontinent)
case URGN_TERRITORY:
self = .init(.territory)
case URGN_GROUPING:
self = .init(.grouping)
default:
return nil
}
}

/// Category representing the whold world.
public static let world: Category = Category(.world)

/// Category representing a continent, regions contained directly by world.
public static let continent: Category = Category(.continent)

/// Category representing a sub-continent, regions contained directly by a continent.
public static let subcontinent: Category = Category(.subcontinent)

/// Category representing a territory.
public static let territory: Category = Category(.territory)

/// Category representing a grouping, regions that has a well defined membership.
public static let grouping: Category = Category(.grouping)

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let inner: Inner
switch try container.decode(Int.self) {
case 0:
inner = .world
case 1:
inner = .continent
case 2:
inner = .subcontinent
case 3:
inner = .territory
case 4:
inner = .grouping
default:
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown Category"))
}
self = .init(inner)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch inner {
case .world:
try container.encode(0)
case .continent:
try container.encode(1)
case .subcontinent:
try container.encode(2)
case .territory:
try container.encode(3)
case .grouping:
try container.encode(4)

}
}
}

/// An array of regions matching the specified categories.
@available(FoundationPreview 6.2, *)
public static func isoRegions(ofCategory category: Category) -> [Locale.Region] {
var status = U_ZERO_ERROR
let values = uregion_getAvailable(category.uregionType, &status)
guard let values, status.isSuccess else {
return []
}
return ICU.Enumerator(enumerator: values).elements.map { Locale.Region($0) }
}

/// The category of the region.
@available(FoundationPreview 6.2, *)
public var category: Category? {
var status = U_ZERO_ERROR
let icuRegion = uregion_getRegionFromCode(identifier, &status)
guard status.isSuccess, let icuRegion else {
return nil
}
let type = uregion_getType(icuRegion)
return Category(uregionType: type)
}

/// An array of the sub-regions, matching the specified category of the region.
@available(FoundationPreview 6.2, *)
public func subRegions(ofCategoy category: Category) -> [Locale.Region] {
var status = U_ZERO_ERROR
let icuRegion = uregion_getRegionFromCode(identifier, &status)
guard let icuRegion, status.isSuccess else {
return []
}

status = U_ZERO_ERROR
let enumerator = uregion_getContainedRegionsOfType(icuRegion, category.uregionType, &status)
guard let enumerator, status.isSuccess else {
return []
}
return ICU.Enumerator(enumerator: enumerator).elements.map { Locale.Region($0) }
}

/// The subcontinent that contains this region, if any.
@available(FoundationPreview 6.2, *)
public var subcontinent: Locale.Region? {
var status = U_ZERO_ERROR
let icuRegion = uregion_getRegionFromCode(identifier, &status)
guard let icuRegion, status.isSuccess else {
return nil
}

guard let containing = uregion_getContainingRegionOfType(icuRegion, URGN_SUBCONTINENT) else {
return nil
}

guard let code = String(validatingCString: uregion_getRegionCode(containing)) else {
return nil
}

return Locale.Region(code)
}
}

@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
Expand Down
56 changes: 56 additions & 0 deletions Tests/FoundationInternationalizationTests/LocaleRegionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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
//
//===----------------------------------------------------------------------===//

import Testing

#if FOUNDATION_FRAMEWORK
import Foundation
#else
import FoundationEssentials
import FoundationInternationalization
#endif

@Suite("Locale.Region Tests")
struct LocaleRegionTests {
@Test func regionCategory() async throws {
#expect(Locale.Region.unknown.category == nil)
#expect(Locale.Region.world.category == .world)
#expect(Locale.Region.unitedStates.category == .territory)
#expect(Locale.Region("EU").category == .grouping)
#expect(Locale.Region("not a region").category == nil)

let africa = Locale.Region("002")
#expect(africa.category == .continent)

let continentOfSpain = try #require(Locale.Region.spain.continent)
#expect(continentOfSpain.category == .continent)
}

@Test func subcontinent() async throws {
#expect(Locale.Region.unknown.subcontinent == nil)
#expect(Locale.Region.world.subcontinent == nil)
#expect(Locale.Region("not a region").subcontinent == nil)
#expect(Locale.Region.argentina.subcontinent == Locale.Region("005"))
}

@Test func subRegionOfCategory() async throws {
#expect(Locale.Region.unknown.subRegions(ofCategoy: .world) == [])
#expect(Locale.Region.unknown.subRegions(ofCategoy: .territory) == [])

#expect(Set(Locale.Region.world.subRegions(ofCategoy: .continent)) == Set(Locale.Region.isoRegions(ofCategory: .continent)))

#expect(Locale.Region.argentina.subRegions(ofCategoy: .continent) == [])
#expect(Locale.Region.argentina.subRegions(ofCategoy: .territory) == Locale.Region.argentina.subRegions)

#expect(Locale.Region("not a region").subRegions(ofCategoy: .territory) == [])
}
}