Skip to content

Commit 90ef7f1

Browse files
authored
Implement Locale.Region category filtering methods (#1253)
1 parent 3ee8bc0 commit 90ef7f1

File tree

2 files changed

+233
-1
lines changed

2 files changed

+233
-1
lines changed

Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ extension Locale.Region {
263263

264264
internal static let _isoRegionCodes: [String] = {
265265
var status = U_ZERO_ERROR
266-
let types = [URGN_WORLD, URGN_CONTINENT, URGN_SUBCONTINENT, URGN_TERRITORY]
266+
let types = [URGN_WORLD, URGN_CONTINENT, URGN_SUBCONTINENT, URGN_TERRITORY, URGN_GROUPING]
267267
var codes: [String] = []
268268
for t in types {
269269
status = U_ZERO_ERROR
@@ -275,6 +275,182 @@ extension Locale.Region {
275275
}
276276
return codes
277277
}()
278+
279+
/// Categories of a region. See https://www.unicode.org/reports/tr35/tr35-35/tr35-info.html#Territory_Data
280+
@available(FoundationPreview 6.2, *)
281+
public struct Category: Codable, Sendable, Hashable, CustomDebugStringConvertible {
282+
public var debugDescription: String {
283+
switch inner {
284+
case .world:
285+
return "world"
286+
case .continent:
287+
return "continent"
288+
case .subcontinent:
289+
return "subcontinent"
290+
case .territory:
291+
return "territory"
292+
case .grouping:
293+
return "grouping"
294+
}
295+
}
296+
297+
enum Inner {
298+
case world
299+
case continent
300+
case subcontinent
301+
case territory
302+
case grouping
303+
}
304+
305+
var inner: Inner
306+
fileprivate init(_ inner: Inner) {
307+
self.inner = inner
308+
}
309+
310+
var uregionType: URegionType {
311+
switch inner {
312+
case .world:
313+
return URGN_WORLD
314+
case .continent:
315+
return URGN_CONTINENT
316+
case .subcontinent:
317+
return URGN_SUBCONTINENT
318+
case .territory:
319+
return URGN_TERRITORY
320+
case .grouping:
321+
return URGN_GROUPING
322+
}
323+
}
324+
325+
fileprivate init?(uregionType: URegionType) {
326+
switch uregionType {
327+
case URGN_CONTINENT:
328+
self = .init(.continent)
329+
case URGN_WORLD:
330+
self = .init(.world)
331+
case URGN_SUBCONTINENT:
332+
self = .init(.subcontinent)
333+
case URGN_TERRITORY:
334+
self = .init(.territory)
335+
case URGN_GROUPING:
336+
self = .init(.grouping)
337+
default:
338+
return nil
339+
}
340+
}
341+
342+
/// Category representing the whold world.
343+
public static let world: Category = Category(.world)
344+
345+
/// Category representing a continent, regions contained directly by world.
346+
public static let continent: Category = Category(.continent)
347+
348+
/// Category representing a sub-continent, regions contained directly by a continent.
349+
public static let subcontinent: Category = Category(.subcontinent)
350+
351+
/// Category representing a territory.
352+
public static let territory: Category = Category(.territory)
353+
354+
/// Category representing a grouping, regions that has a well defined membership.
355+
public static let grouping: Category = Category(.grouping)
356+
357+
public init(from decoder: Decoder) throws {
358+
let container = try decoder.singleValueContainer()
359+
let inner: Inner
360+
switch try container.decode(Int.self) {
361+
case 0:
362+
inner = .world
363+
case 1:
364+
inner = .continent
365+
case 2:
366+
inner = .subcontinent
367+
case 3:
368+
inner = .territory
369+
case 4:
370+
inner = .grouping
371+
default:
372+
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown Category"))
373+
}
374+
self = .init(inner)
375+
}
376+
377+
public func encode(to encoder: Encoder) throws {
378+
var container = encoder.singleValueContainer()
379+
switch inner {
380+
case .world:
381+
try container.encode(0)
382+
case .continent:
383+
try container.encode(1)
384+
case .subcontinent:
385+
try container.encode(2)
386+
case .territory:
387+
try container.encode(3)
388+
case .grouping:
389+
try container.encode(4)
390+
391+
}
392+
}
393+
}
394+
395+
/// An array of regions matching the specified categories.
396+
@available(FoundationPreview 6.2, *)
397+
public static func isoRegions(ofCategory category: Category) -> [Locale.Region] {
398+
var status = U_ZERO_ERROR
399+
let values = uregion_getAvailable(category.uregionType, &status)
400+
guard let values, status.isSuccess else {
401+
return []
402+
}
403+
return ICU.Enumerator(enumerator: values).elements.map { Locale.Region($0) }
404+
}
405+
406+
/// The category of the region.
407+
@available(FoundationPreview 6.2, *)
408+
public var category: Category? {
409+
var status = U_ZERO_ERROR
410+
let icuRegion = uregion_getRegionFromCode(identifier, &status)
411+
guard status.isSuccess, let icuRegion else {
412+
return nil
413+
}
414+
let type = uregion_getType(icuRegion)
415+
return Category(uregionType: type)
416+
}
417+
418+
/// An array of the sub-regions, matching the specified category of the region.
419+
@available(FoundationPreview 6.2, *)
420+
public func subRegions(ofCategoy category: Category) -> [Locale.Region] {
421+
var status = U_ZERO_ERROR
422+
let icuRegion = uregion_getRegionFromCode(identifier, &status)
423+
guard let icuRegion, status.isSuccess else {
424+
return []
425+
}
426+
427+
status = U_ZERO_ERROR
428+
let enumerator = uregion_getContainedRegionsOfType(icuRegion, category.uregionType, &status)
429+
guard let enumerator, status.isSuccess else {
430+
return []
431+
}
432+
return ICU.Enumerator(enumerator: enumerator).elements.map { Locale.Region($0) }
433+
}
434+
435+
/// The subcontinent that contains this region, if any.
436+
@available(FoundationPreview 6.2, *)
437+
public var subcontinent: Locale.Region? {
438+
var status = U_ZERO_ERROR
439+
let icuRegion = uregion_getRegionFromCode(identifier, &status)
440+
guard let icuRegion, status.isSuccess else {
441+
return nil
442+
}
443+
444+
guard let containing = uregion_getContainingRegionOfType(icuRegion, URGN_SUBCONTINENT) else {
445+
return nil
446+
}
447+
448+
guard let code = String(validatingCString: uregion_getRegionCode(containing)) else {
449+
return nil
450+
}
451+
452+
return Locale.Region(code)
453+
}
278454
}
279455

280456
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Testing
14+
15+
#if FOUNDATION_FRAMEWORK
16+
import Foundation
17+
#else
18+
import FoundationEssentials
19+
import FoundationInternationalization
20+
#endif
21+
22+
@Suite("Locale.Region Tests")
23+
struct LocaleRegionTests {
24+
@Test func regionCategory() async throws {
25+
#expect(Locale.Region.unknown.category == nil)
26+
#expect(Locale.Region.world.category == .world)
27+
#expect(Locale.Region.unitedStates.category == .territory)
28+
#expect(Locale.Region("EU").category == .grouping)
29+
#expect(Locale.Region("not a region").category == nil)
30+
31+
let africa = Locale.Region("002")
32+
#expect(africa.category == .continent)
33+
34+
let continentOfSpain = try #require(Locale.Region.spain.continent)
35+
#expect(continentOfSpain.category == .continent)
36+
}
37+
38+
@Test func subcontinent() async throws {
39+
#expect(Locale.Region.unknown.subcontinent == nil)
40+
#expect(Locale.Region.world.subcontinent == nil)
41+
#expect(Locale.Region("not a region").subcontinent == nil)
42+
#expect(Locale.Region.argentina.subcontinent == Locale.Region("005"))
43+
}
44+
45+
@Test func subRegionOfCategory() async throws {
46+
#expect(Locale.Region.unknown.subRegions(ofCategoy: .world) == [])
47+
#expect(Locale.Region.unknown.subRegions(ofCategoy: .territory) == [])
48+
49+
#expect(Set(Locale.Region.world.subRegions(ofCategoy: .continent)) == Set(Locale.Region.isoRegions(ofCategory: .continent)))
50+
51+
#expect(Locale.Region.argentina.subRegions(ofCategoy: .continent) == [])
52+
#expect(Locale.Region.argentina.subRegions(ofCategoy: .territory) == Locale.Region.argentina.subRegions)
53+
54+
#expect(Locale.Region("not a region").subRegions(ofCategoy: .territory) == [])
55+
}
56+
}

0 commit comments

Comments
 (0)